#include "DisplayDriverTFT.h" #include #include #include extern DisplayManager display; // ── Fonts ─────────────────────────────────────────────────── // TFT_eSPI built-in fonts for 320x240 display (further scaled) // Using FreeFonts for better readability void DisplayDriverTFT::setTitleFont() { _tft.setFreeFont(&FreeSansBold12pt7b); } void DisplayDriverTFT::setBodyFont() { _tft.setFreeFont(&FreeSans9pt7b); } 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: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT); ScreenState st; st.screen = ScreenID::BOOT; st.bootStage = BootStage::SPLASH; drawBoot(st); digitalWrite(PIN_LCD_BL, HIGH); Serial.println("[GFX] Backlight ON"); } 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::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); _tft.setTextSize(2); _tft.setCursor(10, 10); _tft.printf("KLUBHAUS v%s", FW_VERSION); _tft.setTextSize(1); _tft.setCursor(10, 40); _tft.print(BOARD_NAME); // Show boot stage status _tft.setCursor(10, 70); 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) { uint32_t elapsed = millis() - st.alertStartMs; uint8_t pulse = 180 + (uint8_t)(75.0f * sinf(elapsed / 300.0f)); uint16_t bg = _tft.color565(pulse, 0, 0); _tft.fillScreen(bg); _tft.setTextColor(TFT_WHITE, bg); _tft.setTextSize(3); _tft.setCursor(10, 20); _tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT"); _tft.setTextSize(2); _tft.setCursor(10, 80); _tft.print(st.alertBody); _tft.setTextSize(1); _tft.setCursor(10, DISPLAY_HEIGHT - 20); _tft.print("Hold to silence..."); } void DisplayDriverTFT::drawDashboard(const ScreenState& st) { _tft.fillScreen(TFT_BLACK); _tft.setTextColor(TFT_WHITE, TFT_BLACK); _tft.setTextSize(1); _tft.setCursor(5, 5); _tft.printf("KLUBHAUS — %s", deviceStateStr(st.deviceState)); int y = 30; _tft.setCursor(5, y); y += 18; _tft.printf("WiFi: %s %ddBm", st.wifiSsid.c_str(), st.wifiRssi); _tft.setCursor(5, y); y += 18; _tft.printf("IP: %s", st.ipAddr.c_str()); _tft.setCursor(5, y); y += 18; _tft.printf("Up: %lus Heap: %d", st.uptimeMs / 1000, ESP.getFreeHeap()); _tft.setCursor(5, y); y += 18; _tft.printf("Last poll: %lus ago", st.lastPollMs > 0 ? (millis() - st.lastPollMs) / 1000 : 0); } // ── 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; return evt; } int DisplayDriverTFT::dashboardTouch(int x, int y) { // 2x2 grid, accounting for 30px header if(y < 30) return -1; int col = (x * 2) / DISPLAY_WIDTH; // 0 or 1 int row = ((y - 30) * 2) / (DISPLAY_HEIGHT - 30); // 0 or 1 if(col < 0 || col > 1 || row < 0 || row > 1) return -1; return row * 2 + col; // 0, 1, 2, or 3 } 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 barW = (int)(DISPLAY_WIDTH * h.progress); _tft.fillRect(0, DISPLAY_HEIGHT - 8, barW, 8, TFT_WHITE); _tft.fillRect(barW, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH - barW, 8, TFT_DARKGREY); } else { if(_holdActive) { // Clear the progress bar when released _tft.fillRect(0, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH, 8, TFT_DARKGREY); } _holdActive = false; } return h; } void DisplayDriverTFT::updateHint(int x, int y, bool active) { float period = active ? 500.0f : 2000.0f; float t = fmodf(millis(), period) / period; uint8_t v = static_cast(30.0f + 30.0f * sinf(t * 2.0f * PI)); uint16_t col = _tft.color565(v, v, v); _tft.drawRect(x - 40, y - 20, 80, 40, col); }