/* * 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 = "IoT-2GHz"; const char* WIFI_PASS = "lesson-greater"; 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 = SILENT; String alertMsg = ""; bool ledOnly = false; unsigned long bootMs = 0; bool gracePeriod = true; bool ntpOk = false; time_t wallClock = 0; unsigned long lastPoll = 0; String lastAlertId, lastSilenceId, lastAdminId; // ============== SETUP ============== void setup() { Serial.begin(115200); delay(2000); bootMs = millis(); Serial.println("\n[BOOT] v3.9.4 ESP32-S3-LCD-1.47"); tft.init(); tft.setRotation(1); tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextDatum(MC_DATUM); tft.setTextSize(2); tft.drawString("KLUBHAUS", 160, 76); tft.setTextSize(1); tft.drawString("v3.9.4", 160, 96); tft.drawString("WiFi...", 160, 110); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 30) { delay(500); attempts++; } tft.fillScreen(TFT_BLACK); tft.setTextSize(2); tft.drawString(WiFi.status() == WL_CONNECTED ? "WIFI OK" : "WIFI FAIL", 160, 86); delay(1000); 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(); if (timeClient.update()) { // Object method: use . ntpOk = true; wallClock = timeClient.getEpochTime(); // FIXED: was timeClient->getEpochTime() } if (gracePeriod && now - bootMs >= 30000) { gracePeriod = false; Serial.println("[BOOT] Grace ended"); } if (now - lastPoll > 5000 && !gracePeriod && ntpOk && WiFi.status() == WL_CONNECTED) { lastPoll = now; 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); } 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 ============== void poll(const char* url, const char* name, String& lastId, void (*cb)(const String&)) { HTTPClient http; http.setTimeout(8000); if (!http.begin(url)) return; int code = http.GET(); if (code == 200) { String body = http.getString(); parse(body, name, lastId, cb); } http.end(); } 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 < 0) end = body.length(); String line = body.substring(pos, end); line.trim(); 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; if (msg && strlen(msg) > 0) { String idStr = id ? String(id) : ""; String msgStr = String(msg); if (idStr == lastId) { pos = end + 1; continue; } 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; } } // ============== 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(MC_DATUM); tft.drawString("SILENT", 160, 86); } void handleAdmin(const String& m) { Serial.println(String("[ADMIN] ") + m); if (m == "SILENCE") setSilent(); else if (m == "REBOOT") ESP.restart(); } // ============== DISPLAY ============== void drawAlert(uint16_t color) { if (ledOnly) return; tft.fillScreen(TFT_BLACK); tft.setTextColor(color, TFT_BLACK); int sz = alertMsg.length() > 10 ? 3 : 4; if (alertMsg.length() > 20) sz = 2; tft.setTextSize(sz); tft.setTextDatum(MC_DATUM); 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(alertMsg, 160, 86); } }