444 lines
12 KiB
C++
444 lines
12 KiB
C++
// 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);
|
||
|
||
// 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(unsigned long holdMs) {
|
||
HoldState state;
|
||
|
||
if(!_lastTouch.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;
|
||
}
|
||
}
|
||
|
||
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
|
||
_gfx->fillRect(0, 0, DISP_W, STYLE_HEADER_HEIGHT, STYLE_COLOR_HEADER);
|
||
setBodyFont();
|
||
_gfx->setTextColor(STYLE_COLOR_FG);
|
||
_gfx->setCursor(STYLE_SPACING_X, 12);
|
||
_gfx->printf("KLUBHAUS");
|
||
|
||
// WiFi status
|
||
_gfx->setCursor(DISP_W - 120, 12);
|
||
_gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF");
|
||
|
||
// Get tile layouts from library helper
|
||
int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP);
|
||
const TileLayout* layouts = display.getTileLayouts();
|
||
|
||
const char* tileLabels[] = { "1", "2", "3", "4", "5", "6", "7", "8" };
|
||
|
||
for(int i = 0; i < tileCount && i < 8; 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, 0x0220);
|
||
|
||
// Tile border
|
||
_gfx->drawRoundRect(x, y, w, h, STYLE_TILE_RADIUS, STYLE_COLOR_FG);
|
||
|
||
// Tile label
|
||
_gfx->setTextColor(STYLE_COLOR_FG);
|
||
setBodyFont();
|
||
_gfx->setCursor(x + w / 2 - 30, y + h / 2 - 10);
|
||
_gfx->print(tileLabels[i]);
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|