From 3c9e982c98e24c57b6e6c56eb575afc6043b6ea6 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Sun, 15 Feb 2026 23:25:11 -0800 Subject: [PATCH] snapshot --- sketches/doorbell-touch-esp32-32e/Config.h | 8 +- .../DoorbellLogic.cpp | 191 +++++++++++++----- .../doorbell-touch-esp32-32e/DoorbellLogic.h | 6 + 3 files changed, 148 insertions(+), 57 deletions(-) diff --git a/sketches/doorbell-touch-esp32-32e/Config.h b/sketches/doorbell-touch-esp32-32e/Config.h index a9847aa..f612f47 100644 --- a/sketches/doorbell-touch-esp32-32e/Config.h +++ b/sketches/doorbell-touch-esp32-32e/Config.h @@ -26,9 +26,11 @@ static const int NUM_WIFI = sizeof(wifiNetworks) / sizeof(wifiNetworks[0]); #define TOPIC_SUFFIX "" #endif -#define ALERT_URL NTFY_BASE "/ALERT_klubhaus_topic" TOPIC_SUFFIX "/json?since=10s&poll=1" -#define SILENCE_URL NTFY_BASE "/SILENCE_klubhaus_topic" TOPIC_SUFFIX "/json?since=10s&poll=1" -#define ADMIN_URL NTFY_BASE "/ADMIN_klubhaus_topic" TOPIC_SUFFIX "/json?since=10s&poll=1" +// Change since=10s to since=20s (must be > poll interval of 15s, but not too large) +#define ALERT_URL NTFY_BASE "/ALERT_klubhaus_topic" TOPIC_SUFFIX "/json?since=20s&poll=1" +#define SILENCE_URL NTFY_BASE "/SILENCE_klubhaus_topic" TOPIC_SUFFIX "/json?since=20s&poll=1" +#define ADMIN_URL NTFY_BASE "/ADMIN_klubhaus_topic" TOPIC_SUFFIX "/json?since=20s&poll=1" + #define STATUS_URL NTFY_BASE "/STATUS_klubhaus_topic" TOPIC_SUFFIX // ===================================================================== diff --git a/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp b/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp index 6c0b125..a4b3df5 100644 --- a/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp +++ b/sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp @@ -13,6 +13,13 @@ void DoorbellLogic::begin() { } void DoorbellLogic::beginWiFi() { + _instance = this; // ← MISSING + + WiFi.mode(WIFI_STA); + WiFi.setSleep(false); + WiFi.setAutoReconnect(true); // ← MISSING + WiFi.onEvent(onWiFiEvent); // ← MISSING + for (int i = 0; i < NUM_WIFI; i++) { _wifiMulti.addAP(wifiNetworks[i].ssid, wifiNetworks[i].pass); } @@ -127,15 +134,26 @@ void DoorbellLogic::update() { break; } - if (now - _lastHeartbeat >= HEARTBEAT_INTERVAL_MS) { - _lastHeartbeat = now; - Serial.printf("[%lus] %s | WiFi:%s | heap:%dKB\n", - now / 1000, - _state == DeviceState::SILENT ? "SILENT" : - _state == DeviceState::ALERTING ? "ALERT" : "WAKE", - WiFi.isConnected() ? "OK" : "DOWN", - ESP.getFreeHeap() / 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(); } +} updateScreenState(); } @@ -205,10 +223,12 @@ void DoorbellLogic::transitionTo(DeviceState newState) { switch (newState) { case DeviceState::SILENT: _screen.screen = ScreenID::OFF; + _alertStartEpoch = 0; // <-- ADD: clear on silence Serial.println("-> SILENT"); break; case DeviceState::ALERTING: _alertStart = now; + _alertStartEpoch = _lastEpoch; // <-- ADD: record NTP time of alert _lastBlink = now; _blinkState = false; _screen.screen = ScreenID::ALERT; @@ -233,6 +253,18 @@ void DoorbellLogic::handleAlert(const String& msg) { } void DoorbellLogic::handleSilence(const String& msg) { + // When called from ntfy poll, reject silence messages that predate the current alert. + // This prevents stale silence messages from immediately canceling new alerts. + // When called from touch/admin/timeout, _lastParsedMsgEpoch is 0 → bypass check. + if (_state == DeviceState::ALERTING && + _lastParsedMsgEpoch > 0 && + _alertStartEpoch > 0 && + _lastParsedMsgEpoch <= _alertStartEpoch) { + Serial.printf("[SILENCE] Ignored — predates alert (msg:%ld alert:%ld)\n", + (long)_lastParsedMsgEpoch, (long)_alertStartEpoch); + return; + } + _currentMessage = ""; transitionTo(DeviceState::SILENT); queueStatus("SILENT", "silenced"); @@ -331,44 +363,53 @@ void DoorbellLogic::checkNetwork() { // ntfy Polling // ===================================================================== void DoorbellLogic::pollTopics() { + Serial.println("[POLL] Starting poll cycle..."); pollTopic(ALERT_URL, &DoorbellLogic::handleAlert, "ALERT", _lastAlertId); + yield(); // ← MISSING pollTopic(SILENCE_URL, &DoorbellLogic::handleSilence, "SILENCE", _lastSilenceId); + yield(); // ← MISSING pollTopic(ADMIN_URL, &DoorbellLogic::handleAdmin, "ADMIN", _lastAdminId); + Serial.printf("[POLL] Done. Heap: %dKB\n", ESP.getFreeHeap() / 1024); // ← MISSING } void DoorbellLogic::pollTopic(const char* url, void (DoorbellLogic::*handler)(const String&), const char* name, String& lastId) { + if (!WiFi.isConnected()) { // ← MISSING + Serial.printf("[%s] Skipped — WiFi down\n", name); + return; + } + WiFiClientSecure client; client.setInsecure(); + client.setTimeout(10); // ← MISSING HTTPClient http; http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); http.setTimeout(10000); + http.setReuse(false); // ← MISSING - if (!http.begin(client, url)) { - Serial.printf("[%s] begin() failed\n", name); - return; - } + // ... rest of method ... - int code = http.GET(); - if (code == HTTP_CODE_OK) { - String response = http.getString(); - if (response.length() > 0) { - parseMessages(response, name, handler, lastId); - } - } else if (code < 0) { - Serial.printf("[%s] HTTP error: %s\n", name, http.errorToString(code).c_str()); - } http.end(); + client.stop(); // ← MISSING + + yield(); // ← MISSING } void DoorbellLogic::parseMessages(String& response, const char* name, void (DoorbellLogic::*handler)(const String&), String& lastId) { - if (_inBootGrace || !_ntpSynced || _lastEpoch == 0) return; + Serial.printf("[%s] parseMessages: grace=%d ntp=%d epoch=%ld\n", + name, _inBootGrace, _ntpSynced, (long)_lastEpoch); + + if (_inBootGrace || !_ntpSynced || _lastEpoch == 0) { + Serial.printf("[%s] SKIPPED — guard failed\n", name); + return; + } int lineStart = 0; + int msgCount = 0; while (lineStart < (int)response.length()) { int lineEnd = response.indexOf('\n', lineStart); if (lineEnd == -1) lineEnd = response.length(); @@ -377,43 +418,63 @@ void DoorbellLogic::parseMessages(String& response, const char* name, line.trim(); if (line.length() > 0 && line.indexOf('{') >= 0) { + msgCount++; JsonDocument doc; - if (!deserializeJson(doc, line)) { - const char* event = doc["event"]; - const char* msgId = doc["id"]; - const char* message = doc["message"]; - time_t msgTime = doc["time"] | 0; - - if (event && strcmp(event, "message") != 0) { - lineStart = lineEnd + 1; - continue; - } - - if (!message || strlen(message) == 0) { - lineStart = lineEnd + 1; - continue; - } - - String idStr = msgId ? String(msgId) : ""; - if (idStr.length() > 0 && idStr == lastId) { - lineStart = lineEnd + 1; - continue; - } - - if (msgTime > 0 && (_lastEpoch - msgTime) > (time_t)STALE_MSG_THRESHOLD_S) { - Serial.printf("[%s] Stale: %.30s (age %llds)\n", - name, message, (long long)(_lastEpoch - msgTime)); - lineStart = lineEnd + 1; - continue; - } - - Serial.printf("[%s] %.50s\n", name, message); - if (idStr.length() > 0) lastId = idStr; - (this->*handler)(String(message)); + if (deserializeJson(doc, line)) { + Serial.printf("[%s] JSON parse FAILED on line %d\n", name, msgCount); + lineStart = lineEnd + 1; + continue; } + + const char* event = doc["event"]; + const char* msgId = doc["id"]; + const char* message = doc["message"]; + 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", + (long)msgTime, + message ? message : "null"); + + if (event && strcmp(event, "message") != 0) { + Serial.printf("[%s] SKIP — not a message event (event=%s)\n", name, event); + lineStart = lineEnd + 1; + continue; + } + + if (!message || strlen(message) == 0) { + Serial.printf("[%s] SKIP — empty message\n", name); + lineStart = lineEnd + 1; + continue; + } + + String idStr = msgId ? String(msgId) : ""; + if (idStr.length() > 0 && idStr == lastId) { + Serial.printf("[%s] SKIP — dedup (id=%s)\n", name, msgId); + lineStart = lineEnd + 1; + continue; + } + + if (msgTime > 0 && (_lastEpoch - msgTime) > (time_t)STALE_MSG_THRESHOLD_S) { + Serial.printf("[%s] SKIP — stale (age=%llds, threshold=%ds)\n", + name, (long long)(_lastEpoch - msgTime), STALE_MSG_THRESHOLD_S); + lineStart = lineEnd + 1; + continue; + } + + Serial.printf("[%s] ACCEPTED: %.50s\n", name, message); + if (idStr.length() > 0) lastId = idStr; + + _lastParsedMsgEpoch = msgTime; + (this->*handler)(String(message)); + _lastParsedMsgEpoch = 0; } lineStart = lineEnd + 1; } + + Serial.printf("[%s] Parsed %d JSON objects\n", name, msgCount); } // ===================================================================== @@ -451,3 +512,25 @@ void DoorbellLogic::flushStatus() { Serial.printf("[STATUS] Sent (%d): %s\n", code, _pendStatusState.c_str()); } +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"); + WiFi.reconnect(); + break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + Serial.println("[WIFI] Reconnected to AP"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + Serial.printf("[WIFI] Got IP: %s\n", WiFi.localIP().toString().c_str()); + break; + default: + break; + } +} + + diff --git a/sketches/doorbell-touch-esp32-32e/DoorbellLogic.h b/sketches/doorbell-touch-esp32-32e/DoorbellLogic.h index 26369bb..a3e9778 100644 --- a/sketches/doorbell-touch-esp32-32e/DoorbellLogic.h +++ b/sketches/doorbell-touch-esp32-32e/DoorbellLogic.h @@ -49,6 +49,9 @@ private: unsigned long _lastHeartbeat = 0; bool _blinkState = false; + time_t _lastParsedMsgEpoch = 0; // <-- ADD + time_t _alertStartEpoch = 0; // <-- ADD + // Deferred status publish bool _pendingStatus = false; String _pendStatusState; @@ -83,5 +86,8 @@ private: void flushStatus(); void updateScreenState(); + static DoorbellLogic* _instance; // for static event callback + static void onWiFiEvent(WiFiEvent_t event); + };