This commit is contained in:
2026-02-16 03:14:38 -08:00
parent 850275ee03
commit d3f5a4e3e3
7 changed files with 255 additions and 304 deletions

View File

@@ -1,29 +1,19 @@
#include "Dashboard.h" #include "Dashboard.h"
// 16-bit color helpers #define COL_BG 0x1082
#define COL_BG 0x1082 // dark charcoal #define COL_BAR 0x2104
#define COL_BAR 0x2104 // slightly lighter
#define COL_RED 0xF800 #define COL_RED 0xF800
#define COL_ORANGE 0xFBE0 #define COL_ORANGE 0xFBE0
#define COL_GREEN 0x07E0 #define COL_GREEN 0x07E0
#define COL_CYAN 0x07FF #define COL_CYAN 0x07FF
#define COL_BLUE 0x001F
#define COL_PURPLE 0x780F #define COL_PURPLE 0x780F
#define COL_YELLOW 0xFFE0
#define COL_WHITE 0xFFFF #define COL_WHITE 0xFFFF
#define COL_GRAY 0x8410 #define COL_GRAY 0x8410
#define COL_DARK_TILE 0x18E3 #define COL_DARK_TILE 0x18E3
// Tile color themes
static const uint16_t tileBG[] = { static const uint16_t tileBG[] = {
COL_RED, // LAST_ALERT COL_RED, COL_ORANGE, COL_CYAN, COL_PURPLE, COL_DARK_TILE, COL_DARK_TILE
COL_ORANGE, // STATS
COL_CYAN, // NETWORK
COL_PURPLE, // MUTE
COL_DARK_TILE, // HISTORY
COL_DARK_TILE // SYSTEM
}; };
static const uint16_t tileFG[] = { static const uint16_t tileFG[] = {
COL_WHITE, COL_WHITE, 0x0000, COL_WHITE, COL_WHITE, COL_WHITE COL_WHITE, COL_WHITE, 0x0000, COL_WHITE, COL_WHITE, COL_WHITE
}; };
@@ -35,7 +25,7 @@ Dashboard::Dashboard(TFT_eSPI& tft)
_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 };
_tiles[TILE_MUTE] = { "M", "MUTE", "OFF", "", 0, 0, true }; _tiles[TILE_MUTE] = { "M", "MUTE", "OFF", "", 0, 0, true };
_tiles[TILE_HISTORY] = { ">", "HISTORY", "tap to view","", 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++) { for (int i = 0; i < TILE_COUNT; i++) {
@@ -72,25 +62,21 @@ void Dashboard::drawTile(TileID id) {
_sprite.fillSprite(t.bgColor); _sprite.fillSprite(t.bgColor);
_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
_sprite.setTextColor(t.fgColor, t.bgColor); _sprite.setTextColor(t.fgColor, t.bgColor);
_sprite.setTextFont(4); _sprite.setTextFont(4);
_sprite.setTextSize(2); _sprite.setTextSize(2);
_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
_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
_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
if (strlen(t.sub) > 0) { if (strlen(t.sub) > 0) {
_sprite.setTextFont(1); _sprite.setTextFont(1);
_sprite.setTextDatum(BC_DATUM); _sprite.setTextDatum(BC_DATUM);
@@ -103,7 +89,6 @@ void Dashboard::drawTile(TileID id) {
void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) { void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) {
_tft.fillRect(0, 0, 480, DASH_TOP_BAR, COL_BAR); _tft.fillRect(0, 0, 480, DASH_TOP_BAR, COL_BAR);
_tft.setTextColor(COL_WHITE, COL_BAR); _tft.setTextColor(COL_WHITE, COL_BAR);
_tft.setTextFont(4); _tft.setTextFont(4);
_tft.setTextSize(1); _tft.setTextSize(1);
@@ -114,7 +99,6 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) {
_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 signal bars
int bars = 0; int bars = 0;
if (wifiOk) { if (wifiOk) {
if (rssi > -50) bars = 4; if (rssi > -50) bars = 4;
@@ -122,7 +106,6 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) {
else if (rssi > -70) bars = 2; else if (rssi > -70) bars = 2;
else bars = 1; else bars = 1;
} }
int barX = 370, barW = 6, barGap = 3; int barX = 370, barW = 6, 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;
@@ -131,7 +114,6 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) {
_tft.fillRect(barX + i * (barW + barGap), barY, barW, barH, col); _tft.fillRect(barX + i * (barW + barGap), barY, barW, barH, col);
} }
// Cache values
strncpy(_barTime, time, sizeof(_barTime) - 1); strncpy(_barTime, time, sizeof(_barTime) - 1);
_barRSSI = rssi; _barRSSI = rssi;
_barWifiOk = wifiOk; _barWifiOk = wifiOk;
@@ -139,18 +121,12 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool 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); bool changed = (strcmp(t.value, value) != 0);
if (sub && strcmp(t.sub, sub) != 0) changed = true; if (sub && strcmp(t.sub, sub) != 0) changed = true;
if (!changed && !t.dirty) return; 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) { strncpy(t.sub, sub, 31); t.sub[31] = '\0'; }
if (sub) {
strncpy(t.sub, sub, 31);
t.sub[31] = '\0';
}
t.dirty = true; t.dirty = true;
drawTile(id); drawTile(id);
} }
@@ -159,19 +135,13 @@ int Dashboard::handleTouch(int x, int y) {
for (int i = 0; i < TILE_COUNT; i++) { for (int i = 0; i < TILE_COUNT; i++) {
int tx, ty; int tx, ty;
tilePosition((TileID)i, tx, ty); tilePosition((TileID)i, tx, ty);
if (x >= tx && x < tx + TILE_W && if (x >= tx && x < tx + TILE_W && y >= ty && y < ty + TILE_H)
y >= ty && y < ty + TILE_H) {
return i; return i;
} }
}
return -1; return -1;
} }
// =====================================================================
// Refresh all tiles from ScreenState — only redraws changed tiles
// =====================================================================
void Dashboard::refreshFromState(const ScreenState& state) { void Dashboard::refreshFromState(const ScreenState& state) {
// Top bar — only redraw if changed
bool barChanged = (strcmp(_barTime, state.timeString) != 0) || bool barChanged = (strcmp(_barTime, state.timeString) != 0) ||
(_barRSSI != state.wifiRSSI) || (_barRSSI != state.wifiRSSI) ||
(_barWifiOk != state.wifiConnected); (_barWifiOk != state.wifiConnected);
@@ -179,7 +149,6 @@ void Dashboard::refreshFromState(const ScreenState& state) {
drawTopBar(state.timeString, state.wifiRSSI, state.wifiConnected); drawTopBar(state.timeString, state.wifiRSSI, state.wifiConnected);
} }
// LAST ALERT tile
if (state.alertHistoryCount > 0) { if (state.alertHistoryCount > 0) {
updateTile(TILE_LAST_ALERT, updateTile(TILE_LAST_ALERT,
state.alertHistory[0].message, state.alertHistory[0].message,
@@ -188,14 +157,12 @@ void Dashboard::refreshFromState(const ScreenState& state) {
updateTile(TILE_LAST_ALERT, "none", ""); updateTile(TILE_LAST_ALERT, "none", "");
} }
// STATS tile
char statsBuf[32]; char statsBuf[32];
snprintf(statsBuf, sizeof(statsBuf), "%d alert%s", snprintf(statsBuf, sizeof(statsBuf), "%d alert%s",
state.alertHistoryCount, state.alertHistoryCount,
state.alertHistoryCount == 1 ? "" : "s"); state.alertHistoryCount == 1 ? "" : "s");
updateTile(TILE_STATS, statsBuf, "this session"); updateTile(TILE_STATS, statsBuf, "this session");
// NETWORK tile
if (state.wifiConnected) { if (state.wifiConnected) {
char rssiBuf[16]; char rssiBuf[16];
snprintf(rssiBuf, sizeof(rssiBuf), "%d dBm", state.wifiRSSI); snprintf(rssiBuf, sizeof(rssiBuf), "%d dBm", state.wifiRSSI);
@@ -204,10 +171,8 @@ void Dashboard::refreshFromState(const ScreenState& state) {
updateTile(TILE_NETWORK, "DOWN", "reconnecting..."); updateTile(TILE_NETWORK, "DOWN", "reconnecting...");
} }
// MUTE tile (placeholder)
updateTile(TILE_MUTE, "OFF", "tap to mute"); updateTile(TILE_MUTE, "OFF", "tap to mute");
// HISTORY tile — show 2nd and 3rd alerts
if (state.alertHistoryCount > 1) { if (state.alertHistoryCount > 1) {
char histBuf[48]; char histBuf[48];
snprintf(histBuf, sizeof(histBuf), "%s %.20s", snprintf(histBuf, sizeof(histBuf), "%s %.20s",
@@ -220,7 +185,6 @@ void Dashboard::refreshFromState(const ScreenState& state) {
updateTile(TILE_HISTORY, "no history", ""); updateTile(TILE_HISTORY, "no history", "");
} }
// SYSTEM tile
char heapBuf[16]; char heapBuf[16];
snprintf(heapBuf, sizeof(heapBuf), "%lu KB", state.freeHeapKB); snprintf(heapBuf, sizeof(heapBuf), "%lu KB", state.freeHeapKB);
char uptimeBuf[20]; char uptimeBuf[20];

View File

@@ -2,17 +2,13 @@
#include <TFT_eSPI.h> #include <TFT_eSPI.h>
#include "ScreenData.h" #include "ScreenData.h"
// Grid layout constants
#define DASH_COLS 3 #define DASH_COLS 3
#define DASH_ROWS 2 #define DASH_ROWS 2
#define DASH_MARGIN 8 #define DASH_MARGIN 8
#define DASH_TOP_BAR 40 #define DASH_TOP_BAR 40
#define TILE_W ((480 - (DASH_COLS + 1) * DASH_MARGIN) / DASH_COLS)
#define TILE_H ((320 - DASH_TOP_BAR - (DASH_ROWS + 1) * DASH_MARGIN) / DASH_ROWS)
// 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
// Tile IDs
enum TileID : uint8_t { enum TileID : uint8_t {
TILE_LAST_ALERT = 0, TILE_LAST_ALERT = 0,
TILE_STATS, TILE_STATS,
@@ -37,13 +33,11 @@ class Dashboard {
public: public:
Dashboard(TFT_eSPI& tft); Dashboard(TFT_eSPI& tft);
void begin(); // create sprite (call once) void begin();
void drawAll(); // fill screen + draw all tiles void drawAll();
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);
// Populate tiles from ScreenState — only redraws changed tiles
void refreshFromState(const ScreenState& state); void refreshFromState(const ScreenState& state);
private: private:
@@ -51,7 +45,6 @@ private:
TFT_eSprite _sprite; TFT_eSprite _sprite;
TileData _tiles[TILE_COUNT]; TileData _tiles[TILE_COUNT];
// Cached top bar values for dirty check
char _barTime[12] = ""; char _barTime[12] = "";
int _barRSSI = 0; int _barRSSI = 0;
bool _barWifiOk = false; bool _barWifiOk = false;

View File

@@ -9,10 +9,12 @@ DisplayManager::DisplayManager()
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); // landscape: 480x320 _tft.setRotation(1);
_tft.fillScreen(COL_BLACK); _tft.fillScreen(COL_BLACK);
uint16_t calData[5] = { 300, 3600, 300, 3600, 1 };
_tft.setTouch(calData);
} }
void DisplayManager::setBacklight(bool on) { void DisplayManager::setBacklight(bool on) {
@@ -35,10 +37,53 @@ int DisplayManager::dashboardTouch(uint16_t x, uint16_t y) {
return _dash.handleTouch(x, y); return _dash.handleTouch(x, y);
} }
// =====================================================================
// Hold detection
// =====================================================================
HoldState DisplayManager::updateHold(unsigned long requiredMs) {
HoldState h;
h.targetMs = requiredMs;
uint16_t tx, ty;
bool touching = _tft.getTouch(&tx, &ty);
if (touching) {
if (!_holdActive) {
_holdActive = true;
_holdStartMs = millis();
_holdX = tx;
_holdY = ty;
}
h.active = true;
h.x = _holdX;
h.y = _holdY;
h.holdMs = millis() - _holdStartMs;
_holdProgress = min((float)h.holdMs / (float)requiredMs, 1.0f);
if (h.holdMs >= requiredMs) {
h.completed = true;
_holdActive = false;
_holdProgress = 0.0f;
}
} else {
_holdActive = false;
_holdProgress = 0.0f;
}
return h;
}
// =====================================================================
// Render
// =====================================================================
void DisplayManager::render(const ScreenState& state) { void DisplayManager::render(const ScreenState& state) {
// Detect screen change → force full redraw
if (state.screen != _lastScreen) { if (state.screen != _lastScreen) {
_needsFullRedraw = true; _needsFullRedraw = true;
if (state.screen == ScreenID::OFF) {
setBacklight(false);
} else if (_lastScreen == ScreenID::OFF) {
setBacklight(true);
}
_lastScreen = state.screen; _lastScreen = state.screen;
} }
@@ -60,6 +105,9 @@ void DisplayManager::render(const ScreenState& state) {
drawAlertScreen(state); drawAlertScreen(state);
_lastBlink = state.blinkPhase; _lastBlink = state.blinkPhase;
} }
if (_holdProgress > 0.0f) {
drawSilenceProgress(_holdProgress);
}
break; break;
case ScreenID::STATUS: case ScreenID::STATUS:
if (_needsFullRedraw) drawStatusScreen(state); if (_needsFullRedraw) drawStatusScreen(state);
@@ -68,42 +116,76 @@ void DisplayManager::render(const ScreenState& state) {
drawDashboard(state); drawDashboard(state);
break; break;
case ScreenID::OFF: case ScreenID::OFF:
if (_needsFullRedraw) {
_tft.fillScreen(COL_BLACK);
_dashSpriteReady = false;
}
break; break;
} }
_needsFullRedraw = false; _needsFullRedraw = false;
} }
// ----- Dashboard ----- // =====================================================================
// Dashboard
// =====================================================================
void DisplayManager::drawDashboard(const ScreenState& s) { void DisplayManager::drawDashboard(const ScreenState& s) {
if (_needsFullRedraw) { if (_needsFullRedraw) {
if (!_dashSpriteReady) { if (!_dashSpriteReady) {
_dash.begin(); // create sprite (once) _dash.begin();
_dashSpriteReady = true; _dashSpriteReady = true;
} }
_dash.drawAll(); // fill screen + draw all tiles _dash.drawAll();
_dash.refreshFromState(s); // update with real data _dash.refreshFromState(s);
_lastDashRefresh = millis(); _lastDashRefresh = millis();
} else if (millis() - _lastDashRefresh > 2000) { } else if (millis() - _lastDashRefresh > 2000) {
_lastDashRefresh = millis(); _lastDashRefresh = millis();
_dash.refreshFromState(s); // only redraws changed tiles _dash.refreshFromState(s);
} }
} }
// ----- Helpers ----- // =====================================================================
// Silence progress bar
// =====================================================================
void DisplayManager::drawSilenceProgress(float progress) {
int barX = 10;
int barY = SCREEN_HEIGHT - 35;
int barW = SCREEN_WIDTH - 20;
int barH = 25;
int fillW = (int)(barW * progress);
_tft.fillRect(barX, barY, barW, barH, COL_BLACK);
if (fillW > 0) {
_tft.fillRect(barX, barY, fillW, barH, COL_GREEN);
}
_tft.drawRect(barX, barY, barW, barH, COL_WHITE);
_tft.setTextFont(1);
_tft.setTextSize(2);
_tft.setTextDatum(MC_DATUM);
if (progress >= 1.0f) {
_tft.setTextColor(COL_BLACK);
_tft.drawString("SILENCED", SCREEN_WIDTH / 2, barY + barH / 2);
} else {
_tft.setTextColor(COL_WHITE);
_tft.drawString("HOLD TO SILENCE", SCREEN_WIDTH / 2, barY + barH / 2);
}
}
// =====================================================================
// 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) {
_tft.setTextFont(1); // ← ADDED: force GLCD font _tft.setTextFont(1);
_tft.setTextSize(sz); _tft.setTextSize(sz);
_tft.setTextColor(col); _tft.setTextColor(col);
int w = _tft.textWidth(txt); // ← use TFT_eSPI's own width calc int w = _tft.textWidth(txt);
_tft.setCursor(max(0, (SCREEN_WIDTH - w) / 2), y); _tft.setCursor(max(0, (SCREEN_WIDTH - w) / 2), y);
_tft.print(txt); _tft.print(txt);
} }
void DisplayManager::drawInfoLine(int x, int y, uint16_t col, const char* text) { void DisplayManager::drawInfoLine(int x, int y, uint16_t col, const char* text) {
_tft.setTextFont(1); // ← ADDED _tft.setTextFont(1);
_tft.setTextSize(1); _tft.setTextSize(1);
_tft.setTextColor(col); _tft.setTextColor(col);
_tft.setCursor(x, y); _tft.setCursor(x, y);
@@ -111,23 +193,24 @@ void DisplayManager::drawInfoLine(int x, int y, uint16_t col, const char* text)
} }
void DisplayManager::drawHeaderBar(uint16_t col, const char* label, const char* timeStr) { void DisplayManager::drawHeaderBar(uint16_t col, const char* label, const char* timeStr) {
_tft.setTextFont(1); // ← ADDED _tft.setTextFont(1);
_tft.setTextSize(2); _tft.setTextSize(2);
_tft.setTextColor(col); _tft.setTextColor(col);
_tft.setCursor(8, 8); _tft.setCursor(8, 8);
_tft.print(label); _tft.print(label);
int tw = _tft.textWidth(timeStr);
int tw = _tft.textWidth(timeStr); // ← proper width calc
_tft.setCursor(SCREEN_WIDTH - tw - 8, 8); _tft.setCursor(SCREEN_WIDTH - tw - 8, 8);
_tft.print(timeStr); _tft.print(timeStr);
} }
// ----- Screens -----
// =====================================================================
// Screens
// =====================================================================
void DisplayManager::drawBootSplash(const ScreenState& s) { void DisplayManager::drawBootSplash(const ScreenState& s) {
_tft.fillScreen(COL_BLACK); _tft.fillScreen(COL_BLACK);
drawCentered("KLUBHAUS", 60, 4, COL_NEON_TEAL); drawCentered("KLUBHAUS", 60, 4, COL_NEON_TEAL);
drawCentered("ALERT", 110, 4, COL_HOT_FUCHSIA); drawCentered("ALERT", 110, 4, COL_HOT_FUCHSIA);
drawCentered("v5.0 E32R35T", 180, 2, COL_DARK_GRAY); drawCentered("v5.1 E32R35T", 180, 2, COL_DARK_GRAY);
if (s.debugMode) { if (s.debugMode) {
drawCentered("DEBUG MODE", 210, 2, COL_YELLOW); drawCentered("DEBUG MODE", 210, 2, COL_YELLOW);
} }
@@ -179,26 +262,25 @@ void DisplayManager::drawAlertScreen(const ScreenState& s) {
drawCentered(s.alertMessage, (SCREEN_HEIGHT - 8 * sz) / 2, sz, fg); drawCentered(s.alertMessage, (SCREEN_HEIGHT - 8 * sz) / 2, sz, fg);
} }
drawCentered("TAP TO SILENCE", SCREEN_HEIGHT - 25, 2, fg); // Only show hint text if not currently holding
if (_holdProgress == 0.0f) {
drawCentered("HOLD TO SILENCE", SCREEN_HEIGHT - 25, 2, fg);
}
} }
void DisplayManager::drawStatusScreen(const ScreenState& s) { void DisplayManager::drawStatusScreen(const ScreenState& s) {
_tft.fillScreen(COL_BLACK); _tft.fillScreen(COL_BLACK);
drawHeaderBar(COL_MINT, "KLUBHAUS", s.timeString); drawHeaderBar(COL_MINT, "KLUBHAUS", s.timeString);
drawCentered("MONITORING", 60, 3, COL_WHITE); drawCentered("MONITORING", 60, 3, COL_WHITE);
char buf[80]; char buf[80];
int y = 110; int y = 110, sp = 22, x = 20;
int sp = 22;
int x = 20;
snprintf(buf, sizeof(buf), "WiFi: %s (%ddBm)", 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; drawInfoLine(x, y, COL_WHITE, buf); y += sp;
snprintf(buf, sizeof(buf), "IP: %s", snprintf(buf, sizeof(buf), "IP: %s", s.wifiConnected ? s.wifiIP : "---");
s.wifiConnected ? s.wifiIP : "---");
drawInfoLine(x, y, COL_WHITE, buf); y += sp; drawInfoLine(x, y, COL_WHITE, buf); y += sp;
snprintf(buf, sizeof(buf), "Up: %lu min Heap: %lu KB", snprintf(buf, sizeof(buf), "Up: %lu min Heap: %lu KB",
@@ -222,66 +304,13 @@ void DisplayManager::drawStatusScreen(const ScreenState& s) {
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;
snprintf(buf, sizeof(buf), "%s %.35s", snprintf(buf, sizeof(buf), "%s %.35s",
s.alertHistory[i].timestamp, s.alertHistory[i].timestamp, 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) {
drawInfoLine(x, y, COL_YELLOW, "DEBUG MODE - _test topics");
}
drawCentered("tap to dismiss", SCREEN_HEIGHT - 18, 1, COL_DARK_GRAY); drawCentered("tap to dismiss", SCREEN_HEIGHT - 18, 1, COL_DARK_GRAY);
} }
HoldState DisplayManager::updateHold(unsigned long requiredMs) {
HoldState h;
h.targetMs = requiredMs;
uint16_t tx, ty;
bool touching = _tft.getTouch(&tx, &ty);
if (touching) {
if (!_holdActive) {
_holdActive = true;
_holdStartMs = millis();
_holdX = tx;
_holdY = ty;
}
h.active = true;
h.x = _holdX;
h.y = _holdY;
h.holdMs = millis() - _holdStartMs;
if (h.holdMs >= requiredMs) {
h.completed = true;
_holdActive = false;
}
} else {
_holdActive = false;
}
return h;
}
void DisplayManager::drawSilenceProgress(float progress) {
int barY = SCREEN_HEIGHT - 30;
int barH = 20;
int barW = (int)(SCREEN_WIDTH * progress);
_tft.fillRect(0, barY, SCREEN_WIDTH, barH, COL_BLACK);
_tft.fillRect(0, barY, barW, barH, COL_GREEN);
_tft.drawRect(0, barY, SCREEN_WIDTH, barH, COL_WHITE);
_tft.setTextFont(1);
_tft.setTextSize(2);
_tft.setTextColor(progress < 1.0f ? COL_WHITE : COL_BLACK);
_tft.setTextDatum(MC_DATUM);
_tft.drawString(
progress < 1.0f ? "HOLD TO SILENCE" : "SILENCED",
SCREEN_WIDTH / 2, barY + barH / 2);
}

View File

@@ -3,14 +3,13 @@
#include "ScreenData.h" #include "ScreenData.h"
#include "Dashboard.h" #include "Dashboard.h"
// Hold gesture result
struct HoldState { struct HoldState {
bool active = false; // finger currently down bool active = false;
bool completed = false; // hold duration met bool completed = false;
uint16_t x = 0; uint16_t x = 0;
uint16_t y = 0; uint16_t y = 0;
unsigned long holdMs = 0; // how long held so far unsigned long holdMs = 0;
unsigned long targetMs = 0; // required hold duration unsigned long targetMs = 0;
}; };
class DisplayManager { class DisplayManager {
@@ -21,29 +20,25 @@ public:
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); int dashboardTouch(uint16_t x, uint16_t y);
// Hold detection — call each loop iteration
HoldState updateHold(unsigned long requiredMs); HoldState updateHold(unsigned long requiredMs);
// Draw hold-to-silence progress bar on alert screen
void drawSilenceProgress(float progress);
private: private:
TFT_eSPI _tft; TFT_eSPI _tft;
Dashboard _dash; 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; bool _dashSpriteReady = false;
unsigned long _lastDashRefresh = 0; unsigned long _lastDashRefresh = 0;
// Hold tracking state // Hold tracking
bool _holdActive = false; bool _holdActive = false;
unsigned long _holdStartMs = 0; unsigned long _holdStartMs = 0;
uint16_t _holdX = 0; uint16_t _holdX = 0;
uint16_t _holdY = 0; uint16_t _holdY = 0;
float _holdProgress = 0.0f;
// Colors // Colors
static constexpr uint16_t COL_NEON_TEAL = 0x07D7; static constexpr uint16_t COL_NEON_TEAL = 0x07D7;
@@ -64,6 +59,7 @@ private:
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); void drawDashboard(const ScreenState& s);
void drawSilenceProgress(float progress);
// 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

@@ -13,12 +13,12 @@ void DoorbellLogic::begin() {
} }
void DoorbellLogic::beginWiFi() { void DoorbellLogic::beginWiFi() {
_instance = this; // ← MISSING _instance = this;
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.setSleep(false); WiFi.setSleep(false);
WiFi.setAutoReconnect(true); // ← MISSING WiFi.setAutoReconnect(true);
WiFi.onEvent(onWiFiEvent); // ← MISSING WiFi.onEvent(onWiFiEvent);
for (int i = 0; i < NUM_WIFI; i++) { for (int i = 0; i < NUM_WIFI; i++) {
_wifiMulti.addAP(wifiNetworks[i].ssid, wifiNetworks[i].pass); _wifiMulti.addAP(wifiNetworks[i].ssid, wifiNetworks[i].pass);
@@ -117,11 +117,6 @@ void DoorbellLogic::update() {
switch (_state) { switch (_state) {
case DeviceState::ALERTING: case DeviceState::ALERTING:
// if (now - _alertStart > ALERT_TIMEOUT_MS) {
// Serial.println("[ALERT] Timeout — auto-silencing");
// handleSilence("timeout");
// break;
// }
if (now - _lastBlink >= BLINK_INTERVAL_MS) { if (now - _lastBlink >= BLINK_INTERVAL_MS) {
_lastBlink = now; _lastBlink = now;
_blinkState = !_blinkState; _blinkState = !_blinkState;
@@ -138,7 +133,7 @@ void DoorbellLogic::update() {
break; break;
} }
if (now - _lastHeartbeat >= HEARTBEAT_INTERVAL_MS) { if (now - _lastHeartbeat >= HEARTBEAT_INTERVAL_MS) {
_lastHeartbeat = now; _lastHeartbeat = now;
uint32_t heap = ESP.getFreeHeap(); uint32_t heap = ESP.getFreeHeap();
Serial.printf("[%lus] %s | WiFi:%s RSSI:%d | heap:%dKB | minHeap:%dKB\n", Serial.printf("[%lus] %s | WiFi:%s RSSI:%d | heap:%dKB | minHeap:%dKB\n",
@@ -157,7 +152,7 @@ if (now - _lastHeartbeat >= HEARTBEAT_INTERVAL_MS) {
delay(200); delay(200);
ESP.restart(); ESP.restart();
} }
} }
updateScreenState(); updateScreenState();
} }
@@ -239,8 +234,8 @@ void DoorbellLogic::transitionTo(DeviceState newState) {
break; break;
case DeviceState::WAKE: case DeviceState::WAKE:
_wakeStart = now; _wakeStart = now;
_screen.screen = ScreenID::DASHBOARD; _screen.screen = ScreenID::DASHBOARD; // ← CHANGED from STATUS
Serial.println("-> WAKE (dashboard)"); Serial.println("-> WAKE (dashboard)"); // ← CHANGED
break; break;
} }
} }
@@ -253,7 +248,6 @@ void DoorbellLogic::handleAlert(const String& msg) {
_currentMessage = msg; _currentMessage = msg;
_alertMsgEpoch = _lastParsedMsgEpoch; _alertMsgEpoch = _lastParsedMsgEpoch;
// Push into history (shift older entries down)
for (int i = ALERT_HISTORY_SIZE - 1; i > 0; i--) { for (int i = ALERT_HISTORY_SIZE - 1; i > 0; i--) {
_screen.alertHistory[i] = _screen.alertHistory[i - 1]; _screen.alertHistory[i] = _screen.alertHistory[i - 1];
} }
@@ -278,8 +272,6 @@ void DoorbellLogic::handleSilence(const String& msg) {
return; return;
} }
// If this came from ntfy poll (_lastParsedMsgEpoch > 0), reject if it
// predates or equals the alert. Both timestamps are from ntfy's server clock.
if (_lastParsedMsgEpoch > 0 && _alertMsgEpoch > 0 && if (_lastParsedMsgEpoch > 0 && _alertMsgEpoch > 0 &&
_lastParsedMsgEpoch <= _alertMsgEpoch) { _lastParsedMsgEpoch <= _alertMsgEpoch) {
Serial.printf("[SILENCE] Ignored — predates alert (silence:%ld <= alert:%ld)\n", Serial.printf("[SILENCE] Ignored — predates alert (silence:%ld <= alert:%ld)\n",
@@ -444,7 +436,6 @@ void DoorbellLogic::pollTopic(const char* url,
http.end(); http.end();
client.stop(); client.stop();
yield(); yield();
} }
@@ -529,7 +520,7 @@ void DoorbellLogic::parseMessages(String& response, const char* name,
} }
// ===================================================================== // =====================================================================
// Status Publishing (deferred) // Status Publishing
// ===================================================================== // =====================================================================
void DoorbellLogic::queueStatus(const char* st, const String& msg) { void DoorbellLogic::queueStatus(const char* st, const String& msg) {
_pendingStatus = true; _pendingStatus = true;
@@ -567,7 +558,6 @@ DoorbellLogic* DoorbellLogic::_instance = nullptr;
void DoorbellLogic::onWiFiEvent(WiFiEvent_t event) { void DoorbellLogic::onWiFiEvent(WiFiEvent_t event) {
if (!_instance) return; if (!_instance) return;
switch (event) { switch (event) {
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
Serial.println("[WIFI] Disconnected — will reconnect"); Serial.println("[WIFI] Disconnected — will reconnect");
@@ -584,4 +574,3 @@ void DoorbellLogic::onWiFiEvent(WiFiEvent_t event) {
} }
} }

View File

@@ -1,10 +1,6 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
// =====================================================================
// Shared enums and structs — NO library dependencies
// =====================================================================
enum class DeviceState : uint8_t { enum class DeviceState : uint8_t {
SILENT, SILENT,
ALERTING, ALERTING,
@@ -18,50 +14,42 @@ enum class ScreenID : uint8_t {
WIFI_FAILED, WIFI_FAILED,
ALERT, ALERT,
STATUS, STATUS,
DASHBOARD, // <-- NEW DASHBOARD,
OFF // backlight off, nothing to draw OFF
}; };
#define ALERT_HISTORY_SIZE 3 #define ALERT_HISTORY_SIZE 3
struct AlertRecord { struct AlertRecord {
char message[64] = ""; char message[64] = "";
char timestamp[12] = ""; // "HH:MM:SS" char timestamp[12] = "";
}; };
// Everything the display needs to render any screen
struct ScreenState { struct ScreenState {
ScreenID screen = ScreenID::BOOT_SPLASH; ScreenID screen = ScreenID::BOOT_SPLASH;
DeviceState deviceState = DeviceState::SILENT; DeviceState deviceState = DeviceState::SILENT;
bool blinkPhase = false; bool blinkPhase = false;
// Alert
char alertMessage[64] = ""; char alertMessage[64] = "";
// WiFi
bool wifiConnected = false; bool wifiConnected = false;
char wifiSSID[33] = ""; char wifiSSID[33] = "";
int wifiRSSI = 0; int wifiRSSI = 0;
char wifiIP[16] = ""; char wifiIP[16] = "";
// NTP
bool ntpSynced = false; bool ntpSynced = false;
char timeString[12] = ""; char timeString[12] = "";
// System
uint32_t uptimeMinutes = 0; uint32_t uptimeMinutes = 0;
uint32_t freeHeapKB = 0; uint32_t freeHeapKB = 0;
bool networkOK = false; bool networkOK = false;
// Debug
bool debugMode = false; bool debugMode = false;
// 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
struct TouchEvent { struct TouchEvent {
bool pressed = false; bool pressed = false;
uint16_t x = 0; uint16_t x = 0;

View File

@@ -1,88 +1,80 @@
/* #include <SPI.h>
* KLUBHAUS ALERT v5.0 — E32R35T Edition
*
* Target: LCDWiki E32R35T (ESP32-WROOM-32E + 3.5" ST7796S + XPT2046)
*
* Refactored: business logic separated from display code.
* Business logic knows nothing about TFT_eSPI.
* Display knows nothing about ntfy/WiFi/state machine.
* They communicate through ScreenState (plain struct).
*/
#include "Config.h" #include "Config.h"
#include "DisplayManager.h"
#include "DoorbellLogic.h" #include "DoorbellLogic.h"
#include "DisplayManager.h"
#define HOLD_TO_SILENCE_MS 1000
DisplayManager display;
DoorbellLogic logic; DoorbellLogic logic;
DisplayManager display;
#include <TFT_eSPI.h>
#ifndef LOAD_GLCD
#error "LOAD_GLCD is NOT defined — fonts missing!"
#endif
#ifndef ST7796_DRIVER
#error "ST7796_DRIVER is NOT defined — wrong setup!"
#endif
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
unsigned long t = millis(); delay(1000);
while (!Serial && millis() - t < 3000) delay(10); Serial.println("\n=== KLUBHAUS ALERT v5.1 ===");
delay(500);
Serial.println("\n========================================");
Serial.println(" KLUBHAUS ALERT v5.0 — E32R35T");
#if DEBUG_MODE
Serial.println(" *** DEBUG MODE — _test topics ***");
#endif
Serial.println("========================================");
// 1. Init display hardware
display.begin(); display.begin();
// 2. Init logic (sets boot splash screen state)
logic.begin(); logic.begin();
display.render(logic.getScreenState()); display.render(logic.getScreenState());
delay(1500); delay(2000);
// 3. WiFi (logic updates screen state, we render each phase) logic.beginWiFi();
// We need a small coupling here for the blocking WiFi connect
// This could be made async later
logic.beginWiFi(); // sets screen to WIFI_CONNECTING
display.render(logic.getScreenState()); display.render(logic.getScreenState());
logic.connectWiFiBlocking(); // blocks, sets CONNECTED or FAILED logic.connectWiFiBlocking();
display.render(logic.getScreenState()); display.render(logic.getScreenState());
delay(1500); delay(1500);
// 4. Finish boot
logic.finishBoot(); logic.finishBoot();
display.setBacklight(false); display.render(logic.getScreenState());
Serial.println("[BOOT] Ready — monitoring ntfy.sh\n");
} }
void loop() { void loop() {
// 1. Read touch logic.update();
TouchEvent touch = display.readTouch();
logic.onTouch(touch);
// 2. Read serial commands const ScreenState& state = logic.getScreenState();
// ---- Touch handling (varies by screen) ----
if (state.screen == ScreenID::ALERT) {
// Hold-to-silence: progress bar drawn by render()
HoldState hold = display.updateHold(HOLD_TO_SILENCE_MS);
if (hold.completed) {
Serial.println("[HOLD] Silence hold completed");
logic.onTouch(TouchEvent{true, hold.x, hold.y});
}
} else if (state.screen == ScreenID::DASHBOARD) {
// Dashboard: tile taps don't dismiss, outside taps dismiss
TouchEvent evt = display.readTouch();
if (evt.pressed) {
int tile = display.dashboardTouch(evt.x, evt.y);
if (tile >= 0) {
Serial.printf("[DASH] Tile %d tapped\n", tile);
// Tile-specific actions go here later
} else {
// Tap outside tiles — dismiss dashboard
logic.onTouch(evt);
}
}
} else {
// All other screens (OFF, boot, etc): simple touch
TouchEvent evt = display.readTouch();
if (evt.pressed) {
logic.onTouch(evt);
}
}
// ---- Render ----
display.render(logic.getScreenState());
// ---- Serial commands ----
if (Serial.available()) { if (Serial.available()) {
String cmd = Serial.readStringUntil('\n'); String cmd = Serial.readStringUntil('\n');
cmd.trim(); cmd.trim();
logic.onSerialCommand(cmd); if (cmd.length() > 0) logic.onSerialCommand(cmd);
} }
// 3. Update business logic
logic.update();
// 4. Render
const ScreenState& state = logic.getScreenState();
display.setBacklight(state.screen != ScreenID::OFF);
display.render(state);
delay(20);
} }