From 502ffc04608932f3a37294fc03cb595a0bda57d7 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Thu, 12 Feb 2026 04:45:00 -0800 Subject: [PATCH] snapshot --- sketches/doorbell/doorbell.html | 614 +++++++++++++++++++++++++++++--- sketches/doorbell/doorbell.ino | 82 +++-- 2 files changed, 617 insertions(+), 79 deletions(-) diff --git a/sketches/doorbell/doorbell.html b/sketches/doorbell/doorbell.html index ac3c205..0d45330 100644 --- a/sketches/doorbell/doorbell.html +++ b/sketches/doorbell/doorbell.html @@ -32,16 +32,33 @@ text-align: center; } - /* Status Indicator */ + #connection { + text-align: center; + font-size: 9px; + margin-bottom: 5px; + letter-spacing: 1px; + font-family: monospace; + } + + #connection.connected { color: #64e8ba; } + #connection.connecting { color: #f890e7; } + #connection.error { color: #f890e7; } + #connection.backoff { color: #888; } + #status { text-align: center; padding: 15px; - margin-bottom: 30px; + margin-bottom: 15px; border-radius: 8px; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; transition: all 0.3s ease; + min-height: 50px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; } #status.silent { @@ -57,12 +74,55 @@ animation: pulse 1s ease-in-out infinite; } + #status.sending { + background: #f890e7; + color: #000; + font-weight: 600; + } + + #status.confirming { + background: #0bd3d3; + color: #000; + font-weight: 600; + } + + #status.offline { + background: #1a1a1a; + color: #f890e7; + border: 1px solid #f890e7; + } + @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } - /* Button Grid */ + .status-detail { + font-size: 10px; + font-weight: 400; + opacity: 0.8; + margin-top: 4px; + letter-spacing: 1px; + } + + #metrics { + font-size: 9px; + color: #444; + text-align: center; + margin-bottom: 15px; + letter-spacing: 1px; + font-family: monospace; + } + + #backoffInfo { + font-size: 9px; + color: #555; + text-align: center; + margin-bottom: 10px; + letter-spacing: 1px; + font-family: monospace; + } + .buttons { display: grid; gap: 15px; @@ -80,11 +140,14 @@ cursor: pointer; transition: transform 0.1s ease, opacity 0.2s ease; color: #fff8fa; - position: relative; - overflow: hidden; } - button:active { + button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + button:not(:disabled):active { transform: scale(0.96); opacity: 0.8; } @@ -139,11 +202,22 @@ cursor: pointer; } - #sendCustom:active { + #sendCustom:not(:disabled):active { background: #0bd3d3; } - /* Feedback toast */ + #silence { + background: #1a1a1a; + border: 2px solid #64e8ba; + color: #64e8ba; + margin-top: 10px; + } + + #silence:not(:disabled):active { + background: #64e8ba; + color: #000; + } + #toast { position: fixed; bottom: 30px; @@ -166,89 +240,411 @@ transform: translateX(-50%) translateY(0); opacity: 1; } + + #toast.error { + background: #f890e7; + }

KLUBHAUS ALERT

