// 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); // Filter out invalid coordinates (touch panel can return garbage on release) // Ignore both press and release transitions when coordinates are out of bounds bool validCoords = !(x < 0 || x > DISP_W || y < 0 || y > DISP_H); if(!validCoords) { pressed = false; // Don't update _lastTouch.pressed - keep previous state to avoid false release return evt; } // 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(const TouchEvent& evt, unsigned long holdMs) { HoldState state; if(!evt.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; case ScreenID::STATUS: if(_needsRedraw) { drawStatus(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 - use Layout for safe positioning _gfx->fillRect(0, 0, DISP_W, STYLE_HEADER_HEIGHT, STYLE_COLOR_HEADER); Layout header = Layout::header(DISP_W, STYLE_HEADER_HEIGHT); Layout safeText = header.padded(STYLE_SPACING_X); setBodyFont(); _gfx->setTextColor(STYLE_COLOR_FG); // Title with scrolling support _headerScroller.setText("KLUBHAUS"); _headerScroller.setScrollSpeed(80); _headerScroller.setPauseDuration(2000); _headerScroller.render( [&](int16_t x, const char* s) { _gfx->setCursor(safeText.x + x, safeText.y + 4); _gfx->print(s); }, safeText.w); // WiFi status - right aligned with scrolling Layout wifiArea(DISP_W - 150, 0, 140, STYLE_HEADER_HEIGHT); Layout safeWifi = wifiArea.padded(4); _wifiScroller.setText(state.wifiSsid.length() > 0 ? state.wifiSsid.c_str() : "WiFi: OFF"); _wifiScroller.setScrollSpeed(60); _wifiScroller.setPauseDuration(1500); _wifiScroller.render( [&](int16_t x, const char* s) { _gfx->setCursor(safeWifi.x + x, safeWifi.y + 4); _gfx->print(s); }, safeWifi.w); // Get tile layouts from library helper int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP); display.setHeaderHeight(STYLE_HEADER_HEIGHT); const TileLayout* layouts = display.getTileLayouts(); const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" }; const uint16_t tileColors[] = { 0x0280, 0x0400, 0x0440, 0x0100 }; for(int i = 0; i < tileCount && i < 4; 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, tileColors[i]); // Tile border _gfx->drawRoundRect(x, y, w, h, STYLE_TILE_RADIUS, STYLE_COLOR_FG); // Tile label _gfx->setTextColor(STYLE_COLOR_FG); setBodyFont(); int textLen = strlen(tileLabels[i]); int textW = textLen * 14; _gfx->setCursor(x + w / 2 - textW / 2, y + h / 2 - 10); _gfx->print(tileLabels[i]); } } void DisplayDriverGFX::drawStatus(const ScreenState& st) { _gfx->fillScreen(STYLE_COLOR_BG); // Header with title and back button _gfx->fillRect(0, 0, DISP_W, STYLE_HEADER_HEIGHT, STYLE_COLOR_HEADER); Layout header = Layout::header(DISP_W, STYLE_HEADER_HEIGHT); Layout safeText = header.padded(STYLE_SPACING_X); setTitleFont(); _gfx->setTextColor(STYLE_COLOR_FG); _gfx->setCursor(safeText.x, safeText.y + 4); _gfx->print("STATUS"); // Back button in lower-right setLabelFont(); _gfx->setCursor(DISP_W - 60, DISP_H - 20); _gfx->print("[BACK]"); // Status items in 2-column layout setBodyFont(); int colWidth = DISP_W / 2; int startY = STYLE_HEADER_HEIGHT + 30; int rowHeight = 35; int labelX = STYLE_SPACING_X + 10; int valueX = STYLE_SPACING_X + 120; // Column 1 int y = startY; // WiFi SSID _gfx->setTextColor(0x8888); _gfx->setCursor(labelX, y); _gfx->print("WiFi:"); _gfx->setTextColor(STYLE_COLOR_FG); _gfx->setCursor(valueX, y); _gfx->print(st.wifiSsid.length() > 0 ? st.wifiSsid.c_str() : "N/A"); // RSSI y += rowHeight; _gfx->setTextColor(0x8888); _gfx->setCursor(labelX, y); _gfx->print("Signal:"); _gfx->setTextColor(STYLE_COLOR_FG); _gfx->setCursor(valueX, y); _gfx->printf("%d dBm", st.wifiRssi); // IP Address y += rowHeight; _gfx->setTextColor(0x8888); _gfx->setCursor(labelX, y); _gfx->print("IP:"); _gfx->setTextColor(STYLE_COLOR_FG); _gfx->setCursor(valueX, y); _gfx->print(st.ipAddr.length() > 0 ? st.ipAddr.c_str() : "N/A"); // Column 2 y = startY; // Uptime uint32_t upSec = st.uptimeMs / 1000; uint32_t upMin = upSec / 60; uint32_t upHr = upMin / 60; upSec = upSec % 60; upMin = upMin % 60; _gfx->setTextColor(0x8888); _gfx->setCursor(colWidth + labelX, y); _gfx->print("Uptime:"); _gfx->setTextColor(STYLE_COLOR_FG); _gfx->setCursor(colWidth + valueX, y); _gfx->printf("%02lu:%02lu:%02lu", upHr, upMin, upSec); // Heap y += rowHeight; _gfx->setTextColor(0x8888); _gfx->setCursor(colWidth + labelX, y); _gfx->print("Heap:"); _gfx->setTextColor(STYLE_COLOR_FG); _gfx->setCursor(colWidth + valueX, y); _gfx->printf("%d KB", ESP.getFreeHeap() / 1024); // Last Poll y += rowHeight; _gfx->setTextColor(0x8888); _gfx->setCursor(colWidth + labelX, y); _gfx->print("Last Poll:"); _gfx->setTextColor(STYLE_COLOR_FG); _gfx->setCursor(colWidth + valueX, y); uint32_t pollAgo = (millis() - st.lastPollMs) / 1000; if(pollAgo < 60) { _gfx->printf("%lu sec", pollAgo); } else { _gfx->printf("%lu min", pollAgo / 60); } // Footer with firmware version setLabelFont(); _gfx->setTextColor(0x6666); _gfx->setCursor(STYLE_SPACING_X, DISP_H - STYLE_SPACING_Y); _gfx->print("v" FW_VERSION); } 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); }