// boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp #include "DisplayDriverGFX.h" #include "LovyanPins.h" #include "board_config.h" #include #include // ── Globals ── static LGFX* _gfx = nullptr; extern DisplayManager display; // ── Forward declarations ── static void initDisplay(); // ── Dimensions ── static constexpr int DISP_W = 800; static constexpr int DISP_H = 480; // ── Display initialization ── static void initDisplay() { Serial.println("LovyanGFX init..."); // Note: LovyanGFX handles I2C internally (port 1 for touch, port 0 for CH422G) // No need to call Wire.begin() or Wire1.begin() _gfx = new LGFX(); _gfx->init(); _gfx->setRotation(0); // Landscape _gfx->fillScreen(0x000000); Serial.println("Display ready"); } // ── Singleton ── DisplayDriverGFX& DisplayDriverGFX::instance() { static DisplayDriverGFX inst; return inst; } // ── IDisplayDriver implementation ── void DisplayDriverGFX::begin() { initDisplay(); // Turn on backlight immediately setBacklight(true); } void DisplayDriverGFX::setBacklight(bool on) { if(_gfx) { // LovyanGFX handles backlight via setBrightness _gfx->setBrightness(on ? 255 : 0); } } int DisplayDriverGFX::width() { return _gfx ? _gfx->width() : DISP_W; } int DisplayDriverGFX::height() { return _gfx ? _gfx->height() : DISP_H; } // ── Fonts ── // LovyanGFX built-in fonts for 800x480 display void DisplayDriverGFX::setTitleFont() { _gfx->setFont(&fonts::FreeSansBold24pt7b); } void DisplayDriverGFX::setBodyFont() { _gfx->setFont(&fonts::FreeSans18pt7b); } void DisplayDriverGFX::setLabelFont() { _gfx->setFont(&fonts::FreeSans12pt7b); } void DisplayDriverGFX::setDefaultFont() { _gfx->setFont(&fonts::Font2); } // Transform touch coordinates to match display orientation // GT911 touch panel on this board is rotated 180° relative to display void DisplayDriverGFX::transformTouch(int* x, int* y) { if(!x || !y) return; // Flip both axes: (0,0) becomes (width, height) *x = DISP_W - *x; *y = DISP_H - *y; } // Test harness: parse serial commands to inject synthetic touches // Commands: // TEST:touch x y press - simulate press at (x, y) [raw panel coords] // TEST:touch x y release - simulate release at (x, y) // TEST:touch clear - clear test mode bool DisplayDriverGFX::parseTestTouch(int* outX, int* outY, bool* outPressed) { if(!Serial.available()) return false; // Only consume if it starts with 'T' - don't steal other commands // Use "TEST:" prefix to avoid conflict with [CMD] echo if(Serial.peek() != 'T') { return false; } String cmd = Serial.readStringUntil('\n'); cmd.trim(); if(!cmd.startsWith("TEST:touch")) return false; // Parse: touch x y press|release int firstSpace = cmd.indexOf(' '); if(firstSpace < 0) return false; String args = cmd.substring(firstSpace + 1); args.trim(); if(args.equals("clear")) { _testMode = false; Serial.println("[TEST] Test mode cleared"); return false; } // Parse x y state int secondSpace = args.indexOf(' '); if(secondSpace < 0) return false; String xStr = args.substring(0, secondSpace); String yState = args.substring(secondSpace + 1); yState.trim(); int x = xStr.toInt(); int y = yState.substring(0, yState.indexOf(' ')).toInt(); String state = yState.substring(yState.indexOf(' ') + 1); state.trim(); bool pressed = state.equals("press"); Serial.printf("[TEST] Injecting touch: (%d,%d) %s\n", x, y, pressed ? "press" : "release"); if(outX) *outX = x; if(outY) *outY = y; if(outPressed) *outPressed = pressed; _testMode = true; return true; } // Touch handling TouchEvent DisplayDriverGFX::readTouch() { TouchEvent evt; if(!_gfx) return evt; // Check for test injection via serial int testX, testY; bool testPressed; if(parseTestTouch(&testX, &testY, &testPressed)) { // Handle test touch with same logic as real touch unsigned long now = millis(); if(testPressed && !_lastTouch.pressed) { evt.pressed = true; evt.downX = testX; evt.downY = testY; _lastTouch.downX = evt.downX; _lastTouch.downY = evt.downY; _touchBounced = false; } else if(!testPressed && _lastTouch.pressed) { evt.released = true; evt.downX = _lastTouch.downX; evt.downY = _lastTouch.downY; _lastReleaseMs = now; _touchBounced = true; } if(testPressed) { evt.x = testX; evt.y = testY; evt.downX = _lastTouch.downX; evt.downY = _lastTouch.downY; _pressStartMs = millis(); } if(_touchBounced && now - _lastReleaseMs >= TOUCH_DEBOUNCE_MS) { _touchBounced = false; } _lastTouch.pressed = testPressed; if(testPressed) { _lastTouch.x = evt.x; _lastTouch.y = evt.y; } return evt; } int32_t x, y; bool pressed = _gfx->getTouch(&x, &y); // Debounce: ignore repeated press events within debounce window after release unsigned long now = millis(); if(pressed && _touchBounced) { // Within debounce window - ignore this press _lastTouch.pressed = pressed; return evt; } // Detect transitions (press/release) if(pressed && !_lastTouch.pressed) { // Press transition: finger just touched down evt.pressed = true; evt.downX = static_cast(x); evt.downY = static_cast(y); _lastTouch.downX = evt.downX; _lastTouch.downY = evt.downY; _touchBounced = false; } else if(!pressed && _lastTouch.pressed) { // Release transition: finger just lifted evt.released = true; evt.downX = _lastTouch.downX; evt.downY = _lastTouch.downY; // Start debounce window _lastReleaseMs = now; _touchBounced = true; } // Current position if still touched if(pressed) { evt.x = static_cast(x); evt.y = static_cast(y); evt.downX = _lastTouch.downX; evt.downY = _lastTouch.downY; _pressStartMs = millis(); } // Check if debounce window has expired if(_touchBounced && now - _lastReleaseMs >= TOUCH_DEBOUNCE_MS) { _touchBounced = false; } // Track previous state _lastTouch.pressed = pressed; if(pressed) { _lastTouch.x = evt.x; _lastTouch.y = evt.y; } return evt; } int DisplayDriverGFX::dashboardTouch(int x, int y) { // Dashboard tiles: 2 rows × 4 columns constexpr int cols = 4; constexpr int rows = 2; constexpr int tileW = DISP_W / cols; constexpr int tileH = DISP_H / rows; if(x < 0 || x >= DISP_W || y < 0 || y >= DISP_H) { return -1; } int col = x / tileW; int row = y / tileH; return row * cols + col; } HoldState DisplayDriverGFX::updateHold(unsigned long holdMs) { HoldState state; if(!_lastTouch.pressed) { _isHolding = false; return state; } unsigned long elapsed = millis() - _pressStartMs; if(!_isHolding) { _isHolding = true; state.started = true; } state.active = true; state.progress = static_cast(elapsed) / static_cast(holdMs); if(state.progress >= 1.0f) { state.progress = 1.0f; state.completed = true; } return state; } // ── Rendering ── void DisplayDriverGFX::render(const ScreenState& state) { if(!_gfx) return; // Check if we need full redraw if(state.screen != _lastScreen || (state.screen == ScreenID::BOOT && state.bootStage != _lastBootStage)) { _needsRedraw = true; _lastScreen = state.screen; _lastBootStage = state.bootStage; } switch(state.screen) { case ScreenID::BOOT: if(_needsRedraw) { drawBoot(state); _needsRedraw = false; } break; case ScreenID::OFF: if(_needsRedraw) { _gfx->fillScreen(0x000000); _needsRedraw = false; } break; case ScreenID::ALERT: // Only redraw on first entry or screen change if(_needsRedraw) { drawAlert(state); _needsRedraw = false; } break; case ScreenID::DASHBOARD: if(_needsRedraw) { drawDashboard(state); _needsRedraw = false; } break; } } void DisplayDriverGFX::drawBoot(const ScreenState& state) { BootStage stage = state.bootStage; _gfx->fillScreen(TFT_BLACK); _gfx->setTextColor(STYLE_COLOR_FG); setTitleFont(); _gfx->setCursor(STYLE_SPACING_X, STYLE_SPACING_Y); _gfx->print("KLUBHAUS"); setBodyFont(); _gfx->setCursor(STYLE_SPACING_X, STYLE_SPACING_Y + STYLE_HEADER_HEIGHT); _gfx->print(BOARD_NAME); // Show boot stage status setLabelFont(); _gfx->setCursor(STYLE_SPACING_X, STYLE_SPACING_Y + STYLE_HEADER_HEIGHT + 30); switch(stage) { case BootStage::SPLASH: _gfx->print("Initializing..."); break; case BootStage::INIT_DISPLAY: _gfx->print("Display OK"); break; case BootStage::INIT_NETWORK: _gfx->print("Network init..."); break; case BootStage::CONNECTING_WIFI: _gfx->print("Connecting WiFi..."); break; case BootStage::READY: _gfx->print("All systems go!"); break; case BootStage::DONE: _gfx->print("Ready!"); break; } } void DisplayDriverGFX::drawAlert(const ScreenState& state) { uint32_t elapsed = millis() - state.alertStartMs; uint8_t pulse = static_cast(180.0f + 75.0f * sinf(elapsed / 300.0f)); uint16_t bg = _gfx->color565(pulse, 0, 0); _gfx->fillScreen(bg); _gfx->setTextColor(STYLE_COLOR_FG, bg); setTitleFont(); _gfx->setCursor(STYLE_SPACING_X, STYLE_HEADER_HEIGHT); _gfx->print(state.alertTitle.length() > 0 ? state.alertTitle.c_str() : "ALERT"); setBodyFont(); _gfx->setCursor(STYLE_SPACING_X, STYLE_HEADER_HEIGHT + 50); _gfx->print(state.alertBody); setLabelFont(); _gfx->setCursor(STYLE_SPACING_X, DISP_H - STYLE_SPACING_Y); _gfx->print("Hold to silence..."); } void DisplayDriverGFX::drawDashboard(const ScreenState& state) { _gfx->fillScreen(STYLE_COLOR_BG); // Header _gfx->fillRect(0, 0, DISP_W, STYLE_HEADER_HEIGHT, STYLE_COLOR_HEADER); setBodyFont(); _gfx->setTextColor(STYLE_COLOR_FG); _gfx->setCursor(STYLE_SPACING_X, 12); _gfx->printf("KLUBHAUS"); // WiFi status _gfx->setCursor(DISP_W - 120, 12); _gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF"); // Get tile layouts from library helper int tileCount = display.calculateDashboardLayouts(30, 8); const TileLayout* layouts = display.getTileLayouts(); const char* tileLabels[] = { "1", "2", "3", "4", "5", "6", "7", "8" }; for(int i = 0; i < tileCount && i < 8; i++) { const TileLayout& lay = layouts[i]; int x = lay.x; int y = lay.y; int w = lay.w; int h = lay.h; // Tile background _gfx->fillRoundRect(x, y, w, h, STYLE_TILE_RADIUS, 0x0220); // Tile border _gfx->drawRoundRect(x, y, w, h, STYLE_TILE_RADIUS, STYLE_COLOR_FG); // Tile label _gfx->setTextColor(STYLE_COLOR_FG); setBodyFont(); _gfx->setCursor(x + w / 2 - 30, y + h / 2 - 10); _gfx->print(tileLabels[i]); } } void DisplayDriverGFX::drawDebugTouch(int x, int y) { if(!_gfx) return; const int size = 20; _gfx->drawLine(x - size, y, x + size, y, TFT_RED); _gfx->drawLine(x, y - size, x, y + size, TFT_RED); }