Files
klubhaus-doorbell/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp

586 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp
#include "DisplayDriverGFX.h"
#include "LovyanPins.h"
#include "board_config.h"
#include <Arduino.h>
#include <KlubhausCore.h>
// ── 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<int>(x);
evt.downY = static_cast<int>(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<int>(x);
evt.y = static_cast<int>(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<float>(elapsed) / static_cast<float>(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<uint8_t>(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);
}