#include "DisplayDriverTFT.h" #include #include #include extern DisplayManager display; // ── Fonts ─────────────────────────────────────────────────── // TFT_eSPI built-in fonts for 320x480 display (scaled from 800x480) // Using FreeFonts - scaled bitmap fonts via setTextSize would be too pixelated // Note: FreeFonts are enabled via LOAD_GFXFF=1 in board-config.sh void DisplayDriverTFT::setTitleFont() { _tft.setFreeFont(&FreeSansBold18pt7b); } void DisplayDriverTFT::setBodyFont() { _tft.setFreeFont(&FreeSans12pt7b); } void DisplayDriverTFT::setLabelFont() { _tft.setFreeFont(&FreeSans9pt7b); } void DisplayDriverTFT::setDefaultFont() { _tft.setTextFont(2); } // ── Test harness ─────────────────────────────────────────────── // Test harness: parse serial commands to inject synthetic touches // Commands: // TEST:touch x y press - simulate press at (x, y) // TEST:touch x y release - simulate release at (x, y) // TEST:touch clear - clear test mode bool DisplayDriverTFT::parseTestTouch(int* outX, int* outY, bool* outPressed) { if(!Serial.available()) return false; if(Serial.peek() != 'T') { return false; } String cmd = Serial.readStringUntil('\n'); cmd.trim(); if(!cmd.startsWith("TEST:touch")) return false; 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; } 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; } void DisplayDriverTFT::begin() { // Backlight pinMode(PIN_LCD_BL, OUTPUT); digitalWrite(PIN_LCD_BL, LOW); _tft.init(); _tft.setRotation(DISPLAY_ROTATION); _tft.fillScreen(TFT_BLACK); Serial.printf("[GFX] Display OK: const %dx%d, tft %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT, _tft.width(), _tft.height()); Serial.flush(); // Debug: check if touch controller is responding uint16_t z = _tft.getTouchRawZ(); Serial.printf("[TOUCH] Raw Z=%d (non-zero = controller detected)\n", z); Serial.flush(); ScreenState st; st.screen = ScreenID::BOOT; st.bootStage = BootStage::SPLASH; drawBoot(st); digitalWrite(PIN_LCD_BL, HIGH); Serial.println("[GFX] Backlight ON"); Serial.flush(); } void DisplayDriverTFT::setBacklight(bool on) { digitalWrite(PIN_LCD_BL, on ? HIGH : LOW); } // ── Rendering ─────────────────────────────────────────────── void DisplayDriverTFT::render(const ScreenState& st) { if(st.screen != _lastScreen || (st.screen == ScreenID::BOOT && st.bootStage != _lastBootStage)) { _needsRedraw = true; _lastScreen = st.screen; _lastBootStage = st.bootStage; } switch(st.screen) { case ScreenID::BOOT: if(_needsRedraw) { drawBoot(st); _needsRedraw = false; } break; case ScreenID::ALERT: drawAlert(st); break; case ScreenID::DASHBOARD: if(_needsRedraw) { drawDashboard(st); _needsRedraw = false; } break; case ScreenID::STATUS: if(_needsRedraw) { drawStatus(st); _needsRedraw = false; } break; case ScreenID::OFF: if(_needsRedraw) { _tft.fillScreen(TFT_BLACK); _needsRedraw = false; } break; } } void DisplayDriverTFT::drawBoot(const ScreenState& st) { BootStage stage = st.bootStage; _tft.fillScreen(TFT_BLACK); _tft.setTextColor(TFT_WHITE, TFT_BLACK); setTitleFont(); _tft.setCursor(10, 28); // y=28 baseline accounts for ~18px font height above baseline _tft.print("KLUBHAUS"); setBodyFont(); _tft.setCursor(10, 55); // y adjusted for ~12px font _tft.print(BOARD_NAME); // Show boot stage status setLabelFont(); _tft.setCursor(10, 85); // y adjusted for ~9px label font switch(stage) { case BootStage::SPLASH: _tft.print("Initializing..."); break; case BootStage::INIT_DISPLAY: _tft.print("Display OK"); break; case BootStage::INIT_NETWORK: _tft.print("Network init..."); break; case BootStage::CONNECTING_WIFI: _tft.print("Connecting WiFi..."); break; case BootStage::READY: _tft.print("All systems go!"); break; case BootStage::DONE: _tft.print("Ready!"); break; } } void DisplayDriverTFT::drawAlert(const ScreenState& st) { // Throttle redraws to ~24fps to prevent tearing uint32_t now = millis(); if(now - _lastAlertDrawMs < ALERT_DRAW_INTERVAL_MS) { return; } _lastAlertDrawMs = now; uint32_t elapsed = millis() - st.alertStartMs; // Slower pulse - divide by 1500 for ~9.4 second cycle (was 300 for ~1.9s) uint8_t pulse = 180 + (uint8_t)(75.0f * sinf(elapsed / 1500.0f)); uint16_t bg = _tft.color565(pulse, 0, 0); _tft.fillScreen(bg); _tft.setTextColor(TFT_WHITE, bg); // Progressive fill hint - draws dark overlay from bottom rising up if(_alertTouchDown) { uint32_t touchElapsed = millis() - _alertTouchStartMs; float progress = (float)touchElapsed / (float)ALERT_FILL_DURATION_MS; if(progress > 1.0f) progress = 1.0f; int dispH = _tft.height(); int fillHeight = (int)(dispH * progress); if(fillHeight > 0) { // Draw dark overlay from bottom uint16_t overlay = _tft.color565(80, 0, 0); // Dark red _tft.fillRect(0, dispH - fillHeight, _tft.width(), fillHeight, overlay); } } setTitleFont(); _tft.setCursor(10, 28); // y=28 baseline for ~18px font _tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT"); setBodyFont(); _tft.setCursor(10, 70); // y adjusted for ~12px body font _tft.print(st.alertBody); setLabelFont(); _tft.setCursor(10, _tft.height() - 10); // y adjusted for ~9px label font _tft.print("Hold to silence..."); } void DisplayDriverTFT::drawDashboard(const ScreenState& st) { _tft.fillScreen(TFT_BLACK); // Use actual display dimensions (after rotation) int dispW = _tft.width(); int dispH = _tft.height(); // Header - using standard bitmap font for reliable positioning _tft.fillRect(0, 0, dispW, STYLE_HEADER_HEIGHT, 0x1A1A); // Dark gray header _tft.setTextSize(1); _tft.setTextColor(TFT_WHITE); _tft.setCursor(5, 20); // y=28 is baseline, text sits above this _tft.print("KLUBHAUS"); // WiFi indicator - right aligned in header const char* wifiText = st.wifiSsid.length() > 0 ? "WiFi:ON" : "WiFi:OFF"; int wifiW = _tft.textWidth(wifiText); _tft.setCursor(dispW - wifiW - 10, 20); _tft.print(wifiText); // Get tile layouts from library helper int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP); 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 _tft.fillRoundRect(x, y, w, h, 8, tileColors[i]); // Tile border _tft.drawRoundRect(x, y, w, h, 8, TFT_WHITE); // Tile label setBodyFont(); _tft.setTextColor(TFT_WHITE); int textLen = strlen(tileLabels[i]); int textW = textLen * 12; _tft.setCursor(x + w / 2 - textW / 2, y + h / 2 - 10); _tft.print(tileLabels[i]); } } void DisplayDriverTFT::drawStatus(const ScreenState& st) { int dispW = _tft.width(); int dispH = _tft.height(); _tft.fillScreen(TFT_BLACK); // Header _tft.fillRect(0, 0, dispW, STYLE_HEADER_HEIGHT, 0x1A1A); _tft.setTextSize(1); _tft.setTextColor(TFT_WHITE); _tft.setCursor(5, 20); _tft.print("STATUS"); // Back button in lower right _tft.setCursor(dispW - 60, dispH - 20); _tft.print("[BACK]"); // Status info setBodyFont(); int y = STYLE_HEADER_HEIGHT + 20; // WiFi _tft.setCursor(10, y); _tft.printf("WiFi: %s", st.wifiSsid.length() > 0 ? st.wifiSsid.c_str() : "N/A"); y += 20; _tft.setCursor(10, y); _tft.printf("RSSI: %d dBm", st.wifiRssi); y += 20; _tft.setCursor(10, y); _tft.printf("IP: %s", st.ipAddr.length() > 0 ? st.ipAddr.c_str() : "N/A"); y += 30; // Uptime uint32_t upSec = st.uptimeMs / 1000; uint32_t upMin = upSec / 60; uint32_t upHr = upMin / 60; upSec = upSec % 60; upMin = upMin % 60; _tft.setCursor(10, y); _tft.printf("Uptime: %02lu:%02lu:%02lu", upHr, upMin, upSec); y += 20; // Heap _tft.setCursor(10, y); _tft.printf("Heap: %d bytes", ESP.getFreeHeap()); y += 20; // Last poll uint32_t pollAgo = (millis() - st.lastPollMs) / 1000; _tft.setCursor(10, y); _tft.printf("Last poll: %lu sec ago", pollAgo); } // ── Touch ─────────────────────────────────────────────────── TouchEvent DisplayDriverTFT::readTouch() { TouchEvent evt; // Check for test injection via serial int testX, testY; bool testPressed; if(parseTestTouch(&testX, &testY, &testPressed)) { if(testPressed && !_touchWasPressed) { evt.pressed = true; _touchDownX = testX; _touchDownY = testY; evt.downX = _touchDownX; evt.downY = _touchDownY; } else if(!testPressed && _touchWasPressed) { evt.released = true; evt.downX = _touchDownX; evt.downY = _touchDownY; } if(testPressed) { evt.x = testX; evt.y = testY; evt.downX = _touchDownX; evt.downY = _touchDownY; } _touchWasPressed = testPressed; return evt; } uint16_t tx, ty; uint8_t touched = _tft.getTouch(&tx, &ty, 100); // Detect transitions (press/release) if(touched && !_touchWasPressed) { // Press transition: finger just touched down evt.pressed = true; _touchDownX = tx; _touchDownY = ty; evt.downX = _touchDownX; evt.downY = _touchDownY; } else if(!touched && _touchWasPressed) { // Release transition: finger just lifted evt.released = true; evt.downX = _touchDownX; evt.downY = _touchDownY; } // Current position if still touched if(touched) { evt.x = tx; evt.y = ty; evt.downX = _touchDownX; evt.downY = _touchDownY; } // Track previous state for next call _touchWasPressed = touched; // Track alert touch for progressive hint if(evt.pressed) { _alertTouchDown = true; _alertTouchStartMs = millis(); } else if(evt.released) { _alertTouchDown = false; } return evt; } void DisplayDriverTFT::transformTouch(int* x, int* y) { // Resistive touch panel is rotated 90° vs display - swap coordinates int temp = *x; *x = *y; *y = temp; } HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) { HoldState h; TouchEvent t = readTouch(); if(t.pressed) { if(!_holdActive) { _holdActive = true; _holdStartMs = millis(); h.started = true; } uint32_t held = millis() - _holdStartMs; h.active = true; h.progress = constrain((float)held / (float)holdMs, 0.0f, 1.0f); h.completed = (held >= holdMs); // Simple progress bar at bottom of screen int dispW = _tft.width(); int dispH = _tft.height(); int barW = (int)(dispW * h.progress); _tft.fillRect(0, dispH - 8, barW, 8, TFT_WHITE); _tft.fillRect(barW, dispH - 8, dispW - barW, 8, TFT_DARKGREY); } else { if(_holdActive) { // Clear the progress bar when released _tft.fillRect(0, _tft.height() - 8, _tft.width(), 8, TFT_DARKGREY); } _holdActive = false; } return h; }