/* * KLUBHAUS ALERT v3.9.2-STABLE * Ultra-defensive: minimal HTTP, no NTP dependency, graceful degradation */ #include #include #include #include // ============== CONFIG ============== const char* WIFI_SSID = "Dobro Veče"; const char* WIFI_PASS = "goodnight"; 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"; // ============== GLOBALS ============== TFT_eSPI tft; enum State { SILENT, ALERTING }; State state = SILENT; String alertMessage = ""; unsigned long lastPoll = 0; unsigned long lastReconnect = 0; unsigned long bootMillis = 0; bool wifiReady = false; // ============== SETUP ============== void setup() { Serial.begin(115200); delay(1000); // Let USB stabilize bootMillis = millis(); Serial.println("\n=== KLUBHAUS v3.9.2-STABLE ==="); Serial.println("Boot at millis=" + String(bootMillis)); // Init display early for feedback tft.init(); tft.setRotation(1); tft.fillScreen(0x0000); tft.setTextColor(0xFFDF, 0x0000); tft.setTextDatum(4); // MC_DATUM tft.setTextSize(2); tft.drawString("BOOTING...", 160, 86); // 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) { 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); } delay(1000); drawSilent(); Serial.println("Setup complete, entering loop"); } // ============== 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"); } } // Poll ntfy (throttled, guarded) if (wifiReady && now - lastPoll > 10000) { // 10 second min interval lastPoll = now; pollNtfy(); } delay(100); // Yield to RTOS } // ============== NTFY POLLING (DEFENSIVE) ============== void pollNtfy() { Serial.println("Polling ntfy..."); HTTPClient http; http.setTimeout(8000); // 8 second timeout http.setConnectTimeout(5000); // 5 second connect timeout // Poll ALERT first if (http.begin(ALERT_URL)) { int code = http.GET(); Serial.println("ALERT HTTP: " + String(code)); 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(); } } void processBody(const String& body, const char* type) { // Simple line-by-line parsing, no heavy JSON int pos = 0; while (pos < body.length()) { int end = body.indexOf('\n', pos); if (end == -1) 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); // Unescape simple cases msg.replace("\\n", "\n"); msg.replace("\\\"", "\""); Serial.println(String(type) + " msg: " + msg); if (strcmp(type, "ALERT") == 0 && msg.length() > 0) { alertMessage = msg; state = ALERTING; drawAlert(); } else if (strcmp(type, "SILENCE") == 0) { state = SILENT; alertMessage = ""; drawSilent(); } } } pos = end + 1; } } // ============== DISPLAY ============== void drawSilent() { tft.fillScreen(0x0000); tft.setTextColor(0xFFDF, 0x0000); tft.setTextSize(2); tft.setTextDatum(4); tft.drawString("SILENT", 160, 86); } void drawAlert() { tft.fillScreen(0x0000); // Blink color uint16_t color = (millis() / 500) % 2 ? 0x07D7 : 0xF81F; tft.setTextColor(color, 0x0000); // Auto size int size = 4; if (alertMessage.length() > 8) size = 3; if (alertMessage.length() > 16) size = 2; tft.setTextSize(size); tft.setTextDatum(4); // Simple wrap if (alertMessage.length() > 20) { int mid = alertMessage.length() / 2; int split = alertMessage.lastIndexOf(' ', mid); if (split < 0) split = mid; tft.drawString(alertMessage.substring(0, split), 160, 70); tft.drawString(alertMessage.substring(split + 1), 160, 102); } else { tft.drawString(alertMessage, 160, 86); } }