From 67613120ad41e452fee724dbff6a69e689e09522 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Wed, 18 Feb 2026 04:28:35 -0800 Subject: [PATCH] refactor(display): extract dashboard tile grid logic to DisplayManager --- .clangd | 46 +++----- boards/esp32-32e-4/DisplayDriverTFT.cpp | 80 ++++++++++---- boards/esp32-32e-4/DisplayDriverTFT.h | 5 +- boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp | 16 +++ boards/esp32-s3-lcd-43/DisplayDriverGFX.h | 4 +- libraries/KlubhausCore/src/DisplayManager.h | 110 ++++++++++++++++++- libraries/KlubhausCore/src/DoorbellLogic.cpp | 27 ++++- libraries/KlubhausCore/src/IDisplayDriver.h | 8 +- libraries/KlubhausCore/src/ScreenState.h | 25 +++++ scripts/monitor-agent.py | 2 +- 10 files changed, 259 insertions(+), 64 deletions(-) diff --git a/.clangd b/.clangd index 4d43dbe..2e15213 100644 --- a/.clangd +++ b/.clangd @@ -1,36 +1,26 @@ CompileFlags: Add: - - "-std=c++17" - - "-DARDUINO=200" - - "-DESP32" - - "-DCORE_DEBUG_LEVEL=0" - - "-DBOARD_HAS_PSRAM" - - "-DLGFX_USE_V1" - - "-DDEBUG_MODE" - "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32" - - "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/tools" - - "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/libraries" - - "-I/home/david/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include" - - "-I/home/david/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/freertos/FreeRTOS-Kernel/include" - - "-I/home/david/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/freertos/config/include/freertos" - - "-I/home/david/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/freertos/config/include" - - "-I/home/david/.arduino15/packages/arduino/hardware/arduino/1.8.6/cores/arduino" - - "-I/home/david/.arduino15/packages/arduino/hardware/arduino/1.8.6/libraries/WiFi/src" - - "-I/home/david/Arduino/sketchbook/libraries/ArduinoJson/src" - - "-I/home/david/Arduino/sketchbook/libraries/NTPClient" + - "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/tools/sdk/esp32/include" + - "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/tools/sdk/esp32/include/esp_hw_support" + - "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/WiFi/src" + - "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/EEPROM/src" + - "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-s3-lcd-43/LovyanGFX/src" + - "-I/home/david/Arduino/libraries/TFT_eSPI" - "-I/home/david/Arduino/sketches/doorbell-touch/libraries/KlubhausCore/src" - - "-I/home/david/Arduino/sketches/doorbell-touch/boards/esp32-s3-lcd-43" - - "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-s3-lcd-43/LovyanGFX/src/lgfx" - - "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-s3-lcd-43/LovyanGFX/src/lgfx/v0" - - "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-s3-lcd-43/LovyanGFX/src/lgfx/v0/platforms/esp32" - - "-I/home/david/Arduino/sketches/doorbell-touch/boards/esp32-32e" - "-I/home/david/Arduino/sketches/doorbell-touch/boards/esp32-32e-4" - - "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-32e/TFT_eSPI" - - "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-32e-4/TFT_eSPI" - - "-I/home/david/Arduino/sketchbook/libraries/XPT2046_Touchscreen" + - "-DARDUINO=200" + - "-DESC32" + - "-DESP_PLATFORM" + - "-Dcore_debug=0" Diagnostics: ClangTidy: - Remove: [readability-*, modernize-*, performance-*, bugprone-*] - Add: [clang-diagnostic-*, modernize-use-trailing-return-type] - UnusedIncludes: Strict + Add: + - modernize-* + - performance-* + - readability-* + - bugprone-* + Remove: + - modernize-use-trailing-return-type + - readability-magic-numbers diff --git a/boards/esp32-32e-4/DisplayDriverTFT.cpp b/boards/esp32-32e-4/DisplayDriverTFT.cpp index 75f1bb3..40a6e52 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.cpp +++ b/boards/esp32-32e-4/DisplayDriverTFT.cpp @@ -123,28 +123,51 @@ void DisplayDriverTFT::drawAlert(const ScreenState& st) { void DisplayDriverTFT::drawDashboard(const ScreenState& st) { _tft.fillScreen(TFT_BLACK); - _tft.setTextColor(TFT_WHITE, TFT_BLACK); + // Header + _tft.setTextColor(TFT_WHITE, TFT_BLACK); _tft.setTextSize(1); _tft.setCursor(5, 5); - _tft.printf("KLUBHAUS — %s", deviceStateStr(st.deviceState)); + _tft.printf("KLUBHAUS"); - int y = 30; - _tft.setCursor(5, y); - y += 18; - _tft.printf("WiFi: %s %ddBm", st.wifiSsid.c_str(), st.wifiRssi); + // WiFi indicator + _tft.setCursor(DISPLAY_WIDTH - 50, 5); + _tft.printf("WiFi:%s", st.wifiSsid.length() > 0 ? "ON" : "OFF"); - _tft.setCursor(5, y); - y += 18; - _tft.printf("IP: %s", st.ipAddr.c_str()); + // Render tiles via DisplayManager (abstracted to library) + // For now, draw directly using our tile implementation + constexpr int cols = 2; + constexpr int rows = 2; + constexpr int tileW = DISPLAY_WIDTH / cols; + constexpr int tileH = (DISPLAY_HEIGHT - 30) / rows; + constexpr int margin = 8; - _tft.setCursor(5, y); - y += 18; - _tft.printf("Up: %lus Heap: %d", st.uptimeMs / 1000, ESP.getFreeHeap()); + const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" }; + const uint16_t tileColors[] = { 0x0280, 0x0400, 0x0440, 0x0100 }; - _tft.setCursor(5, y); - y += 18; - _tft.printf("Last poll: %lus ago", st.lastPollMs > 0 ? (millis() - st.lastPollMs) / 1000 : 0); + for(int i = 0; i < 4; i++) { + int col = i % cols; + int row = i / cols; + + int x = col * tileW + margin; + int y = 30 + row * tileH + margin; + int w = tileW - 2 * margin; + int h = tileH - 2 * margin; + + // Tile background + _tft.fillRoundRect(x, y, w, h, 8, tileColors[i]); + + // Tile border + _tft.drawRoundRect(x, y, w, h, 8, TFT_WHITE); + + // Tile label + _tft.setTextColor(TFT_WHITE); + _tft.setTextSize(2); + int textLen = strlen(tileLabels[i]); + int textW = textLen * 12; + _tft.setCursor(x + w/2 - textW/2, y + h/2 - 10); + _tft.print(tileLabels[i]); + } } // ── Touch ─────────────────────────────────────────────────── @@ -166,18 +189,27 @@ uint16_t DisplayDriverTFT::getRawTouchZ() { return _tft.getTouchRawZ(); } -int DisplayDriverTFT::dashboardTouch(int x, int y) { - // 2x2 grid, accounting for 30px header - if(y < 30) - return -1; +void DisplayDriverTFT::transformTouch(int* x, int* y) { + // Resistive touch panel is rotated 90° vs display - swap coordinates + int temp = *x; + *x = *y; + *y = temp; +} - int col = (x * 2) / DISPLAY_WIDTH; // 0 or 1 - int row = ((y - 30) * 2) / (DISPLAY_HEIGHT - 30); // 0 or 1 +void DisplayDriverTFT::drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) { + // Tile background + _tft.fillRoundRect(x, y, w, h, 8, bgColor); - if(col < 0 || col > 1 || row < 0 || row > 1) - return -1; + // Tile border + _tft.drawRoundRect(x, y, w, h, 8, TFT_WHITE); - return row * 2 + col; // 0, 1, 2, or 3 + // Tile label + _tft.setTextColor(TFT_WHITE); + _tft.setTextSize(2); + int textLen = strlen(label); + int textW = textLen * 12; // approx width + _tft.setCursor(x + w/2 - textW/2, y + h/2 - 10); + _tft.print(label); } HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) { diff --git a/boards/esp32-32e-4/DisplayDriverTFT.h b/boards/esp32-32e-4/DisplayDriverTFT.h index c2fbecc..2dc2abc 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.h +++ b/boards/esp32-32e-4/DisplayDriverTFT.h @@ -12,12 +12,15 @@ public: void render(const ScreenState& state) override; TouchEvent readTouch() override; uint16_t getRawTouchZ(); - int dashboardTouch(int x, int y) override; HoldState updateHold(unsigned long holdMs) override; void updateHint(int x, int y, bool active) override; int width() override { return DISPLAY_WIDTH; } int height() override { return DISPLAY_HEIGHT; } + // Dashboard tiles - library handles grid math, we just draw + void transformTouch(int* x, int* y) override; + void drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) override; + private: void drawBoot(const ScreenState& st); void drawAlert(const ScreenState& st); diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp index 45d8178..235c6a7 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp @@ -96,6 +96,22 @@ int DisplayDriverGFX::dashboardTouch(int x, int y) { return row * cols + col; } +void DisplayDriverGFX::drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) { + // Tile background (use fillRect - LovyanGFX may not have fillRoundRect) + _gfx->fillRect(x, y, w, h, bgColor); + + // Tile border + _gfx->drawRect(x, y, w, h, 0xFFFF); + + // Tile label + _gfx->setTextColor(0xFFFF); + _gfx->setTextSize(2); + int textLen = strlen(label); + int textW = textLen * 12; + _gfx->setCursor(x + w/2 - textW/2, y + h/2 - 10); + _gfx->print(label); +} + HoldState DisplayDriverGFX::updateHold(unsigned long holdMs) { HoldState state; diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h index 991579b..d5d89d6 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h @@ -12,13 +12,15 @@ public: void render(const ScreenState& state) override; TouchEvent readTouch() override; - int dashboardTouch(int x, int y) override; HoldState updateHold(unsigned long holdMs) override; void updateHint(int x, int y, bool active) override; int width() override; int height() override; + // Dashboard tiles - library handles grid math, we just draw + void drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) override; + // ── Internal ── static DisplayDriverGFX& instance(); diff --git a/libraries/KlubhausCore/src/DisplayManager.h b/libraries/KlubhausCore/src/DisplayManager.h index 5295199..fb6e05d 100644 --- a/libraries/KlubhausCore/src/DisplayManager.h +++ b/libraries/KlubhausCore/src/DisplayManager.h @@ -1,6 +1,7 @@ #pragma once #include "IDisplayDriver.h" +#include "ScreenState.h" class DisplayManager { public: @@ -24,8 +25,6 @@ public: TouchEvent readTouch() { return _drv ? _drv->readTouch() : TouchEvent {}; } - int dashboardTouch(int x, int y) { return _drv ? _drv->dashboardTouch(x, y) : -1; } - HoldState updateHold(unsigned long ms) { return _drv ? _drv->updateHold(ms) : HoldState {}; } void updateHint(int x, int y, bool active) { @@ -34,9 +33,114 @@ public: } int width() { return _drv ? _drv->width() : 0; } - int height() { return _drv ? _drv->height() : 0; } + /// Auto-calculate grid dimensions for dashboard tiles + /// Prefers landscape (more columns than rows) for wide displays + void getTileGrid(int tileCount, int* outRows, int* outCols) const { + if(tileCount <= 0) { + *outRows = *outCols = 0; + return; + } + // Calculate cols: try to make it wider than tall + int cols = (int)sqrt(tileCount); + if(cols * cols < tileCount) cols++; + + // Make cols >= rows for landscape preference + while(cols * ((tileCount + cols - 1) / cols) < tileCount) { + cols++; + } + + int rows = (tileCount + cols - 1) / cols; + + // If display is wider, prefer more columns + if(_drv) { + int w = _drv->width(); + int h = _drv->height(); + if(w > h) { + // Landscape display - maximize columns + cols = tileCount; + rows = 1; + } + } + + *outRows = rows; + *outCols = cols; + } + + /// Render all dashboard tiles with auto-calculated grid + void renderDashboard(const ScreenState& st) { + if(!_drv) return; + + int rows, cols; + getTileGrid(DASHBOARD_TILE_COUNT, &rows, &cols); + + int dispW = _drv->width(); + int dispH = _drv->height(); + + // Reserve space for header + int headerH = 30; + int contentH = dispH - headerH; + + // Calculate tile sizes + int tileW = dispW / cols; + int tileH = contentH / rows; + int margin = 8; + + for(int i = 0; i < DASHBOARD_TILE_COUNT; i++) { + int row = i / cols; + int col = i % cols; + + int x = col * tileW + margin; + int y = headerH + row * tileH + margin; + int w = tileW - 2 * margin; + int h = tileH - 2 * margin; + + _drv->drawTileAt(x, y, w, h, DASHBOARD_TILES[i].label, DASHBOARD_TILES[i].bgColor); + } + + // Store grid for touch calculations + _gridRows = rows; + _gridCols = cols; + } + + /// Handle dashboard touch - returns action for tapped tile, or NONE + TileAction handleDashboardTouch(int x, int y) const { + if(!_drv || _gridCols <= 0) return TileAction::NONE; + + // Transform touch coordinates (handles rotated touch panels) + _drv->transformTouch(&x, &y); + + int dispW = _drv->width(); + int dispH = _drv->height(); + int headerH = 30; + + // Check if in header area + if(y < headerH) return TileAction::NONE; + + // Calculate which tile was touched + int tileW = dispW / _gridCols; + int contentH = dispH - headerH; + int tileH = contentH / _gridRows; + + int col = x / tileW; + int row = (y - headerH) / tileH; + + // Bounds check + if(col < 0 || col >= _gridCols || row < 0 || row >= _gridRows) { + return TileAction::NONE; + } + + int index = row * _gridCols + col; + if(index < 0 || index >= DASHBOARD_TILE_COUNT) { + return TileAction::NONE; + } + + return DASHBOARD_TILES[index].action; + } + private: IDisplayDriver* _drv; + mutable int _gridRows = 0; + mutable int _gridCols = 0; }; diff --git a/libraries/KlubhausCore/src/DoorbellLogic.cpp b/libraries/KlubhausCore/src/DoorbellLogic.cpp index c2db148..04c31d0 100644 --- a/libraries/KlubhausCore/src/DoorbellLogic.cpp +++ b/libraries/KlubhausCore/src/DoorbellLogic.cpp @@ -323,11 +323,30 @@ int DoorbellLogic::handleTouch(const TouchEvent& evt) { } if(_state.screen == ScreenID::DASHBOARD) { - int tile = _display->dashboardTouch(evt.x, evt.y); - if(tile >= 0) { - Serial.printf("[DASH] Tile %d tapped\n", tile); + TileAction action = _display->handleDashboardTouch(evt.x, evt.y); + if(action != TileAction::NONE) { + Serial.printf("[DASH] Action: %d\n", (int)action); + switch(action) { + case TileAction::ALERT: + onAlert("Manual Alert", "Tile tap"); + break; + case TileAction::SILENCE: + if(_state.deviceState == DeviceState::ALERTING) + silenceAlert(); + break; + case TileAction::STATUS: + heartbeat(); + break; + case TileAction::REBOOT: + flushStatus("REBOOT (tile)"); + delay(500); + ESP.restart(); + break; + default: + break; + } } - return tile; + return (int)action; } if(_state.screen == ScreenID::ALERT) { diff --git a/libraries/KlubhausCore/src/IDisplayDriver.h b/libraries/KlubhausCore/src/IDisplayDriver.h index 978dd8b..4049c18 100644 --- a/libraries/KlubhausCore/src/IDisplayDriver.h +++ b/libraries/KlubhausCore/src/IDisplayDriver.h @@ -27,8 +27,6 @@ public: // ── Touch ── virtual TouchEvent readTouch() = 0; - /// Returns tile index at (x,y), or -1 if none. - virtual int dashboardTouch(int x, int y) = 0; /// Track a long-press gesture; returns progress/completion. virtual HoldState updateHold(unsigned long holdMs) = 0; /// Idle hint animation (e.g. pulsing ring) while alert is showing. @@ -36,4 +34,10 @@ public: virtual void updateHint(int x, int y, bool active) = 0; virtual int width() = 0; virtual int height() = 0; + + // ── Dashboard tiles ── + /// Transform raw touch coordinates (for rotated touch panels) + virtual void transformTouch(int* x, int* y) { /* default: no transform */ } + /// Draw a tile at specified position (library handles grid math) + virtual void drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) = 0; }; diff --git a/libraries/KlubhausCore/src/ScreenState.h b/libraries/KlubhausCore/src/ScreenState.h index 8581551..9997e5e 100644 --- a/libraries/KlubhausCore/src/ScreenState.h +++ b/libraries/KlubhausCore/src/ScreenState.h @@ -7,6 +7,31 @@ enum class ScreenID { BOOT, OFF, ALERT, DASHBOARD }; enum class BootStage { SPLASH, INIT_DISPLAY, INIT_NETWORK, CONNECTING_WIFI, READY, DONE }; +/// Dashboard tile action handlers +enum class TileAction { + NONE, + ALERT, // Trigger alert + SILENCE, // Silence alert + STATUS, // Send heartbeat/status + REBOOT, // Reboot device +}; + +/// Dashboard tile definitions — shared across all boards +struct DashboardTile { + const char* label; + uint16_t bgColor; // RGB565 + TileAction action; +}; + +/// Standard dashboard tiles (auto-gridded based on count) +static constexpr DashboardTile DASHBOARD_TILES[] = { + { "Alert", 0x0280, TileAction::ALERT }, + { "Silent", 0x0400, TileAction::SILENCE }, + { "Status", 0x0440, TileAction::STATUS }, + { "Reboot", 0x0100, TileAction::REBOOT }, +}; +static constexpr int DASHBOARD_TILE_COUNT = sizeof(DASHBOARD_TILES) / sizeof(DASHBOARD_TILES[0]); + struct ScreenState { DeviceState deviceState = DeviceState::BOOTED; ScreenID screen = ScreenID::BOOT; diff --git a/scripts/monitor-agent.py b/scripts/monitor-agent.py index 96d4804..f172f9b 100644 --- a/scripts/monitor-agent.py +++ b/scripts/monitor-agent.py @@ -66,7 +66,7 @@ def cmd_reader(): if ready: cmd = fifo.read().strip() if cmd: - ser.write((cmd + "\r").encode()) + ser.write((cmd + "\n").encode()) print(f"[SENT] {cmd}") except Exception as e: if cmd_running: