446 lines
13 KiB
C++
446 lines
13 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 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) {
|
|
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);
|
|
|
|
// 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;
|
|
}
|