From dc62265fbba932108061c2d77c2fd326c08c7270 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Fri, 13 Feb 2026 21:42:32 -0800 Subject: [PATCH] snapshot --- doorbell-touch.ino | 182 +++++++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 90 deletions(-) diff --git a/doorbell-touch.ino b/doorbell-touch.ino index c3bbc40..a0a8c07 100644 --- a/doorbell-touch.ino +++ b/doorbell-touch.ino @@ -1,11 +1,16 @@ /* - * KLUBHAUS ALERT v4.1 — Touch Edition + * KLUBHAUS ALERT v4.2 — Touch Edition * * Target: Waveshare ESP32-S3-Touch-LCD-4.3 (non-B) * - * v4.1 fixes: + * v4.2 fixes: + * - OPI PSRAM mode (double bandwidth — fixes WiFi+RGB coexistence) + * - Bounce buffer for RGB DMA * - Fixed variadic lambda crash in drawStatusScreen * - Single tap for all interactions + * - Deferred status publishing (no nested HTTPS) + * - Per-topic message dedup + * - since=10s (no stale message replay) * - DEBUG_MODE with _test topic suffix */ @@ -13,26 +18,26 @@ #include #include #include +#include #include #include #include #include #include -#include // ===================================================================== -// DEBUG MODE — set to true to use _test topic suffix +// DEBUG MODE — set to 1 to use _test topic suffix // ===================================================================== -#define DEBUG_MODE 0 +#define DEBUG_MODE 1 // ===================================================================== // WiFi // ===================================================================== struct WiFiCred { const char *ssid; const char *pass; }; WiFiCred wifiNetworks[] = { - { "Dobro Veče", "goodnight" }, - { "berylpunk", "dhgwilliam" }, - // { "iot-2GHz", "lesson-greater" }, + { "Dobro Veče", "goodnight" }, + { "berylpunk", "dhgwilliam" }, + // { "iot-2GHz", "lesson-greater" }, // blocks outbound TCP }; const int NUM_WIFI = sizeof(wifiNetworks) / sizeof(wifiNetworks[0]); WiFiMulti wifiMulti; @@ -56,7 +61,7 @@ WiFiMulti wifiMulti; // ===================================================================== // Timing // ===================================================================== -#define POLL_INTERVAL_MS 5000 +#define POLL_INTERVAL_MS 15000 #define BLINK_INTERVAL_MS 500 #define STALE_MSG_THRESHOLD_S 600 #define NTP_SYNC_INTERVAL_MS 3600000 @@ -178,7 +183,7 @@ bool gt911_read(int16_t *x, int16_t *y) { } // ===================================================================== -// RGB Display — correct pins for 4.3 non-B +// RGB Display — 4.3 non-B with bounce buffer for WiFi coexistence // ===================================================================== Arduino_ESP32RGBPanel *rgbpanel = new Arduino_ESP32RGBPanel( 5, 3, 46, 7, @@ -187,7 +192,11 @@ Arduino_ESP32RGBPanel *rgbpanel = new Arduino_ESP32RGBPanel( 14, 38, 18, 17, 10, 0, 8, 4, 8, 0, 8, 4, 8, - 1, 16000000 + 1, 16000000, // ← back to 16MHz + false, // useBigEndian + 0, // de_idle_high + 0, // pclk_idle_high + 8000 // bounce_buffer_size_px ); Arduino_RGB_Display *gfx = new Arduino_RGB_Display( @@ -206,27 +215,27 @@ NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, NTP_SYNC_INTERVAL_MS); enum DeviceState { STATE_SILENT, STATE_ALERTING, STATE_WAKE }; DeviceState currentState = STATE_SILENT; -String currentMessage = ""; -String lastAlertId = ""; -String lastSilenceId = ""; -String lastAdminId = ""; -time_t lastKnownEpoch = 0; -bool ntpSynced = false; +String currentMessage = ""; +String lastAlertId = ""; +String lastSilenceId = ""; +String lastAdminId = ""; +time_t lastKnownEpoch = 0; +bool ntpSynced = false; -unsigned long bootTime = 0; -bool inBootGrace = true; +unsigned long bootTime = 0; +bool inBootGrace = true; -unsigned long lastPoll = 0; -unsigned long lastBlinkToggle = 0; -bool blinkState = false; -unsigned long alertStart = 0; -unsigned long wakeStart = 0; +unsigned long lastPoll = 0; +unsigned long lastBlinkToggle = 0; +bool blinkState = false; +unsigned long alertStart = 0; +unsigned long wakeStart = 0; -// Deferred status publishing — avoids nested HTTPS connections -bool pendingStatus = false; -String pendingStatusState = ""; -String pendingStatusMsg = ""; +bool pendingStatus = false; +String pendingStatusState = ""; +String pendingStatusMsg = ""; +bool networkOK = false; // ===================================================================== // Drawing Helpers @@ -239,7 +248,6 @@ void drawCentered(const char *txt, int y, int sz, uint16_t col) { gfx->print(txt); } -// Safe line drawing — replaces the variadic lambda that caused crashes void drawInfoLine(int x, int y, uint16_t col, const char *text) { gfx->setTextSize(2); gfx->setTextColor(col); @@ -309,8 +317,9 @@ void drawStatusScreen() { drawInfoLine(x, y, COL_WHITE, buf); y += spacing; - snprintf(buf, sizeof(buf), "Up: %lu min Heap: %d KB", - (millis() - bootTime) / 60000, ESP.getFreeHeap() / 1024); + snprintf(buf, sizeof(buf), "Up: %lu min Heap: %d KB PSRAM: %d KB", + (millis() - bootTime) / 60000, ESP.getFreeHeap() / 1024, + ESP.getFreePsram() / 1024); drawInfoLine(x, y, COL_WHITE, buf); y += spacing; @@ -319,9 +328,10 @@ void drawStatusScreen() { drawInfoLine(x, y, COL_WHITE, buf); y += spacing; - snprintf(buf, sizeof(buf), "State: %s", + snprintf(buf, sizeof(buf), "State: %s Net: %s", currentState == STATE_SILENT ? "SILENT" : - currentState == STATE_ALERTING ? "ALERTING" : "WAKE"); + currentState == STATE_ALERTING ? "ALERTING" : "WAKE", + networkOK ? "OK" : "FAIL"); uint16_t stCol = currentState == STATE_ALERTING ? COL_RED : currentState == STATE_SILENT ? COL_GREEN : COL_NEON_TEAL; drawInfoLine(x, y, stCol, buf); @@ -377,6 +387,7 @@ void flushStatus() { pendingStatusState = ""; pendingStatusMsg = ""; } + // ===================================================================== // Message Parsing — per-topic dedup // ===================================================================== @@ -412,13 +423,11 @@ void parseMessages(String &response, const char *topicName, String msgStr = String(message); String idStr = msgId ? String(msgId) : ""; - // Per-topic dedup if (idStr.length() > 0 && idStr == lastId) { lineStart = lineEnd + 1; continue; } - // Age check if (msgTime > 0) { time_t age = lastKnownEpoch - msgTime; if (age > (time_t)STALE_MSG_THRESHOLD_S) { @@ -440,20 +449,17 @@ void parseMessages(String &response, const char *topicName, } // ===================================================================== -// ntfy Polling — with diagnostics and proper HTTPS +// Network Diagnostics // ===================================================================== -#include - -bool networkOK = false; - void checkNetwork() { Serial.printf("[NET] WiFi: %s RSSI: %d IP: %s\n", WiFi.SSID().c_str(), WiFi.RSSI(), WiFi.localIP().toString().c_str()); Serial.printf("[NET] GW: %s DNS: %s\n", WiFi.gatewayIP().toString().c_str(), WiFi.dnsIP().toString().c_str()); + Serial.printf("[NET] Heap: %d KB PSRAM free: %d KB\n", + ESP.getFreeHeap() / 1024, ESP.getFreePsram() / 1024); Serial.flush(); - // Test DNS IPAddress ip; if (!WiFi.hostByName("ntfy.sh", ip)) { Serial.println("[NET] *** DNS FAILED ***"); @@ -462,35 +468,24 @@ void checkNetwork() { } Serial.printf("[NET] ntfy.sh -> %s\n", ip.toString().c_str()); - // Test TCP connectivity on port 80 - WiFiClient testClient; - Serial.println("[NET] Testing TCP to ntfy.sh:80..."); + WiFiClientSecure tls; + tls.setInsecure(); + Serial.println("[NET] Testing TLS to ntfy.sh:443..."); Serial.flush(); - if (testClient.connect("ntfy.sh", 80, 5000)) { - Serial.println("[NET] TCP port 80 OK!"); - testClient.stop(); + if (tls.connect("ntfy.sh", 443, 15000)) { + Serial.println("[NET] TLS OK!"); + tls.stop(); networkOK = true; } else { - Serial.println("[NET] *** TCP port 80 BLOCKED ***"); + Serial.println("[NET] *** TLS FAILED ***"); + networkOK = false; } - - // Test TCP connectivity on port 443 - WiFiClient testClient2; - Serial.println("[NET] Testing TCP to ntfy.sh:443..."); - Serial.flush(); - if (testClient2.connect("ntfy.sh", 443, 5000)) { - Serial.println("[NET] TCP port 443 OK!"); - testClient2.stop(); - networkOK = true; - } else { - Serial.println("[NET] *** TCP port 443 BLOCKED ***"); - } - Serial.flush(); } -#include - +// ===================================================================== +// ntfy Polling +// ===================================================================== void pollTopic(const char *url, void (*handler)(const String&), const char *topicName, @@ -519,6 +514,7 @@ void pollTopic(const char *url, } http.end(); } + // ===================================================================== // Message Handlers // ===================================================================== @@ -560,11 +556,12 @@ void handleAdminMessage(const String &message) { else if (message == "status") { char buf[256]; snprintf(buf, sizeof(buf), - "State:%s WiFi:%s RSSI:%d Heap:%dKB Up:%lus", + "State:%s WiFi:%s RSSI:%d Heap:%dKB Up:%lus Net:%s", currentState == STATE_SILENT ? "SILENT" : currentState == STATE_ALERTING ? "ALERT" : "WAKE", WiFi.SSID().c_str(), WiFi.RSSI(), - ESP.getFreeHeap() / 1024, (millis() - bootTime) / 1000); + ESP.getFreeHeap() / 1024, (millis() - bootTime) / 1000, + networkOK ? "OK" : "FAIL"); queueStatus("STATUS", buf); } else if (message == "wake") { @@ -575,23 +572,23 @@ void handleAdminMessage(const String &message) { } else if (message == "REBOOT") { queueStatus("REBOOTING", "admin"); + flushStatus(); delay(200); ESP.restart(); } } // ===================================================================== -// Touch — trigger on finger DOWN only, not while held +// Touch — trigger on finger DOWN only // ===================================================================== void handleTouch() { int16_t tx, ty; bool touching = gt911_read(&tx, &ty); - static bool wasTouching = false; + static bool wasTouching = false; static unsigned long lastAction = 0; unsigned long now = millis(); - // Only act on the transition: NOT touching → touching (new finger down) if (touching && !wasTouching) { if (now - lastAction >= TOUCH_DEBOUNCE_MS) { lastAction = now; @@ -692,16 +689,17 @@ void setup() { bootTime = millis(); Serial.println("\n========================================"); - Serial.println(" KLUBHAUS ALERT v4.1 — Touch Edition"); + Serial.println(" KLUBHAUS ALERT v4.2 — Touch Edition"); #if DEBUG_MODE Serial.println(" *** DEBUG MODE — _test topics ***"); #endif Serial.printf(" Grace period: %d ms\n", BOOT_GRACE_MS); Serial.println("========================================"); - Serial.printf("PSRAM: %d MB\n", ESP.getPsramSize() / (1024 * 1024)); + Serial.printf("Heap: %d KB PSRAM: %d KB\n", + ESP.getFreeHeap() / 1024, ESP.getPsramSize() / 1024); if (ESP.getPsramSize() == 0) { - Serial.println("PSRAM required!"); + Serial.println("PSRAM required! Check FQBN has PSRAM=opi"); while (true) delay(1000); } @@ -711,14 +709,17 @@ void setup() { gfx->fillScreen(COL_BLACK); drawCentered("KLUBHAUS", 120, 6, COL_NEON_TEAL); drawCentered("ALERT", 200, 6, COL_HOT_FUCHSIA); - drawCentered("v4.1 Touch", 300, 2, COL_DARK_GRAY); + drawCentered("v4.2 Touch", 300, 2, COL_DARK_GRAY); #if DEBUG_MODE drawCentered("DEBUG MODE", 340, 2, COL_YELLOW); #endif delay(1500); connectWiFi(); -if (WiFi.isConnected()) checkNetwork(); + + if (WiFi.isConnected()) { + checkNetwork(); + } timeClient.begin(); if (timeClient.update()) { @@ -727,7 +728,7 @@ if (WiFi.isConnected()) checkNetwork(); Serial.printf("[NTP] Synced: %ld\n", lastKnownEpoch); } - queueStatus("BOOTED", "v4.1 Touch Edition"); + queueStatus("BOOTED", "v4.2 Touch Edition"); currentState = STATE_SILENT; setBacklight(false); @@ -744,11 +745,14 @@ void loop() { String cmd = Serial.readStringUntil('\n'); cmd.trim(); if (cmd == "CLEAR_DEDUP") { - lastAlertId = ""; + lastAlertId = ""; lastSilenceId = ""; - lastAdminId = ""; + lastAdminId = ""; Serial.println("[CMD] Dedup cleared (all topics)"); } + else if (cmd == "NET") { + checkNetwork(); + } } if (timeClient.update()) { @@ -770,25 +774,21 @@ void loop() { } } -// Poll ntfy.sh -if (now - lastPoll >= POLL_INTERVAL_MS) { - lastPoll = now; - if (WiFi.isConnected() && ntpSynced) { - if (!networkOK) checkNetwork(); // diagnose on first poll - pollTopic(ALERT_URL, handleAlertMessage, "ALERT", lastAlertId); - pollTopic(SILENCE_URL, handleSilenceMessage, "SILENCE", lastSilenceId); - pollTopic(ADMIN_URL, handleAdminMessage, "ADMIN", lastAdminId); + if (now - lastPoll >= POLL_INTERVAL_MS) { + lastPoll = now; + if (WiFi.isConnected() && ntpSynced) { + pollTopic(ALERT_URL, handleAlertMessage, "ALERT", lastAlertId); + pollTopic(SILENCE_URL, handleSilenceMessage, "SILENCE", lastSilenceId); + pollTopic(ADMIN_URL, handleAdminMessage, "ADMIN", lastAdminId); + } } -} -// Send any queued status AFTER polling connections are closed -flushStatus(); + flushStatus(); switch (currentState) { case STATE_ALERTING: if (now - alertStart > ALERT_TIMEOUT_MS) { handleSilenceMessage("timeout"); - queueStatus("SILENT", "auto-timeout"); break; } if (now - lastBlinkToggle >= BLINK_INTERVAL_MS) { @@ -812,13 +812,15 @@ flushStatus(); static unsigned long lastHB = 0; if (now - lastHB >= 30000) { lastHB = now; - Serial.printf("[%lus] %s | WiFi:%s | heap:%dKB\n", + Serial.printf("[%lus] %s | WiFi:%s | heap:%dKB | net:%s\n", now / 1000, currentState == STATE_SILENT ? "SILENT" : currentState == STATE_ALERTING ? "ALERT" : "WAKE", WiFi.isConnected() ? "OK" : "DOWN", - ESP.getFreeHeap() / 1024); + ESP.getFreeHeap() / 1024, + networkOK ? "OK" : "FAIL"); } delay(20); } +