+
WebSocket: connecting...
+
-
Checking...
+
+ Initializing... +
+
- - - + +
+ +
Sent
diff --git a/sketches/doorbell/doorbell.ino b/sketches/doorbell/doorbell.ino index 8e8bf5a..f7e1981 100644 --- a/sketches/doorbell/doorbell.ino +++ b/sketches/doorbell/doorbell.ino @@ -7,18 +7,16 @@ const char* ssid = "iot-2GHz"; const char* password = "lesson-greater"; -// TOPIC URLS — naming indicates direction and method -// Device SUBSCRIBES (GET poll) to these: -const char* COMMAND_POLL_URL = "http://ntfy.sh/ALERT_klubhaus_topic/json?poll=1"; -const char* SILENCE_POLL_URL = "http://ntfy.sh/SILENCE_klubhaus_topic/json?poll=1"; - -// Device PUBLISHES (POST) to this: +// TOPIC URLS — short poll (no ?poll=1) for ESP32 HTTP client compatibility +const char* COMMAND_POLL_URL = "http://ntfy.sh/ALERT_klubhaus_topic/json"; +const char* SILENCE_POLL_URL = "http://ntfy.sh/SILENCE_klubhaus_topic/json"; const char* STATUS_POST_URL = "http://ntfy.sh/STATUS_klubhaus_topic"; -const unsigned long COMMAND_POLL_INTERVAL = 30000; -const unsigned long SILENCE_POLL_INTERVAL = 5000; +const unsigned long COMMAND_POLL_INTERVAL = 30000; // 30s normal +const unsigned long SILENCE_POLL_INTERVAL = 5000; // 5s when alerting const unsigned long BLINK_DURATION = 180000; const unsigned long BLINK_PERIOD = 1000; +const unsigned long ERROR_BACKOFF = 60000; // Back off on 429/500 #define BACKLIGHT_PIN 22 #define BUTTON_PIN 9 @@ -50,6 +48,7 @@ State lastPublishedState = STATE_SILENT; unsigned long lastCommandPoll = 0; unsigned long lastSilencePoll = 0; +unsigned long nextPollTime = 0; // Backoff timer unsigned long blinkStartTime = 0; bool blinkPhase = false; @@ -61,8 +60,8 @@ String alertMessage = ""; void setup() { Serial.begin(115200); delay(3000); - Serial.println("\n=== KLUBHAUS DOORBELL v3.3 ==="); - Serial.println("URLs: COMMAND_POLL_URL, SILENCE_POLL_URL, STATUS_POST_URL"); + Serial.println("\n=== KLUBHAUS DOORBELL v3.4 ==="); + Serial.println("Fix: Short polling, 204 handling, rate limit backoff"); pinMode(BACKLIGHT_PIN, OUTPUT); digitalWrite(BACKLIGHT_PIN, LOW); @@ -96,6 +95,13 @@ void setup() { void loop() { unsigned long now = millis(); + + // Respect backoff after errors + if (now < nextPollTime) { + delay(100); + return; + } + handleButton(); switch (currentState) { @@ -123,7 +129,7 @@ void loop() { break; } - delay(10); + delay(50); // Small delay to prevent tight loops } // ============== STATUS PUBLISHING ============== @@ -188,10 +194,7 @@ void transitionTo(State newState) { void checkCommandTopic() { Serial.println("Poll COMMAND..."); String response = fetchNtfyJson(COMMAND_POLL_URL); - if (response.length() == 0) { - Serial.println(" No new command"); - return; - } + if (response.length() == 0) return; StaticJsonDocument<512> doc; DeserializationError error = deserializeJson(doc, response); @@ -202,15 +205,23 @@ void checkCommandTopic() { } String id = doc["id"] | ""; - if (id == lastCommandId) { + String message = doc["message"] | ""; + + // CRITICAL: Skip empty messages + if (message.length() == 0 || message.trim().length() == 0) { + Serial.println(" Empty message, skip"); + return; + } + + if (id == lastCommandId && id.length() > 0) { Serial.println(" Same ID, skip"); return; } lastCommandId = id; - String message = doc["message"] | ""; - Serial.print(" NEW: "); - Serial.println(message); + Serial.print(" NEW: ["); + Serial.print(message); + Serial.println("]"); if (message.equalsIgnoreCase("SILENCE")) { Serial.println(" -> SILENCE command"); @@ -232,12 +243,17 @@ void checkSilenceTopic() { if (error) return; String id = doc["id"] | ""; - if (id == lastSilenceId) return; + String message = doc["message"] | ""; + + // Skip empty messages + if (message.length() == 0 || message.trim().length() == 0) return; + + if (id == lastSilenceId && id.length() > 0) return; lastSilenceId = id; - String message = doc["message"] | ""; - Serial.print("Silence topic: "); - Serial.println(message); + Serial.print("Silence topic: ["); + Serial.print(message); + Serial.println("]"); if (currentState == STATE_ALERT) { Serial.println(" -> Force silence"); @@ -249,9 +265,26 @@ void checkSilenceTopic() { String fetchNtfyJson(const char* url) { HTTPClient http; http.begin(url); - http.setTimeout(5000); + http.setTimeout(8000); // 8s timeout for short poll int code = http.GET(); + + // 204 No Content = no new messages (normal, not an error) + if (code == 204) { + http.end(); + return ""; + } + + // Rate limited or server error — back off + if (code == 429 || code == 500 || code == 502 || code == 503) { + Serial.print(" HTTP error "); + Serial.print(code); + Serial.println(" — backing off 60s"); + nextPollTime = millis() + ERROR_BACKOFF; + http.end(); + return ""; + } + if (code != 200) { Serial.print(" HTTP error: "); Serial.println(code); @@ -262,6 +295,7 @@ String fetchNtfyJson(const char* url) { String payload = http.getString(); http.end(); + // Take first line if multiple JSON objects int newline = payload.indexOf('\n'); if (newline > 0) payload = payload.substring(0, newline);