Files
klubhaus-doorbell/boards/esp32-32e/DisplayDriverTFT.cpp

335 lines
9.3 KiB
C++

#include "DisplayDriverTFT.h"
#include <Arduino.h>
#include <KlubhausCore.h>
#include <TFT_eSPI.h>
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, 10);
_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::drawDebugTouch(int x, int y) {
const int size = 20;
_tft.drawLine(x - size, y, x + size, y, TFT_RED);
_tft.drawLine(x, y - size, x, y + size, TFT_RED);
}