From 3e62c7d481cdc6040e83f05e81ce9526d1f3c2e3 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 16 Feb 2026 02:53:08 -0800 Subject: [PATCH] implement dashboard on wake --- .../doorbell-touch-esp32-32e/Dashboard.cpp | 129 +++++++++++++----- sketches/doorbell-touch-esp32-32e/Dashboard.h | 29 ++-- .../DisplayManager.cpp | 82 +++++++---- .../doorbell-touch-esp32-32e/DisplayManager.h | 18 ++- .../DoorbellLogic.cpp | 4 +- .../doorbell-touch-esp32-32e/ScreenData.h | 36 ++--- 6 files changed, 202 insertions(+), 96 deletions(-) diff --git a/sketches/doorbell-touch-esp32-32e/Dashboard.cpp b/sketches/doorbell-touch-esp32-32e/Dashboard.cpp index 0708886..8635f38 100644 --- a/sketches/doorbell-touch-esp32-32e/Dashboard.cpp +++ b/sketches/doorbell-touch-esp32-32e/Dashboard.cpp @@ -16,12 +16,12 @@ // Tile color themes static const uint16_t tileBG[] = { - COL_RED, // LAST_ALERT — red - COL_ORANGE, // STATS — orange - COL_CYAN, // NETWORK — cyan - COL_PURPLE, // MUTE — purple - COL_DARK_TILE, // HISTORY — dark - COL_DARK_TILE // SYSTEM — dark + COL_RED, // LAST_ALERT + COL_ORANGE, // STATS + COL_CYAN, // NETWORK + COL_PURPLE, // MUTE + COL_DARK_TILE, // HISTORY + COL_DARK_TILE // SYSTEM }; static const uint16_t tileFG[] = { @@ -31,13 +31,12 @@ static const uint16_t tileFG[] = { Dashboard::Dashboard(TFT_eSPI& tft) : _tft(tft), _sprite(&tft) { - // Initialize tile metadata - _tiles[TILE_LAST_ALERT] = { "!", "LAST ALERT", "none", "", 0, 0, true }; - _tiles[TILE_STATS] = { "#", "TODAY", "0 alerts", "", 0, 0, true }; - _tiles[TILE_NETWORK] = { "~", "NETWORK", "---", "", 0, 0, true }; - _tiles[TILE_MUTE] = { "M", "MUTE", "OFF", "", 0, 0, true }; + _tiles[TILE_LAST_ALERT] = { "!", "LAST ALERT", "none", "", 0, 0, true }; + _tiles[TILE_STATS] = { "#", "TODAY", "0 alerts", "", 0, 0, true }; + _tiles[TILE_NETWORK] = { "~", "NETWORK", "---", "", 0, 0, true }; + _tiles[TILE_MUTE] = { "M", "MUTE", "OFF", "", 0, 0, true }; _tiles[TILE_HISTORY] = { ">", "HISTORY", "tap to view","", 0, 0, true }; - _tiles[TILE_SYSTEM] = { "*", "SYSTEM", "---", "", 0, 0, true }; + _tiles[TILE_SYSTEM] = { "*", "SYSTEM", "---", "", 0, 0, true }; for (int i = 0; i < TILE_COUNT; i++) { _tiles[i].bgColor = tileBG[i]; @@ -46,10 +45,16 @@ Dashboard::Dashboard(TFT_eSPI& tft) } void Dashboard::begin() { - // Create sprite sized to one tile (reused for each) _sprite.createSprite(TILE_W, TILE_H); _sprite.setTextDatum(MC_DATUM); +} + +void Dashboard::drawAll() { _tft.fillScreen(COL_BG); + drawTopBar("--:--", 0, false); + for (int i = 0; i < TILE_COUNT; i++) { + drawTile((TileID)i); + } } void Dashboard::tilePosition(TileID id, int& x, int& y) { @@ -64,10 +69,7 @@ void Dashboard::drawTile(TileID id) { int tx, ty; tilePosition(id, tx, ty); - // Draw into sprite (off-screen) _sprite.fillSprite(t.bgColor); - - // Rounded corner effect — draw border pixels _sprite.drawRoundRect(0, 0, TILE_W, TILE_H, 8, COL_GRAY); // Icon — large character at top @@ -77,27 +79,25 @@ void Dashboard::drawTile(TileID id) { _sprite.setTextDatum(TC_DATUM); _sprite.drawString(t.icon, TILE_W / 2, 8); - // Label — below icon + // Label _sprite.setTextSize(1); _sprite.setTextFont(2); _sprite.setTextDatum(MC_DATUM); _sprite.drawString(t.label, TILE_W / 2, TILE_H / 2 + 5); - // Value — bottom area + // Value _sprite.setTextFont(2); _sprite.setTextDatum(BC_DATUM); _sprite.drawString(t.value, TILE_W / 2, TILE_H - 25); - // Sub text — very bottom + // Sub text if (strlen(t.sub) > 0) { _sprite.setTextFont(1); _sprite.setTextDatum(BC_DATUM); _sprite.drawString(t.sub, TILE_W / 2, TILE_H - 8); } - // Push sprite to screen in one operation — no flicker _sprite.pushSprite(tx, ty); - t.dirty = false; } @@ -108,15 +108,13 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) { _tft.setTextFont(4); _tft.setTextSize(1); - // Title — left _tft.setTextDatum(ML_DATUM); _tft.drawString("KLUBHAUS ALERT", DASH_MARGIN, DASH_TOP_BAR / 2); - // Time — right _tft.setTextDatum(MR_DATUM); _tft.drawString(time, 470, DASH_TOP_BAR / 2); - // WiFi indicator — signal bars + // WiFi signal bars int bars = 0; if (wifiOk) { if (rssi > -50) bars = 4; @@ -125,26 +123,28 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) { else bars = 1; } - int barX = 370; - int barW = 6; - int barGap = 3; + int barX = 370, barW = 6, barGap = 3; for (int i = 0; i < 4; i++) { int barH = 6 + i * 5; int barY = DASH_TOP_BAR - 8 - barH; uint16_t col = (i < bars) ? COL_GREEN : COL_GRAY; _tft.fillRect(barX + i * (barW + barGap), barY, barW, barH, col); } -} -void Dashboard::drawAll() { - drawTopBar("--:--", 0, false); - for (int i = 0; i < TILE_COUNT; i++) { - drawTile((TileID)i); - } + // Cache values + strncpy(_barTime, time, sizeof(_barTime) - 1); + _barRSSI = rssi; + _barWifiOk = wifiOk; } void Dashboard::updateTile(TileID id, const char* value, const char* sub) { TileData& t = _tiles[id]; + + // Dirty check — skip redraw if nothing changed + bool changed = (strcmp(t.value, value) != 0); + if (sub && strcmp(t.sub, sub) != 0) changed = true; + if (!changed && !t.dirty) return; + strncpy(t.value, value, 31); t.value[31] = '\0'; if (sub) { @@ -152,7 +152,7 @@ void Dashboard::updateTile(TileID id, const char* value, const char* sub) { t.sub[31] = '\0'; } t.dirty = true; - drawTile(id); // immediate redraw of just this tile + drawTile(id); } int Dashboard::handleTouch(int x, int y) { @@ -167,3 +167,64 @@ int Dashboard::handleTouch(int x, int y) { return -1; } +// ===================================================================== +// Refresh all tiles from ScreenState — only redraws changed tiles +// ===================================================================== +void Dashboard::refreshFromState(const ScreenState& state) { + // Top bar — only redraw if changed + bool barChanged = (strcmp(_barTime, state.timeString) != 0) || + (_barRSSI != state.wifiRSSI) || + (_barWifiOk != state.wifiConnected); + if (barChanged) { + drawTopBar(state.timeString, state.wifiRSSI, state.wifiConnected); + } + + // LAST ALERT tile + if (state.alertHistoryCount > 0) { + updateTile(TILE_LAST_ALERT, + state.alertHistory[0].message, + state.alertHistory[0].timestamp); + } else { + updateTile(TILE_LAST_ALERT, "none", ""); + } + + // STATS tile + char statsBuf[32]; + snprintf(statsBuf, sizeof(statsBuf), "%d alert%s", + state.alertHistoryCount, + state.alertHistoryCount == 1 ? "" : "s"); + updateTile(TILE_STATS, statsBuf, "this session"); + + // NETWORK tile + if (state.wifiConnected) { + char rssiBuf[16]; + snprintf(rssiBuf, sizeof(rssiBuf), "%d dBm", state.wifiRSSI); + updateTile(TILE_NETWORK, rssiBuf, state.wifiSSID); + } else { + updateTile(TILE_NETWORK, "DOWN", "reconnecting..."); + } + + // MUTE tile (placeholder) + updateTile(TILE_MUTE, "OFF", "tap to mute"); + + // HISTORY tile — show 2nd and 3rd alerts + if (state.alertHistoryCount > 1) { + char histBuf[48]; + snprintf(histBuf, sizeof(histBuf), "%s %.20s", + state.alertHistory[1].timestamp, + state.alertHistory[1].message); + const char* sub = (state.alertHistoryCount > 2) + ? state.alertHistory[2].message : ""; + updateTile(TILE_HISTORY, histBuf, sub); + } else { + updateTile(TILE_HISTORY, "no history", ""); + } + + // SYSTEM tile + char heapBuf[16]; + snprintf(heapBuf, sizeof(heapBuf), "%lu KB", state.freeHeapKB); + char uptimeBuf[20]; + snprintf(uptimeBuf, sizeof(uptimeBuf), "up %lum", state.uptimeMinutes); + updateTile(TILE_SYSTEM, heapBuf, uptimeBuf); +} + diff --git a/sketches/doorbell-touch-esp32-32e/Dashboard.h b/sketches/doorbell-touch-esp32-32e/Dashboard.h index 2890127..e4a3a4d 100644 --- a/sketches/doorbell-touch-esp32-32e/Dashboard.h +++ b/sketches/doorbell-touch-esp32-32e/Dashboard.h @@ -1,5 +1,6 @@ #pragma once #include +#include "ScreenData.h" // Grid layout constants #define DASH_COLS 3 @@ -8,8 +9,8 @@ #define DASH_TOP_BAR 40 // Tile dimensions (calculated for 480x320) -#define TILE_W ((480 - (DASH_COLS + 1) * DASH_MARGIN) / DASH_COLS) // ~148 -#define TILE_H ((320 - DASH_TOP_BAR - (DASH_ROWS + 1) * DASH_MARGIN) / DASH_ROWS) // ~128 +#define TILE_W ((480 - (DASH_COLS + 1) * DASH_MARGIN) / DASH_COLS) // ~148 +#define TILE_H ((320 - DASH_TOP_BAR - (DASH_ROWS + 1) * DASH_MARGIN) / DASH_ROWS) // ~128 // Tile IDs enum TileID : uint8_t { @@ -23,30 +24,38 @@ enum TileID : uint8_t { }; struct TileData { - const char* icon; // emoji/symbol character - const char* label; // tile name - char value[32]; // dynamic value text - char sub[32]; // secondary line + const char* icon; + const char* label; + char value[32]; + char sub[32]; uint16_t bgColor; uint16_t fgColor; - bool dirty; // needs redraw + bool dirty; }; class Dashboard { public: Dashboard(TFT_eSPI& tft); - void begin(); - void drawAll(); + void begin(); // create sprite (call once) + void drawAll(); // fill screen + draw all tiles void drawTopBar(const char* time, int rssi, bool wifiOk); void updateTile(TileID id, const char* value, const char* sub = nullptr); - int handleTouch(int x, int y); // returns TileID or -1 + int handleTouch(int x, int y); // returns TileID or -1 + + // Populate tiles from ScreenState — only redraws changed tiles + void refreshFromState(const ScreenState& state); private: TFT_eSPI& _tft; TFT_eSprite _sprite; TileData _tiles[TILE_COUNT]; + // Cached top bar values for dirty check + char _barTime[12] = ""; + int _barRSSI = 0; + bool _barWifiOk = false; + void drawTile(TileID id); void tilePosition(TileID id, int& x, int& y); }; diff --git a/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp b/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp index 7ee37b3..95f15dc 100644 --- a/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp +++ b/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp @@ -1,12 +1,17 @@ #include "DisplayManager.h" #include "Config.h" +DisplayManager::DisplayManager() + : _dash(_tft) +{ +} + void DisplayManager::begin() { pinMode(PIN_LCD_BL, OUTPUT); setBacklight(true); _tft.init(); - _tft.setRotation(1); + _tft.setRotation(1); // landscape: 480x320 _tft.fillScreen(COL_BLACK); } @@ -25,6 +30,11 @@ TouchEvent DisplayManager::readTouch() { return evt; } +int DisplayManager::dashboardTouch(uint16_t x, uint16_t y) { + if (_lastScreen != ScreenID::DASHBOARD) return -1; + return _dash.handleTouch(x, y); +} + void DisplayManager::render(const ScreenState& state) { // Detect screen change → force full redraw if (state.screen != _lastScreen) { @@ -46,7 +56,6 @@ void DisplayManager::render(const ScreenState& state) { if (_needsFullRedraw) drawWifiFailed(); break; case ScreenID::ALERT: - // Alert redraws every blink cycle if (_needsFullRedraw || state.blinkPhase != _lastBlink) { drawAlertScreen(state); _lastBlink = state.blinkPhase; @@ -55,6 +64,9 @@ void DisplayManager::render(const ScreenState& state) { case ScreenID::STATUS: if (_needsFullRedraw) drawStatusScreen(state); break; + case ScreenID::DASHBOARD: + drawDashboard(state); + break; case ScreenID::OFF: break; } @@ -62,6 +74,23 @@ void DisplayManager::render(const ScreenState& state) { _needsFullRedraw = false; } +// ----- Dashboard ----- + +void DisplayManager::drawDashboard(const ScreenState& s) { + if (_needsFullRedraw) { + if (!_dashSpriteReady) { + _dash.begin(); // create sprite (once) + _dashSpriteReady = true; + } + _dash.drawAll(); // fill screen + draw all tiles + _dash.refreshFromState(s); // update with real data + _lastDashRefresh = millis(); + } else if (millis() - _lastDashRefresh > 2000) { + _lastDashRefresh = millis(); + _dash.refreshFromState(s); // only redraws changed tiles + } +} + // ----- Helpers ----- void DisplayManager::drawCentered(const char* txt, int y, int sz, uint16_t col) { @@ -122,12 +151,11 @@ void DisplayManager::drawWifiFailed() { void DisplayManager::drawAlertScreen(const ScreenState& s) { uint16_t bg = s.blinkPhase ? COL_NEON_TEAL : COL_HOT_FUCHSIA; - uint16_t fg = s.blinkPhase ? COL_BLACK : COL_WHITE; + uint16_t fg = s.blinkPhase ? COL_BLACK : COL_WHITE; _tft.fillScreen(bg); drawHeaderBar(fg, "ALERT", s.timeString); - // Scale text to fit 480px wide screen int sz = 5; int len = strlen(s.alertMessage); if (len > 10) sz = 4; @@ -135,7 +163,6 @@ void DisplayManager::drawAlertScreen(const ScreenState& s) { if (len > 30) sz = 2; if (len > 12) { - // Two-line split String msg(s.alertMessage); int mid = len / 2; int sp = msg.lastIndexOf(' ', mid); @@ -165,42 +192,41 @@ void DisplayManager::drawStatusScreen(const ScreenState& s) { int x = 20; snprintf(buf, sizeof(buf), "WiFi: %s (%ddBm)", - s.wifiConnected ? s.wifiSSID : "DOWN", s.wifiRSSI); + s.wifiConnected ? s.wifiSSID : "DOWN", s.wifiRSSI); drawInfoLine(x, y, COL_WHITE, buf); y += sp; - snprintf(buf, sizeof(buf), "IP: %s", - s.wifiConnected ? s.wifiIP : "---"); + snprintf(buf, sizeof(buf), "IP: %s", + s.wifiConnected ? s.wifiIP : "---"); drawInfoLine(x, y, COL_WHITE, buf); y += sp; - snprintf(buf, sizeof(buf), "Up: %lu min Heap: %lu KB", - s.uptimeMinutes, s.freeHeapKB); + snprintf(buf, sizeof(buf), "Up: %lu min Heap: %lu KB", + s.uptimeMinutes, s.freeHeapKB); drawInfoLine(x, y, COL_WHITE, buf); y += sp; - snprintf(buf, sizeof(buf), "NTP: %s UTC", - s.ntpSynced ? s.timeString : "not synced"); + snprintf(buf, sizeof(buf), "NTP: %s UTC", + s.ntpSynced ? s.timeString : "not synced"); drawInfoLine(x, y, COL_WHITE, buf); y += sp; - const char* stName = s.deviceState == DeviceState::SILENT ? "SILENT" : - s.deviceState == DeviceState::ALERTING ? "ALERTING" : "WAKE"; + const char* stName = s.deviceState == DeviceState::SILENT ? "SILENT" : + s.deviceState == DeviceState::ALERTING ? "ALERTING" : "WAKE"; snprintf(buf, sizeof(buf), "State: %s Net: %s", - stName, s.networkOK ? "OK" : "FAIL"); + stName, s.networkOK ? "OK" : "FAIL"); uint16_t stCol = s.deviceState == DeviceState::ALERTING ? COL_RED : - s.deviceState == DeviceState::SILENT ? COL_GREEN : COL_NEON_TEAL; + s.deviceState == DeviceState::SILENT ? COL_GREEN : COL_NEON_TEAL; drawInfoLine(x, y, stCol, buf); y += sp; -// Recent alerts -if (s.alertHistoryCount > 0) { - drawInfoLine(x, y, COL_MINT, "Recent Alerts:"); y += sp; - for (int i = 0; i < s.alertHistoryCount; i++) { - uint16_t col = (i == 0) ? COL_YELLOW : COL_DARK_GRAY; - snprintf(buf, sizeof(buf), "%s %.35s", - s.alertHistory[i].timestamp, - s.alertHistory[i].message); - drawInfoLine(x, y, col, buf); y += sp; + if (s.alertHistoryCount > 0) { + drawInfoLine(x, y, COL_MINT, "Recent Alerts:"); y += sp; + for (int i = 0; i < s.alertHistoryCount; i++) { + uint16_t col = (i == 0) ? COL_YELLOW : COL_DARK_GRAY; + snprintf(buf, sizeof(buf), "%s %.35s", + s.alertHistory[i].timestamp, + s.alertHistory[i].message); + drawInfoLine(x, y, col, buf); y += sp; + } + } else { + drawInfoLine(x, y, COL_DARK_GRAY, "No alerts yet"); y += sp; } -} else { - drawInfoLine(x, y, COL_DARK_GRAY, "No alerts yet"); y += sp; -} if (s.debugMode) { drawInfoLine(x, y, COL_YELLOW, "DEBUG MODE - _test topics"); diff --git a/sketches/doorbell-touch-esp32-32e/DisplayManager.h b/sketches/doorbell-touch-esp32-32e/DisplayManager.h index c45d3bd..f58acc8 100644 --- a/sketches/doorbell-touch-esp32-32e/DisplayManager.h +++ b/sketches/doorbell-touch-esp32-32e/DisplayManager.h @@ -1,19 +1,28 @@ #pragma once #include #include "ScreenData.h" +#include "Dashboard.h" class DisplayManager { public: + DisplayManager(); void begin(); void render(const ScreenState& state); void setBacklight(bool on); TouchEvent readTouch(); + // Dashboard tile touch — returns TileID or -1 + int dashboardTouch(uint16_t x, uint16_t y); + private: - TFT_eSPI _tft; - ScreenID _lastScreen = ScreenID::BOOT_SPLASH; - bool _needsFullRedraw = true; - bool _lastBlink = false; + TFT_eSPI _tft; + Dashboard _dash; + + ScreenID _lastScreen = ScreenID::BOOT_SPLASH; + bool _needsFullRedraw = true; + bool _lastBlink = false; + bool _dashSpriteReady = false; + unsigned long _lastDashRefresh = 0; // Colors static constexpr uint16_t COL_NEON_TEAL = 0x07D7; @@ -33,6 +42,7 @@ private: void drawWifiFailed(); void drawAlertScreen(const ScreenState& s); void drawStatusScreen(const ScreenState& s); + void drawDashboard(const ScreenState& s); // Helpers void drawCentered(const char* txt, int y, int sz, uint16_t col); diff --git a/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp b/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp index 1bd20e3..0d546fc 100644 --- a/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp +++ b/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp @@ -239,8 +239,8 @@ void DoorbellLogic::transitionTo(DeviceState newState) { break; case DeviceState::WAKE: _wakeStart = now; - _screen.screen = ScreenID::STATUS; - Serial.println("-> WAKE"); + _screen.screen = ScreenID::DASHBOARD; + Serial.println("-> WAKE (dashboard)"); break; } } diff --git a/sketches/doorbell-touch-esp32-32e/ScreenData.h b/sketches/doorbell-touch-esp32-32e/ScreenData.h index c2ecfb0..6ab9623 100644 --- a/sketches/doorbell-touch-esp32-32e/ScreenData.h +++ b/sketches/doorbell-touch-esp32-32e/ScreenData.h @@ -2,7 +2,7 @@ #include // ===================================================================== -// Shared enums and structs — NO library dependencies +// Shared enums and structs — NO library dependencies // ===================================================================== enum class DeviceState : uint8_t { @@ -18,47 +18,47 @@ enum class ScreenID : uint8_t { WIFI_FAILED, ALERT, STATUS, - OFF // backlight off, nothing to draw + DASHBOARD, // <-- NEW + OFF // backlight off, nothing to draw }; #define ALERT_HISTORY_SIZE 3 struct AlertRecord { char message[64] = ""; - char timestamp[12] = ""; // "HH:MM:SS" + char timestamp[12] = ""; // "HH:MM:SS" }; // Everything the display needs to render any screen struct ScreenState { - ScreenID screen = ScreenID::BOOT_SPLASH; - DeviceState deviceState = DeviceState::SILENT; - bool blinkPhase = false; + ScreenID screen = ScreenID::BOOT_SPLASH; + DeviceState deviceState = DeviceState::SILENT; + bool blinkPhase = false; // Alert - char alertMessage[64] = ""; + char alertMessage[64] = ""; // WiFi - bool wifiConnected = false; - char wifiSSID[33] = ""; - int wifiRSSI = 0; - char wifiIP[16] = ""; + bool wifiConnected = false; + char wifiSSID[33] = ""; + int wifiRSSI = 0; + char wifiIP[16] = ""; // NTP - bool ntpSynced = false; - char timeString[12] = ""; + bool ntpSynced = false; + char timeString[12] = ""; // System - uint32_t uptimeMinutes = 0; - uint32_t freeHeapKB = 0; - bool networkOK = false; + uint32_t uptimeMinutes = 0; + uint32_t freeHeapKB = 0; + bool networkOK = false; // Debug - bool debugMode = false; + bool debugMode = false; // Alert history (newest first) AlertRecord alertHistory[ALERT_HISTORY_SIZE] = {}; int alertHistoryCount = 0; - }; // Touch event passed from display to logic