diff --git a/sketches/doorbell-touch-esp32-32e/Dashboard.cpp b/sketches/doorbell-touch-esp32-32e/Dashboard.cpp index 8635f38..ae0f7a0 100644 --- a/sketches/doorbell-touch-esp32-32e/Dashboard.cpp +++ b/sketches/doorbell-touch-esp32-32e/Dashboard.cpp @@ -1,29 +1,19 @@ #include "Dashboard.h" -// 16-bit color helpers -#define COL_BG 0x1082 // dark charcoal -#define COL_BAR 0x2104 // slightly lighter +#define COL_BG 0x1082 +#define COL_BAR 0x2104 #define COL_RED 0xF800 #define COL_ORANGE 0xFBE0 #define COL_GREEN 0x07E0 #define COL_CYAN 0x07FF -#define COL_BLUE 0x001F #define COL_PURPLE 0x780F -#define COL_YELLOW 0xFFE0 #define COL_WHITE 0xFFFF #define COL_GRAY 0x8410 #define COL_DARK_TILE 0x18E3 -// Tile color themes static const uint16_t tileBG[] = { - COL_RED, // LAST_ALERT - COL_ORANGE, // STATS - COL_CYAN, // NETWORK - COL_PURPLE, // MUTE - COL_DARK_TILE, // HISTORY - COL_DARK_TILE // SYSTEM + COL_RED, COL_ORANGE, COL_CYAN, COL_PURPLE, COL_DARK_TILE, COL_DARK_TILE }; - static const uint16_t tileFG[] = { COL_WHITE, COL_WHITE, 0x0000, COL_WHITE, COL_WHITE, COL_WHITE }; @@ -31,12 +21,12 @@ static const uint16_t tileFG[] = { Dashboard::Dashboard(TFT_eSPI& tft) : _tft(tft), _sprite(&tft) { - _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_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 }; for (int i = 0; i < TILE_COUNT; i++) { _tiles[i].bgColor = tileBG[i]; @@ -72,25 +62,21 @@ void Dashboard::drawTile(TileID id) { _sprite.fillSprite(t.bgColor); _sprite.drawRoundRect(0, 0, TILE_W, TILE_H, 8, COL_GRAY); - // Icon — large character at top _sprite.setTextColor(t.fgColor, t.bgColor); _sprite.setTextFont(4); _sprite.setTextSize(2); _sprite.setTextDatum(TC_DATUM); _sprite.drawString(t.icon, TILE_W / 2, 8); - // Label _sprite.setTextSize(1); _sprite.setTextFont(2); _sprite.setTextDatum(MC_DATUM); _sprite.drawString(t.label, TILE_W / 2, TILE_H / 2 + 5); - // Value _sprite.setTextFont(2); _sprite.setTextDatum(BC_DATUM); _sprite.drawString(t.value, TILE_W / 2, TILE_H - 25); - // Sub text if (strlen(t.sub) > 0) { _sprite.setTextFont(1); _sprite.setTextDatum(BC_DATUM); @@ -103,7 +89,6 @@ void Dashboard::drawTile(TileID id) { void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) { _tft.fillRect(0, 0, 480, DASH_TOP_BAR, COL_BAR); - _tft.setTextColor(COL_WHITE, COL_BAR); _tft.setTextFont(4); _tft.setTextSize(1); @@ -114,7 +99,6 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) { _tft.setTextDatum(MR_DATUM); _tft.drawString(time, 470, DASH_TOP_BAR / 2); - // WiFi signal bars int bars = 0; if (wifiOk) { 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 bars = 1; } - int barX = 370, barW = 6, barGap = 3; for (int i = 0; i < 4; i++) { int barH = 6 + i * 5; @@ -131,26 +114,19 @@ void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) { _tft.fillRect(barX + i * (barW + barGap), barY, barW, barH, col); } - // Cache values strncpy(_barTime, time, sizeof(_barTime) - 1); - _barRSSI = rssi; + _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) { - strncpy(t.sub, sub, 31); - t.sub[31] = '\0'; - } + strncpy(t.value, value, 31); t.value[31] = '\0'; + if (sub) { strncpy(t.sub, sub, 31); t.sub[31] = '\0'; } t.dirty = true; drawTile(id); } @@ -159,19 +135,13 @@ int Dashboard::handleTouch(int x, int y) { for (int i = 0; i < TILE_COUNT; i++) { int tx, ty; tilePosition((TileID)i, tx, ty); - if (x >= tx && x < tx + TILE_W && - y >= ty && y < ty + TILE_H) { + if (x >= tx && x < tx + TILE_W && y >= ty && y < ty + TILE_H) return i; - } } 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); @@ -179,7 +149,6 @@ void Dashboard::refreshFromState(const ScreenState& state) { drawTopBar(state.timeString, state.wifiRSSI, state.wifiConnected); } - // LAST ALERT tile if (state.alertHistoryCount > 0) { updateTile(TILE_LAST_ALERT, state.alertHistory[0].message, @@ -188,14 +157,12 @@ void Dashboard::refreshFromState(const ScreenState& state) { 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); @@ -204,10 +171,8 @@ void Dashboard::refreshFromState(const ScreenState& state) { 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", @@ -220,7 +185,6 @@ void Dashboard::refreshFromState(const ScreenState& state) { updateTile(TILE_HISTORY, "no history", ""); } - // SYSTEM tile char heapBuf[16]; snprintf(heapBuf, sizeof(heapBuf), "%lu KB", state.freeHeapKB); char uptimeBuf[20]; diff --git a/sketches/doorbell-touch-esp32-32e/Dashboard.h b/sketches/doorbell-touch-esp32-32e/Dashboard.h index e4a3a4d..9bf1332 100644 --- a/sketches/doorbell-touch-esp32-32e/Dashboard.h +++ b/sketches/doorbell-touch-esp32-32e/Dashboard.h @@ -2,17 +2,13 @@ #include #include "ScreenData.h" -// Grid layout constants #define DASH_COLS 3 #define DASH_ROWS 2 #define DASH_MARGIN 8 #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 { TILE_LAST_ALERT = 0, TILE_STATS, @@ -37,13 +33,11 @@ class Dashboard { public: Dashboard(TFT_eSPI& tft); - void begin(); // create sprite (call once) - void drawAll(); // fill screen + draw all tiles + void begin(); + void drawAll(); 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 - - // Populate tiles from ScreenState — only redraws changed tiles + int handleTouch(int x, int y); void refreshFromState(const ScreenState& state); private: @@ -51,7 +45,6 @@ private: TFT_eSprite _sprite; TileData _tiles[TILE_COUNT]; - // Cached top bar values for dirty check char _barTime[12] = ""; int _barRSSI = 0; bool _barWifiOk = false; diff --git a/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp b/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp index 60ba5bb..d22857f 100644 --- a/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp +++ b/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp @@ -9,10 +9,12 @@ DisplayManager::DisplayManager() void DisplayManager::begin() { pinMode(PIN_LCD_BL, OUTPUT); setBacklight(true); - _tft.init(); - _tft.setRotation(1); // landscape: 480x320 + _tft.setRotation(1); _tft.fillScreen(COL_BLACK); + + uint16_t calData[5] = { 300, 3600, 300, 3600, 1 }; + _tft.setTouch(calData); } void DisplayManager::setBacklight(bool on) { @@ -35,10 +37,53 @@ int DisplayManager::dashboardTouch(uint16_t x, uint16_t 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) { - // Detect screen change → force full redraw if (state.screen != _lastScreen) { _needsFullRedraw = true; + if (state.screen == ScreenID::OFF) { + setBacklight(false); + } else if (_lastScreen == ScreenID::OFF) { + setBacklight(true); + } _lastScreen = state.screen; } @@ -60,6 +105,9 @@ void DisplayManager::render(const ScreenState& state) { drawAlertScreen(state); _lastBlink = state.blinkPhase; } + if (_holdProgress > 0.0f) { + drawSilenceProgress(_holdProgress); + } break; case ScreenID::STATUS: if (_needsFullRedraw) drawStatusScreen(state); @@ -68,42 +116,76 @@ void DisplayManager::render(const ScreenState& state) { drawDashboard(state); break; case ScreenID::OFF: + if (_needsFullRedraw) { + _tft.fillScreen(COL_BLACK); + _dashSpriteReady = false; + } break; } _needsFullRedraw = false; } -// ----- Dashboard ----- - +// ===================================================================== +// Dashboard +// ===================================================================== void DisplayManager::drawDashboard(const ScreenState& s) { if (_needsFullRedraw) { if (!_dashSpriteReady) { - _dash.begin(); // create sprite (once) + _dash.begin(); _dashSpriteReady = true; } - _dash.drawAll(); // fill screen + draw all tiles - _dash.refreshFromState(s); // update with real data + _dash.drawAll(); + _dash.refreshFromState(s); _lastDashRefresh = millis(); } else if (millis() - _lastDashRefresh > 2000) { _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) { - _tft.setTextFont(1); // ← ADDED: force GLCD font + _tft.setTextFont(1); _tft.setTextSize(sz); _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.print(txt); } void DisplayManager::drawInfoLine(int x, int y, uint16_t col, const char* text) { - _tft.setTextFont(1); // ← ADDED + _tft.setTextFont(1); _tft.setTextSize(1); _tft.setTextColor(col); _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) { - _tft.setTextFont(1); // ← ADDED + _tft.setTextFont(1); _tft.setTextSize(2); _tft.setTextColor(col); _tft.setCursor(8, 8); _tft.print(label); - - int tw = _tft.textWidth(timeStr); // ← proper width calc + int tw = _tft.textWidth(timeStr); _tft.setCursor(SCREEN_WIDTH - tw - 8, 8); _tft.print(timeStr); } -// ----- Screens ----- +// ===================================================================== +// Screens +// ===================================================================== void DisplayManager::drawBootSplash(const ScreenState& s) { _tft.fillScreen(COL_BLACK); drawCentered("KLUBHAUS", 60, 4, COL_NEON_TEAL); 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) { 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("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) { _tft.fillScreen(COL_BLACK); drawHeaderBar(COL_MINT, "KLUBHAUS", s.timeString); - drawCentered("MONITORING", 60, 3, COL_WHITE); char buf[80]; - int y = 110; - int sp = 22; - int x = 20; + int y = 110, sp = 22, x = 20; snprintf(buf, sizeof(buf), "WiFi: %s (%ddBm)", 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", @@ -222,66 +304,13 @@ void DisplayManager::drawStatusScreen(const ScreenState& s) { 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); + 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; } - if (s.debugMode) { - drawInfoLine(x, y, COL_YELLOW, "DEBUG MODE - _test topics"); - } - 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); -} - diff --git a/sketches/doorbell-touch-esp32-32e/DisplayManager.h b/sketches/doorbell-touch-esp32-32e/DisplayManager.h index c2f0e64..b8e6b32 100644 --- a/sketches/doorbell-touch-esp32-32e/DisplayManager.h +++ b/sketches/doorbell-touch-esp32-32e/DisplayManager.h @@ -3,14 +3,13 @@ #include "ScreenData.h" #include "Dashboard.h" -// Hold gesture result struct HoldState { - bool active = false; // finger currently down - bool completed = false; // hold duration met + bool active = false; + bool completed = false; uint16_t x = 0; uint16_t y = 0; - unsigned long holdMs = 0; // how long held so far - unsigned long targetMs = 0; // required hold duration + unsigned long holdMs = 0; + unsigned long targetMs = 0; }; class DisplayManager { @@ -21,29 +20,25 @@ public: void setBacklight(bool on); TouchEvent readTouch(); - // Dashboard tile touch — returns TileID or -1 - int dashboardTouch(uint16_t x, uint16_t y); - - // Hold detection — call each loop iteration + int dashboardTouch(uint16_t x, uint16_t y); HoldState updateHold(unsigned long requiredMs); - // Draw hold-to-silence progress bar on alert screen - void drawSilenceProgress(float progress); - private: TFT_eSPI _tft; Dashboard _dash; - ScreenID _lastScreen = ScreenID::BOOT_SPLASH; - bool _needsFullRedraw = true; - bool _lastBlink = false; - bool _dashSpriteReady = false; + + ScreenID _lastScreen = ScreenID::BOOT_SPLASH; + bool _needsFullRedraw = true; + bool _lastBlink = false; + bool _dashSpriteReady = false; unsigned long _lastDashRefresh = 0; - // Hold tracking state - bool _holdActive = false; - unsigned long _holdStartMs = 0; - uint16_t _holdX = 0; - uint16_t _holdY = 0; + // Hold tracking + bool _holdActive = false; + unsigned long _holdStartMs = 0; + uint16_t _holdX = 0; + uint16_t _holdY = 0; + float _holdProgress = 0.0f; // Colors static constexpr uint16_t COL_NEON_TEAL = 0x07D7; @@ -64,6 +59,7 @@ private: void drawAlertScreen(const ScreenState& s); void drawStatusScreen(const ScreenState& s); void drawDashboard(const ScreenState& s); + void drawSilenceProgress(float progress); // 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 0d546fc..6ca2400 100644 --- a/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp +++ b/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp @@ -1,7 +1,7 @@ #include "DoorbellLogic.h" // ===================================================================== -// Lifecycle +// Lifecycle // ===================================================================== void DoorbellLogic::begin() { _bootTime = millis(); @@ -13,12 +13,12 @@ void DoorbellLogic::begin() { } void DoorbellLogic::beginWiFi() { - _instance = this; // ← MISSING + _instance = this; WiFi.mode(WIFI_STA); WiFi.setSleep(false); - WiFi.setAutoReconnect(true); // ← MISSING - WiFi.onEvent(onWiFiEvent); // ← MISSING + WiFi.setAutoReconnect(true); + WiFi.onEvent(onWiFiEvent); for (int i = 0; i < NUM_WIFI; i++) { _wifiMulti.addAP(wifiNetworks[i].ssid, wifiNetworks[i].pass); @@ -78,16 +78,16 @@ void DoorbellLogic::finishBoot() { flushStatus(); } - Serial.printf("[CONFIG] ALERT_URL: %s\n", ALERT_URL); + Serial.printf("[CONFIG] ALERT_URL: %s\n", ALERT_URL); Serial.printf("[CONFIG] SILENCE_URL: %s\n", SILENCE_URL); - Serial.printf("[CONFIG] ADMIN_URL: %s\n", ADMIN_URL); + Serial.printf("[CONFIG] ADMIN_URL: %s\n", ADMIN_URL); transitionTo(DeviceState::SILENT); Serial.printf("[BOOT] Grace period: %d ms\n", BOOT_GRACE_MS); } // ===================================================================== -// Main Update Loop +// Main Update Loop // ===================================================================== void DoorbellLogic::update() { unsigned long now = millis(); @@ -117,11 +117,6 @@ void DoorbellLogic::update() { switch (_state) { case DeviceState::ALERTING: - // if (now - _alertStart > ALERT_TIMEOUT_MS) { - // Serial.println("[ALERT] Timeout — auto-silencing"); - // handleSilence("timeout"); - // break; - // } if (now - _lastBlink >= BLINK_INTERVAL_MS) { _lastBlink = now; _blinkState = !_blinkState; @@ -138,32 +133,32 @@ void DoorbellLogic::update() { break; } -if (now - _lastHeartbeat >= HEARTBEAT_INTERVAL_MS) { - _lastHeartbeat = now; - uint32_t heap = ESP.getFreeHeap(); - Serial.printf("[%lus] %s | WiFi:%s RSSI:%d | heap:%dKB | minHeap:%dKB\n", - now / 1000, - _state == DeviceState::SILENT ? "SILENT" : - _state == DeviceState::ALERTING ? "ALERT" : "WAKE", - WiFi.isConnected() ? "OK" : "DOWN", - WiFi.RSSI(), - heap / 1024, - ESP.getMinFreeHeap() / 1024); + if (now - _lastHeartbeat >= HEARTBEAT_INTERVAL_MS) { + _lastHeartbeat = now; + uint32_t heap = ESP.getFreeHeap(); + Serial.printf("[%lus] %s | WiFi:%s RSSI:%d | heap:%dKB | minHeap:%dKB\n", + now / 1000, + _state == DeviceState::SILENT ? "SILENT" : + _state == DeviceState::ALERTING ? "ALERT" : "WAKE", + WiFi.isConnected() ? "OK" : "DOWN", + WiFi.RSSI(), + heap / 1024, + ESP.getMinFreeHeap() / 1024); - if (heap < 20000) { - Serial.println("[HEAP] CRITICAL — rebooting!"); - queueStatus("REBOOTING", "low heap"); - flushStatus(); - delay(200); - ESP.restart(); + if (heap < 20000) { + Serial.println("[HEAP] CRITICAL — rebooting!"); + queueStatus("REBOOTING", "low heap"); + flushStatus(); + delay(200); + ESP.restart(); + } } -} updateScreenState(); } // ===================================================================== -// Input Events +// Input Events // ===================================================================== void DoorbellLogic::onTouch(const TouchEvent& evt) { if (!evt.pressed) return; @@ -196,7 +191,7 @@ void DoorbellLogic::onSerialCommand(const String& cmd) { checkNetwork(); } else if (cmd == "STATUS") { Serial.printf("[CMD] State:%s WiFi:%s RSSI:%d Heap:%dKB NTP:%s\n", - _state == DeviceState::SILENT ? "SILENT" : + _state == DeviceState::SILENT ? "SILENT" : _state == DeviceState::ALERTING ? "ALERT" : "WAKE", WiFi.isConnected() ? WiFi.SSID().c_str() : "DOWN", WiFi.RSSI(), @@ -218,7 +213,7 @@ void DoorbellLogic::onSerialCommand(const String& cmd) { } // ===================================================================== -// State Transitions +// State Transitions // ===================================================================== void DoorbellLogic::transitionTo(DeviceState newState) { _state = newState; @@ -232,28 +227,27 @@ void DoorbellLogic::transitionTo(DeviceState newState) { break; case DeviceState::ALERTING: _alertStart = now; - _lastBlink = now; + _lastBlink = now; _blinkState = false; _screen.screen = ScreenID::ALERT; Serial.printf("-> ALERTING: %s\n", _currentMessage.c_str()); break; case DeviceState::WAKE: _wakeStart = now; - _screen.screen = ScreenID::DASHBOARD; - Serial.println("-> WAKE (dashboard)"); + _screen.screen = ScreenID::DASHBOARD; // ← CHANGED from STATUS + Serial.println("-> WAKE (dashboard)"); // ← CHANGED break; } } // ===================================================================== -// Message Handlers +// Message Handlers // ===================================================================== void DoorbellLogic::handleAlert(const String& msg) { if (_state == DeviceState::ALERTING && _currentMessage == msg) return; _currentMessage = msg; _alertMsgEpoch = _lastParsedMsgEpoch; - // Push into history (shift older entries down) for (int i = ALERT_HISTORY_SIZE - 1; i > 0; i--) { _screen.alertHistory[i] = _screen.alertHistory[i - 1]; } @@ -278,8 +272,6 @@ void DoorbellLogic::handleSilence(const String& msg) { 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 && _lastParsedMsgEpoch <= _alertMsgEpoch) { Serial.printf("[SILENCE] Ignored — predates alert (silence:%ld <= alert:%ld)\n", @@ -298,13 +290,13 @@ void DoorbellLogic::handleSilence(const String& msg) { void DoorbellLogic::handleAdmin(const String& msg) { Serial.printf("[ADMIN] %s\n", msg.c_str()); - if (msg == "SILENCE") handleSilence("admin"); - else if (msg == "PING") queueStatus("PONG", "ping"); - else if (msg == "test") handleAlert("TEST ALERT"); + if (msg == "SILENCE") handleSilence("admin"); + else if (msg == "PING") queueStatus("PONG", "ping"); + else if (msg == "test") handleAlert("TEST ALERT"); else if (msg == "status") { char buf[128]; snprintf(buf, sizeof(buf), "State:%s WiFi:%s RSSI:%d Heap:%dKB", - _state == DeviceState::SILENT ? "SILENT" : + _state == DeviceState::SILENT ? "SILENT" : _state == DeviceState::ALERTING ? "ALERT" : "WAKE", WiFi.SSID().c_str(), WiFi.RSSI(), ESP.getFreeHeap() / 1024); queueStatus("STATUS", buf); @@ -321,15 +313,15 @@ void DoorbellLogic::handleAdmin(const String& msg) { } // ===================================================================== -// Screen State Sync +// Screen State Sync // ===================================================================== void DoorbellLogic::updateScreenState() { - _screen.deviceState = _state; - _screen.blinkPhase = _blinkState; + _screen.deviceState = _state; + _screen.blinkPhase = _blinkState; strncpy(_screen.alertMessage, _currentMessage.c_str(), sizeof(_screen.alertMessage) - 1); _screen.alertMessage[sizeof(_screen.alertMessage) - 1] = '\0'; - _screen.wifiConnected = WiFi.isConnected(); + _screen.wifiConnected = WiFi.isConnected(); if (_screen.wifiConnected) { strncpy(_screen.wifiSSID, WiFi.SSID().c_str(), sizeof(_screen.wifiSSID) - 1); _screen.wifiSSID[sizeof(_screen.wifiSSID) - 1] = '\0'; @@ -338,20 +330,20 @@ void DoorbellLogic::updateScreenState() { _screen.wifiRSSI = WiFi.RSSI(); } - _screen.ntpSynced = _ntpSynced; + _screen.ntpSynced = _ntpSynced; if (_ntpSynced) { strncpy(_screen.timeString, _timeClient->getFormattedTime().c_str(), - sizeof(_screen.timeString) - 1); + sizeof(_screen.timeString) - 1); _screen.timeString[sizeof(_screen.timeString) - 1] = '\0'; } - _screen.uptimeMinutes = (millis() - _bootTime) / 60000; - _screen.freeHeapKB = ESP.getFreeHeap() / 1024; - _screen.networkOK = _networkOK; + _screen.uptimeMinutes = (millis() - _bootTime) / 60000; + _screen.freeHeapKB = ESP.getFreeHeap() / 1024; + _screen.networkOK = _networkOK; } // ===================================================================== -// WiFi & Network +// WiFi & Network // ===================================================================== void DoorbellLogic::syncNTP() { if (_timeClient->update()) { @@ -385,7 +377,7 @@ void DoorbellLogic::checkNetwork() { } // ===================================================================== -// ntfy Polling +// ntfy Polling // ===================================================================== void DoorbellLogic::pollTopics() { Serial.printf("[POLL] Starting poll cycle... WiFi:%s NTP:%d Grace:%d\n", @@ -393,16 +385,16 @@ void DoorbellLogic::pollTopics() { pollTopic(ALERT_URL, &DoorbellLogic::handleAlert, "ALERT", _lastAlertId); yield(); - pollTopic(SILENCE_URL, &DoorbellLogic::handleSilence, "SILENCE", _lastSilenceId); + pollTopic(SILENCE_URL, &DoorbellLogic::handleSilence, "SILENCE", _lastSilenceId); yield(); - pollTopic(ADMIN_URL, &DoorbellLogic::handleAdmin, "ADMIN", _lastAdminId); + pollTopic(ADMIN_URL, &DoorbellLogic::handleAdmin, "ADMIN", _lastAdminId); Serial.printf("[POLL] Done. Heap: %dKB\n", ESP.getFreeHeap() / 1024); } void DoorbellLogic::pollTopic(const char* url, - void (DoorbellLogic::*handler)(const String&), - const char* name, String& lastId) { + void (DoorbellLogic::*handler)(const String&), + const char* name, String& lastId) { Serial.printf("[%s] Polling: %s\n", name, url); if (!WiFi.isConnected()) { @@ -444,13 +436,12 @@ void DoorbellLogic::pollTopic(const char* url, http.end(); client.stop(); - yield(); } void DoorbellLogic::parseMessages(String& response, const char* name, - void (DoorbellLogic::*handler)(const String&), - String& lastId) { + void (DoorbellLogic::*handler)(const String&), + String& lastId) { Serial.printf("[%s] parseMessages: grace=%d ntp=%d epoch=%ld\n", name, _inBootGrace, _ntpSynced, (long)_lastEpoch); @@ -480,12 +471,12 @@ void DoorbellLogic::parseMessages(String& response, const char* name, const char* event = doc["event"]; const char* msgId = doc["id"]; const char* message = doc["message"]; - time_t msgTime = doc["time"] | 0; + time_t msgTime = doc["time"] | 0; Serial.printf("[%s] msg#%d: event=%s id=%s time=%ld msg=%.30s\n", name, msgCount, - event ? event : "null", - msgId ? msgId : "null", + event ? event : "null", + msgId ? msgId : "null", (long)msgTime, message ? message : "null"); @@ -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) { _pendingStatus = true; @@ -567,7 +558,6 @@ DoorbellLogic* DoorbellLogic::_instance = nullptr; void DoorbellLogic::onWiFiEvent(WiFiEvent_t event) { if (!_instance) return; - switch (event) { case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: Serial.println("[WIFI] Disconnected — will reconnect"); @@ -584,4 +574,3 @@ void DoorbellLogic::onWiFiEvent(WiFiEvent_t event) { } } - diff --git a/sketches/doorbell-touch-esp32-32e/ScreenData.h b/sketches/doorbell-touch-esp32-32e/ScreenData.h index 6ab9623..16e145e 100644 --- a/sketches/doorbell-touch-esp32-32e/ScreenData.h +++ b/sketches/doorbell-touch-esp32-32e/ScreenData.h @@ -1,10 +1,6 @@ #pragma once #include -// ===================================================================== -// Shared enums and structs — NO library dependencies -// ===================================================================== - enum class DeviceState : uint8_t { SILENT, ALERTING, @@ -18,50 +14,42 @@ enum class ScreenID : uint8_t { WIFI_FAILED, ALERT, STATUS, - DASHBOARD, // <-- NEW - OFF // backlight off, nothing to draw + DASHBOARD, + OFF }; #define ALERT_HISTORY_SIZE 3 struct AlertRecord { char message[64] = ""; - char timestamp[12] = ""; // "HH:MM:SS" + char timestamp[12] = ""; }; -// Everything the display needs to render any screen struct ScreenState { ScreenID screen = ScreenID::BOOT_SPLASH; DeviceState deviceState = DeviceState::SILENT; bool blinkPhase = false; - // Alert char alertMessage[64] = ""; - // WiFi bool wifiConnected = false; char wifiSSID[33] = ""; int wifiRSSI = 0; char wifiIP[16] = ""; - // NTP bool ntpSynced = false; char timeString[12] = ""; - // System uint32_t uptimeMinutes = 0; uint32_t freeHeapKB = 0; bool networkOK = false; - // Debug bool debugMode = false; - // Alert history (newest first) AlertRecord alertHistory[ALERT_HISTORY_SIZE] = {}; int alertHistoryCount = 0; }; -// Touch event passed from display to logic struct TouchEvent { bool pressed = false; uint16_t x = 0; diff --git a/sketches/doorbell-touch-esp32-32e/doorbell-touch-esp32-32e.ino b/sketches/doorbell-touch-esp32-32e/doorbell-touch-esp32-32e.ino index af4ec3d..7148154 100644 --- a/sketches/doorbell-touch-esp32-32e/doorbell-touch-esp32-32e.ino +++ b/sketches/doorbell-touch-esp32-32e/doorbell-touch-esp32-32e.ino @@ -1,88 +1,80 @@ -/* - * 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 #include "Config.h" -#include "DisplayManager.h" #include "DoorbellLogic.h" +#include "DisplayManager.h" + +#define HOLD_TO_SILENCE_MS 1000 -DisplayManager display; DoorbellLogic logic; - -#include - -#ifndef LOAD_GLCD - #error "LOAD_GLCD is NOT defined — fonts missing!" -#endif - -#ifndef ST7796_DRIVER - #error "ST7796_DRIVER is NOT defined — wrong setup!" -#endif +DisplayManager display; void setup() { Serial.begin(115200); - unsigned long t = millis(); - while (!Serial && millis() - t < 3000) delay(10); - delay(500); + delay(1000); + Serial.println("\n=== KLUBHAUS ALERT v5.1 ==="); - 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(); - // 2. Init logic (sets boot splash screen state) logic.begin(); display.render(logic.getScreenState()); - delay(1500); + delay(2000); - // 3. WiFi (logic updates screen state, we render each phase) - // We need a small coupling here for the blocking WiFi connect - // This could be made async later - logic.beginWiFi(); // sets screen to WIFI_CONNECTING + logic.beginWiFi(); display.render(logic.getScreenState()); - logic.connectWiFiBlocking(); // blocks, sets CONNECTED or FAILED + logic.connectWiFiBlocking(); display.render(logic.getScreenState()); delay(1500); - // 4. Finish boot logic.finishBoot(); - display.setBacklight(false); - - Serial.println("[BOOT] Ready — monitoring ntfy.sh\n"); + display.render(logic.getScreenState()); } void loop() { - // 1. Read touch - TouchEvent touch = display.readTouch(); - logic.onTouch(touch); + logic.update(); - // 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()) { String cmd = Serial.readStringUntil('\n'); 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); } +