From df0527b1234c9702c4ef3fb5bfafcf83c743fb59 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Fri, 13 Feb 2026 13:12:01 -0800 Subject: [PATCH] snapshot --- TFT_eSPI | 1 - sketches/doorbell/doorbell.ino | 260 ++++++++++++++++----------------- sketches/doorbell/sketch.yaml | 3 +- 3 files changed, 132 insertions(+), 132 deletions(-) delete mode 160000 TFT_eSPI diff --git a/TFT_eSPI b/TFT_eSPI deleted file mode 160000 index 42f64b3..0000000 --- a/TFT_eSPI +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 42f64b37e450e43868ca0e811075484e2f3bfccb diff --git a/sketches/doorbell/doorbell.ino b/sketches/doorbell/doorbell.ino index 9a26737..f9aeb76 100644 --- a/sketches/doorbell/doorbell.ino +++ b/sketches/doorbell/doorbell.ino @@ -1,214 +1,214 @@ /* - * KLUBHAUS ALERT v3.9.2-STABLE - * Ultra-defensive: minimal HTTP, no NTP dependency, graceful degradation + * KLUBHAUS v3.9.4-ESP32S3-LCD147 + * Fixed: timeClient is object, not pointer (use . not ->) */ #include #include #include #include +#include +#include // ============== CONFIG ============== -const char* WIFI_SSID = "KH-IoT"; -const char* WIFI_PASS = "goodnight"; +const char* WIFI_SSID = "IoT-2GHz"; +const char* WIFI_PASS = "lesson-greater"; -const char* ALERT_URL = "https://ntfy.sh/ALERT_klubhaus_topic/json?since=all"; -const char* SILENCE_URL = "https://ntfy.sh/SILENCE_klubhaus_topic/json?since=all"; +const char* ALERT_TOPIC = "https://ntfy.sh/ALERT_klubhaus_topic/json?since=all"; +const char* SILENCE_TOPIC = "https://ntfy.sh/SILENCE_klubhaus_topic/json?since=all"; +const char* ADMIN_TOPIC = "https://ntfy.sh/ADMIN_klubhaus_topic/json?since=all"; +const char* STATUS_TOPIC = "https://ntfy.sh/STATUS_klubhaus_topic"; // ============== GLOBALS ============== TFT_eSPI tft; +WiFiUDP ntpUDP; +NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000); // Stack object, not pointer -enum State { SILENT, ALERTING }; -State state = SILENT; -String alertMessage = ""; +enum State { SILENT, ALERTING } state = SILENT; +String alertMsg = ""; +bool ledOnly = false; +unsigned long bootMs = 0; +bool gracePeriod = true; +bool ntpOk = false; +time_t wallClock = 0; unsigned long lastPoll = 0; -unsigned long lastReconnect = 0; -unsigned long bootMillis = 0; -bool wifiReady = false; + +String lastAlertId, lastSilenceId, lastAdminId; // ============== SETUP ============== void setup() { Serial.begin(115200); - delay(1000); // Let USB stabilize + delay(2000); - bootMillis = millis(); - Serial.println("\n=== KLUBHAUS v3.9.2-STABLE ==="); - Serial.println("Boot at millis=" + String(bootMillis)); + bootMs = millis(); + Serial.println("\n[BOOT] v3.9.4 ESP32-S3-LCD-1.47"); - // Init display early for feedback tft.init(); tft.setRotation(1); - tft.fillScreen(0x0000); - tft.setTextColor(0xFFDF, 0x0000); - tft.setTextDatum(4); // MC_DATUM + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextDatum(MC_DATUM); tft.setTextSize(2); - tft.drawString("BOOTING...", 160, 86); + tft.drawString("KLUBHAUS", 160, 76); + tft.setTextSize(1); + tft.drawString("v3.9.4", 160, 96); + tft.drawString("WiFi...", 160, 110); - // Connect WiFi with timeout WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); - Serial.print("WiFi connecting"); int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 50) { + while (WiFi.status() != WL_CONNECTED && attempts < 30) { delay(500); - Serial.print("."); attempts++; - if (attempts % 10 == 0) { - Serial.println("\nWiFi status: " + String(WiFi.status())); - } - } - - if (WiFi.status() == WL_CONNECTED) { - wifiReady = true; - Serial.println("\nWiFi OK: " + WiFi.localIP().toString()); - tft.drawString("WIFI OK", 160, 110); - } else { - Serial.println("\nWiFi FAILED, continuing offline"); - tft.drawString("WIFI FAIL", 160, 110); } + tft.fillScreen(TFT_BLACK); + tft.setTextSize(2); + tft.drawString(WiFi.status() == WL_CONNECTED ? "WIFI OK" : "WIFI FAIL", 160, 86); delay(1000); - drawSilent(); - Serial.println("Setup complete, entering loop"); + + timeClient.begin(); // Object method: use . + if (timeClient.update()) { // Object method: use . + ntpOk = true; + wallClock = timeClient.getEpochTime(); // FIXED: was timeClient->getEpochTime() + } + + Serial.println("[BOOT] Grace: 30s"); } // ============== LOOP ============== void loop() { unsigned long now = millis(); - // WiFi reconnect logic (throttled) - if (!wifiReady && now - lastReconnect > 30000) { - lastReconnect = now; - Serial.println("WiFi reconnect attempt..."); - WiFi.reconnect(); - if (WiFi.status() == WL_CONNECTED) { - wifiReady = true; - Serial.println("WiFi reconnected"); - } + if (timeClient.update()) { // Object method: use . + ntpOk = true; + wallClock = timeClient.getEpochTime(); // FIXED: was timeClient->getEpochTime() } - // Poll ntfy (throttled, guarded) - if (wifiReady && now - lastPoll > 10000) { // 10 second min interval + if (gracePeriod && now - bootMs >= 30000) { + gracePeriod = false; + Serial.println("[BOOT] Grace ended"); + } + + if (now - lastPoll > 5000 && !gracePeriod && ntpOk && WiFi.status() == WL_CONNECTED) { lastPoll = now; - pollNtfy(); + poll(ALERT_TOPIC, "ALERT", lastAlertId, [](const String& m){ setAlert(m); }); + poll(SILENCE_TOPIC, "SILENCE", lastSilenceId, [](const String& m){ setSilent(); }); + poll(ADMIN_TOPIC, "ADMIN", lastAdminId, handleAdmin); } - delay(100); // Yield to RTOS + if (state == ALERTING && !ledOnly) { + static unsigned long lastBlink = 0; + static bool blinkOn = false; + if (now - lastBlink > 500) { + lastBlink = now; + blinkOn = !blinkOn; + drawAlert(blinkOn ? 0x07D7 : 0xF81F); + } + } + + delay(50); } -// ============== NTFY POLLING (DEFENSIVE) ============== -void pollNtfy() { - Serial.println("Polling ntfy..."); - +// ============== NTFY ============== +void poll(const char* url, const char* name, String& lastId, void (*cb)(const String&)) { HTTPClient http; - http.setTimeout(8000); // 8 second timeout - http.setConnectTimeout(5000); // 5 second connect timeout + http.setTimeout(8000); - // Poll ALERT first - if (http.begin(ALERT_URL)) { - int code = http.GET(); - Serial.println("ALERT HTTP: " + String(code)); + if (!http.begin(url)) return; - if (code == 200) { - String body = http.getString(); - Serial.println("ALERT body len: " + String(body.length())); - processBody(body, "ALERT"); - } - http.end(); - } - - delay(100); // Brief pause between requests - - // Poll SILENCE - if (http.begin(SILENCE_URL)) { - int code = http.GET(); - Serial.println("SILENCE HTTP: " + String(code)); - - if (code == 200) { - String body = http.getString(); - processBody(body, "SILENCE"); - } - http.end(); + int code = http.GET(); + if (code == 200) { + String body = http.getString(); + parse(body, name, lastId, cb); } + http.end(); } -void processBody(const String& body, const char* type) { - // Simple line-by-line parsing, no heavy JSON +void parse(const String& body, const char* name, String& lastId, void (*cb)(const String&)) { int pos = 0; while (pos < body.length()) { int end = body.indexOf('\n', pos); - if (end == -1) end = body.length(); + if (end < 0) end = body.length(); String line = body.substring(pos, end); line.trim(); - // Look for "message":"..." pattern - int msgStart = line.indexOf("\"message\":\""); - if (msgStart >= 0) { - msgStart += 11; // Skip "message":" - int msgEnd = line.indexOf("\"", msgStart); - if (msgEnd > msgStart) { - String msg = line.substring(msgStart, msgEnd); + if (line.length() > 10 && line.indexOf('{') >= 0) { + StaticJsonDocument<512> doc; + if (!deserializeJson(doc, line)) { + const char* id = doc["id"]; + const char* msg = doc["message"]; + time_t t = doc["time"] | 0; - // Unescape simple cases - msg.replace("\\n", "\n"); - msg.replace("\\\"", "\""); + if (msg && strlen(msg) > 0) { + String idStr = id ? String(id) : ""; + String msgStr = String(msg); - Serial.println(String(type) + " msg: " + msg); + if (idStr == lastId) { pos = end + 1; continue; } - if (strcmp(type, "ALERT") == 0 && msg.length() > 0) { - alertMessage = msg; - state = ALERTING; - drawAlert(); - } - else if (strcmp(type, "SILENCE") == 0) { - state = SILENT; - alertMessage = ""; - drawSilent(); + if (t > 0 && wallClock - t > 600) { + Serial.println(String("[STALE] ") + name); + pos = end + 1; + continue; + } + + Serial.println(String("[") + name + "] " + msgStr.substring(0, 40)); + lastId = idStr; + cb(msgStr); } } } - pos = end + 1; } } -// ============== DISPLAY ============== -void drawSilent() { - tft.fillScreen(0x0000); - tft.setTextColor(0xFFDF, 0x0000); +// ============== ACTIONS ============== +void setAlert(const String& m) { + state = ALERTING; + alertMsg = m; + drawAlert(0x07D7); +} + +void setSilent() { + state = SILENT; + alertMsg = ""; + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); - tft.setTextDatum(4); + tft.setTextDatum(MC_DATUM); tft.drawString("SILENT", 160, 86); } -void drawAlert() { - tft.fillScreen(0x0000); +void handleAdmin(const String& m) { + Serial.println(String("[ADMIN] ") + m); + if (m == "SILENCE") setSilent(); + else if (m == "REBOOT") ESP.restart(); +} - // Blink color - uint16_t color = (millis() / 500) % 2 ? 0x07D7 : 0xF81F; - tft.setTextColor(color, 0x0000); +// ============== DISPLAY ============== +void drawAlert(uint16_t color) { + if (ledOnly) return; - // Auto size - int size = 4; - if (alertMessage.length() > 8) size = 3; - if (alertMessage.length() > 16) size = 2; + tft.fillScreen(TFT_BLACK); + tft.setTextColor(color, TFT_BLACK); - tft.setTextSize(size); - tft.setTextDatum(4); + int sz = alertMsg.length() > 10 ? 3 : 4; + if (alertMsg.length() > 20) sz = 2; - // Simple wrap - if (alertMessage.length() > 20) { - int mid = alertMessage.length() / 2; - int split = alertMessage.lastIndexOf(' ', mid); - if (split < 0) split = mid; + tft.setTextSize(sz); + tft.setTextDatum(MC_DATUM); - tft.drawString(alertMessage.substring(0, split), 160, 70); - tft.drawString(alertMessage.substring(split + 1), 160, 102); + if (alertMsg.length() > 15) { + int mid = alertMsg.length() / 2; + int sp = alertMsg.lastIndexOf(' ', mid); + if (sp < 0) sp = mid; + tft.drawString(alertMsg.substring(0, sp), 160, 70); + tft.drawString(alertMsg.substring(sp + 1), 160, 102); } else { - tft.drawString(alertMessage, 160, 86); + tft.drawString(alertMsg, 160, 86); } } diff --git a/sketches/doorbell/sketch.yaml b/sketches/doorbell/sketch.yaml index 86ea2ed..702b0bc 100644 --- a/sketches/doorbell/sketch.yaml +++ b/sketches/doorbell/sketch.yaml @@ -1,3 +1,4 @@ default_port: /dev/ttyACM0 -default_fqbn: esp32:esp32:esp32c6:CDCOnBoot=cdc +# default_fqbn: esp32:esp32:esp32c6:CDCOnBoot=cdc +default_fqbn: esp32:esp32:esp32s3 default_programmer: esptool