implement dashboard on wake

This commit is contained in:
2026-02-16 02:53:08 -08:00
parent e24d19eb94
commit 3e62c7d481
6 changed files with 202 additions and 96 deletions

View File

@@ -16,12 +16,12 @@
// Tile color themes // Tile color themes
static const uint16_t tileBG[] = { static const uint16_t tileBG[] = {
COL_RED, // LAST_ALERT — red COL_RED, // LAST_ALERT
COL_ORANGE, // STATS — orange COL_ORANGE, // STATS
COL_CYAN, // NETWORK — cyan COL_CYAN, // NETWORK
COL_PURPLE, // MUTE — purple COL_PURPLE, // MUTE
COL_DARK_TILE, // HISTORY — dark COL_DARK_TILE, // HISTORY
COL_DARK_TILE // SYSTEM — dark COL_DARK_TILE // SYSTEM
}; };
static const uint16_t tileFG[] = { static const uint16_t tileFG[] = {
@@ -31,7 +31,6 @@ static const uint16_t tileFG[] = {
Dashboard::Dashboard(TFT_eSPI& tft) Dashboard::Dashboard(TFT_eSPI& tft)
: _tft(tft), _sprite(&tft) : _tft(tft), _sprite(&tft)
{ {
// Initialize tile metadata
_tiles[TILE_LAST_ALERT] = { "!", "LAST ALERT", "none", "", 0, 0, true }; _tiles[TILE_LAST_ALERT] = { "!", "LAST ALERT", "none", "", 0, 0, true };
_tiles[TILE_STATS] = { "#", "TODAY", "0 alerts", "", 0, 0, true }; _tiles[TILE_STATS] = { "#", "TODAY", "0 alerts", "", 0, 0, true };
_tiles[TILE_NETWORK] = { "~", "NETWORK", "---", "", 0, 0, true }; _tiles[TILE_NETWORK] = { "~", "NETWORK", "---", "", 0, 0, true };
@@ -46,10 +45,16 @@ Dashboard::Dashboard(TFT_eSPI& tft)
} }
void Dashboard::begin() { void Dashboard::begin() {
// Create sprite sized to one tile (reused for each)
_sprite.createSprite(TILE_W, TILE_H); _sprite.createSprite(TILE_W, TILE_H);
_sprite.setTextDatum(MC_DATUM); _sprite.setTextDatum(MC_DATUM);
}
void Dashboard::drawAll() {
_tft.fillScreen(COL_BG); _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) { void Dashboard::tilePosition(TileID id, int& x, int& y) {
@@ -64,10 +69,7 @@ void Dashboard::drawTile(TileID id) {
int tx, ty; int tx, ty;
tilePosition(id, tx, ty); tilePosition(id, tx, ty);
// Draw into sprite (off-screen)
_sprite.fillSprite(t.bgColor); _sprite.fillSprite(t.bgColor);
// Rounded corner effect — draw border pixels
_sprite.drawRoundRect(0, 0, TILE_W, TILE_H, 8, COL_GRAY); _sprite.drawRoundRect(0, 0, TILE_W, TILE_H, 8, COL_GRAY);
// Icon — large character at top // Icon — large character at top
@@ -77,27 +79,25 @@ void Dashboard::drawTile(TileID id) {
_sprite.setTextDatum(TC_DATUM); _sprite.setTextDatum(TC_DATUM);
_sprite.drawString(t.icon, TILE_W / 2, 8); _sprite.drawString(t.icon, TILE_W / 2, 8);
// Label — below icon // Label
_sprite.setTextSize(1); _sprite.setTextSize(1);
_sprite.setTextFont(2); _sprite.setTextFont(2);
_sprite.setTextDatum(MC_DATUM); _sprite.setTextDatum(MC_DATUM);
_sprite.drawString(t.label, TILE_W / 2, TILE_H / 2 + 5); _sprite.drawString(t.label, TILE_W / 2, TILE_H / 2 + 5);
// Value — bottom area // Value
_sprite.setTextFont(2); _sprite.setTextFont(2);
_sprite.setTextDatum(BC_DATUM); _sprite.setTextDatum(BC_DATUM);
_sprite.drawString(t.value, TILE_W / 2, TILE_H - 25); _sprite.drawString(t.value, TILE_W / 2, TILE_H - 25);
// Sub text — very bottom // Sub text
if (strlen(t.sub) > 0) { if (strlen(t.sub) > 0) {
_sprite.setTextFont(1); _sprite.setTextFont(1);
_sprite.setTextDatum(BC_DATUM); _sprite.setTextDatum(BC_DATUM);
_sprite.drawString(t.sub, TILE_W / 2, TILE_H - 8); _sprite.drawString(t.sub, TILE_W / 2, TILE_H - 8);
} }
// Push sprite to screen in one operation — no flicker
_sprite.pushSprite(tx, ty); _sprite.pushSprite(tx, ty);
t.dirty = false; t.dirty = false;
} }
@@ -108,15 +108,13 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) {
_tft.setTextFont(4); _tft.setTextFont(4);
_tft.setTextSize(1); _tft.setTextSize(1);
// Title — left
_tft.setTextDatum(ML_DATUM); _tft.setTextDatum(ML_DATUM);
_tft.drawString("KLUBHAUS ALERT", DASH_MARGIN, DASH_TOP_BAR / 2); _tft.drawString("KLUBHAUS ALERT", DASH_MARGIN, DASH_TOP_BAR / 2);
// Time — right
_tft.setTextDatum(MR_DATUM); _tft.setTextDatum(MR_DATUM);
_tft.drawString(time, 470, DASH_TOP_BAR / 2); _tft.drawString(time, 470, DASH_TOP_BAR / 2);
// WiFi indicator — signal bars // WiFi signal bars
int bars = 0; int bars = 0;
if (wifiOk) { if (wifiOk) {
if (rssi > -50) bars = 4; if (rssi > -50) bars = 4;
@@ -125,26 +123,28 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) {
else bars = 1; else bars = 1;
} }
int barX = 370; int barX = 370, barW = 6, barGap = 3;
int barW = 6;
int barGap = 3;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
int barH = 6 + i * 5; int barH = 6 + i * 5;
int barY = DASH_TOP_BAR - 8 - barH; int barY = DASH_TOP_BAR - 8 - barH;
uint16_t col = (i < bars) ? COL_GREEN : COL_GRAY; uint16_t col = (i < bars) ? COL_GREEN : COL_GRAY;
_tft.fillRect(barX + i * (barW + barGap), barY, barW, barH, col); _tft.fillRect(barX + i * (barW + barGap), barY, barW, barH, col);
} }
}
void Dashboard::drawAll() { // Cache values
drawTopBar("--:--", 0, false); strncpy(_barTime, time, sizeof(_barTime) - 1);
for (int i = 0; i < TILE_COUNT; i++) { _barRSSI = rssi;
drawTile((TileID)i); _barWifiOk = wifiOk;
}
} }
void Dashboard::updateTile(TileID id, const char* value, const char* sub) { void Dashboard::updateTile(TileID id, const char* value, const char* sub) {
TileData& t = _tiles[id]; 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); strncpy(t.value, value, 31);
t.value[31] = '\0'; t.value[31] = '\0';
if (sub) { if (sub) {
@@ -152,7 +152,7 @@ void Dashboard::updateTile(TileID id, const char* value, const char* sub) {
t.sub[31] = '\0'; t.sub[31] = '\0';
} }
t.dirty = true; t.dirty = true;
drawTile(id); // immediate redraw of just this tile drawTile(id);
} }
int Dashboard::handleTouch(int x, int y) { int Dashboard::handleTouch(int x, int y) {
@@ -167,3 +167,64 @@ int Dashboard::handleTouch(int x, int y) {
return -1; 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);
}

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <TFT_eSPI.h> #include <TFT_eSPI.h>
#include "ScreenData.h"
// Grid layout constants // Grid layout constants
#define DASH_COLS 3 #define DASH_COLS 3
@@ -23,30 +24,38 @@ enum TileID : uint8_t {
}; };
struct TileData { struct TileData {
const char* icon; // emoji/symbol character const char* icon;
const char* label; // tile name const char* label;
char value[32]; // dynamic value text char value[32];
char sub[32]; // secondary line char sub[32];
uint16_t bgColor; uint16_t bgColor;
uint16_t fgColor; uint16_t fgColor;
bool dirty; // needs redraw bool dirty;
}; };
class Dashboard { class Dashboard {
public: public:
Dashboard(TFT_eSPI& tft); Dashboard(TFT_eSPI& tft);
void begin(); void begin(); // create sprite (call once)
void drawAll(); void drawAll(); // fill screen + draw all tiles
void drawTopBar(const char* time, int rssi, bool wifiOk); void drawTopBar(const char* time, int rssi, bool wifiOk);
void updateTile(TileID id, const char* value, const char* sub = nullptr); 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: private:
TFT_eSPI& _tft; TFT_eSPI& _tft;
TFT_eSprite _sprite; TFT_eSprite _sprite;
TileData _tiles[TILE_COUNT]; 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 drawTile(TileID id);
void tilePosition(TileID id, int& x, int& y); void tilePosition(TileID id, int& x, int& y);
}; };

View File

@@ -1,12 +1,17 @@
#include "DisplayManager.h" #include "DisplayManager.h"
#include "Config.h" #include "Config.h"
DisplayManager::DisplayManager()
: _dash(_tft)
{
}
void DisplayManager::begin() { void DisplayManager::begin() {
pinMode(PIN_LCD_BL, OUTPUT); pinMode(PIN_LCD_BL, OUTPUT);
setBacklight(true); setBacklight(true);
_tft.init(); _tft.init();
_tft.setRotation(1); _tft.setRotation(1); // landscape: 480x320
_tft.fillScreen(COL_BLACK); _tft.fillScreen(COL_BLACK);
} }
@@ -25,6 +30,11 @@ TouchEvent DisplayManager::readTouch() {
return evt; 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) { void DisplayManager::render(const ScreenState& state) {
// Detect screen change → force full redraw // Detect screen change → force full redraw
if (state.screen != _lastScreen) { if (state.screen != _lastScreen) {
@@ -46,7 +56,6 @@ void DisplayManager::render(const ScreenState& state) {
if (_needsFullRedraw) drawWifiFailed(); if (_needsFullRedraw) drawWifiFailed();
break; break;
case ScreenID::ALERT: case ScreenID::ALERT:
// Alert redraws every blink cycle
if (_needsFullRedraw || state.blinkPhase != _lastBlink) { if (_needsFullRedraw || state.blinkPhase != _lastBlink) {
drawAlertScreen(state); drawAlertScreen(state);
_lastBlink = state.blinkPhase; _lastBlink = state.blinkPhase;
@@ -55,6 +64,9 @@ void DisplayManager::render(const ScreenState& state) {
case ScreenID::STATUS: case ScreenID::STATUS:
if (_needsFullRedraw) drawStatusScreen(state); if (_needsFullRedraw) drawStatusScreen(state);
break; break;
case ScreenID::DASHBOARD:
drawDashboard(state);
break;
case ScreenID::OFF: case ScreenID::OFF:
break; break;
} }
@@ -62,6 +74,23 @@ void DisplayManager::render(const ScreenState& state) {
_needsFullRedraw = false; _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 ----- // ----- Helpers -----
void DisplayManager::drawCentered(const char* txt, int y, int sz, uint16_t col) { void DisplayManager::drawCentered(const char* txt, int y, int sz, uint16_t col) {
@@ -127,7 +156,6 @@ void DisplayManager::drawAlertScreen(const ScreenState& s) {
_tft.fillScreen(bg); _tft.fillScreen(bg);
drawHeaderBar(fg, "ALERT", s.timeString); drawHeaderBar(fg, "ALERT", s.timeString);
// Scale text to fit 480px wide screen
int sz = 5; int sz = 5;
int len = strlen(s.alertMessage); int len = strlen(s.alertMessage);
if (len > 10) sz = 4; if (len > 10) sz = 4;
@@ -135,7 +163,6 @@ void DisplayManager::drawAlertScreen(const ScreenState& s) {
if (len > 30) sz = 2; if (len > 30) sz = 2;
if (len > 12) { if (len > 12) {
// Two-line split
String msg(s.alertMessage); String msg(s.alertMessage);
int mid = len / 2; int mid = len / 2;
int sp = msg.lastIndexOf(' ', mid); int sp = msg.lastIndexOf(' ', mid);
@@ -188,8 +215,7 @@ void DisplayManager::drawStatusScreen(const ScreenState& s) {
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; drawInfoLine(x, y, stCol, buf); y += sp;
// Recent alerts if (s.alertHistoryCount > 0) {
if (s.alertHistoryCount > 0) {
drawInfoLine(x, y, COL_MINT, "Recent Alerts:"); y += sp; drawInfoLine(x, y, COL_MINT, "Recent Alerts:"); y += sp;
for (int i = 0; i < s.alertHistoryCount; i++) { for (int i = 0; i < s.alertHistoryCount; i++) {
uint16_t col = (i == 0) ? COL_YELLOW : COL_DARK_GRAY; uint16_t col = (i == 0) ? COL_YELLOW : COL_DARK_GRAY;
@@ -198,9 +224,9 @@ if (s.alertHistoryCount > 0) {
s.alertHistory[i].message); s.alertHistory[i].message);
drawInfoLine(x, y, col, buf); y += sp; drawInfoLine(x, y, col, buf); y += sp;
} }
} else { } else {
drawInfoLine(x, y, COL_DARK_GRAY, "No alerts yet"); y += sp; drawInfoLine(x, y, COL_DARK_GRAY, "No alerts yet"); y += sp;
} }
if (s.debugMode) { if (s.debugMode) {
drawInfoLine(x, y, COL_YELLOW, "DEBUG MODE - _test topics"); drawInfoLine(x, y, COL_YELLOW, "DEBUG MODE - _test topics");

View File

@@ -1,19 +1,28 @@
#pragma once #pragma once
#include <TFT_eSPI.h> #include <TFT_eSPI.h>
#include "ScreenData.h" #include "ScreenData.h"
#include "Dashboard.h"
class DisplayManager { class DisplayManager {
public: public:
DisplayManager();
void begin(); void begin();
void render(const ScreenState& state); void render(const ScreenState& state);
void setBacklight(bool on); void setBacklight(bool on);
TouchEvent readTouch(); TouchEvent readTouch();
// Dashboard tile touch — returns TileID or -1
int dashboardTouch(uint16_t x, uint16_t y);
private: private:
TFT_eSPI _tft; TFT_eSPI _tft;
Dashboard _dash;
ScreenID _lastScreen = ScreenID::BOOT_SPLASH; ScreenID _lastScreen = ScreenID::BOOT_SPLASH;
bool _needsFullRedraw = true; bool _needsFullRedraw = true;
bool _lastBlink = false; bool _lastBlink = false;
bool _dashSpriteReady = false;
unsigned long _lastDashRefresh = 0;
// Colors // Colors
static constexpr uint16_t COL_NEON_TEAL = 0x07D7; static constexpr uint16_t COL_NEON_TEAL = 0x07D7;
@@ -33,6 +42,7 @@ private:
void drawWifiFailed(); void drawWifiFailed();
void drawAlertScreen(const ScreenState& s); void drawAlertScreen(const ScreenState& s);
void drawStatusScreen(const ScreenState& s); void drawStatusScreen(const ScreenState& s);
void drawDashboard(const ScreenState& s);
// Helpers // Helpers
void drawCentered(const char* txt, int y, int sz, uint16_t col); void drawCentered(const char* txt, int y, int sz, uint16_t col);

View File

@@ -239,8 +239,8 @@ void DoorbellLogic::transitionTo(DeviceState newState) {
break; break;
case DeviceState::WAKE: case DeviceState::WAKE:
_wakeStart = now; _wakeStart = now;
_screen.screen = ScreenID::STATUS; _screen.screen = ScreenID::DASHBOARD;
Serial.println("-> WAKE"); Serial.println("-> WAKE (dashboard)");
break; break;
} }
} }

View File

@@ -18,6 +18,7 @@ enum class ScreenID : uint8_t {
WIFI_FAILED, WIFI_FAILED,
ALERT, ALERT,
STATUS, STATUS,
DASHBOARD, // <-- NEW
OFF // backlight off, nothing to draw OFF // backlight off, nothing to draw
}; };
@@ -58,7 +59,6 @@ struct ScreenState {
// Alert history (newest first) // Alert history (newest first)
AlertRecord alertHistory[ALERT_HISTORY_SIZE] = {}; AlertRecord alertHistory[ALERT_HISTORY_SIZE] = {};
int alertHistoryCount = 0; int alertHistoryCount = 0;
}; };
// Touch event passed from display to logic // Touch event passed from display to logic