refactor(display): extract dashboard tile grid logic to DisplayManager

This commit is contained in:
2026-02-18 04:28:35 -08:00
parent 3b8e54c511
commit 67613120ad
10 changed files with 259 additions and 64 deletions

46
.clangd
View File

@@ -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

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;
};

View File

@@ -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) {

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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: