This commit is contained in:
2026-02-13 13:12:01 -08:00
parent f9c726626c
commit df0527b123
3 changed files with 132 additions and 132 deletions

Submodule TFT_eSPI deleted from 42f64b37e4

View File

@@ -1,214 +1,214 @@
/* /*
* KLUBHAUS ALERT v3.9.2-STABLE * KLUBHAUS v3.9.4-ESP32S3-LCD147
* Ultra-defensive: minimal HTTP, no NTP dependency, graceful degradation * Fixed: timeClient is object, not pointer (use . not ->)
*/ */
#include <WiFi.h> #include <WiFi.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <TFT_eSPI.h> #include <TFT_eSPI.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
// ============== CONFIG ============== // ============== CONFIG ==============
const char* WIFI_SSID = "KH-IoT"; const char* WIFI_SSID = "IoT-2GHz";
const char* WIFI_PASS = "goodnight"; const char* WIFI_PASS = "lesson-greater";
const char* ALERT_URL = "https://ntfy.sh/ALERT_klubhaus_topic/json?since=all"; const char* ALERT_TOPIC = "https://ntfy.sh/ALERT_klubhaus_topic/json?since=all";
const char* SILENCE_URL = "https://ntfy.sh/SILENCE_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 ============== // ============== GLOBALS ==============
TFT_eSPI tft; TFT_eSPI tft;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000); // Stack object, not pointer
enum State { SILENT, ALERTING }; enum State { SILENT, ALERTING } state = SILENT;
State state = SILENT; String alertMsg = "";
String alertMessage = ""; bool ledOnly = false;
unsigned long bootMs = 0;
bool gracePeriod = true;
bool ntpOk = false;
time_t wallClock = 0;
unsigned long lastPoll = 0; unsigned long lastPoll = 0;
unsigned long lastReconnect = 0;
unsigned long bootMillis = 0; String lastAlertId, lastSilenceId, lastAdminId;
bool wifiReady = false;
// ============== SETUP ============== // ============== SETUP ==============
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
delay(1000); // Let USB stabilize delay(2000);
bootMillis = millis(); bootMs = millis();
Serial.println("\n=== KLUBHAUS v3.9.2-STABLE ==="); Serial.println("\n[BOOT] v3.9.4 ESP32-S3-LCD-1.47");
Serial.println("Boot at millis=" + String(bootMillis));
// Init display early for feedback
tft.init(); tft.init();
tft.setRotation(1); tft.setRotation(1);
tft.fillScreen(0x0000); tft.fillScreen(TFT_BLACK);
tft.setTextColor(0xFFDF, 0x0000); tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextDatum(4); // MC_DATUM tft.setTextDatum(MC_DATUM);
tft.setTextSize(2); 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.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS); WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("WiFi connecting");
int attempts = 0; int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 50) { while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500); delay(500);
Serial.print(".");
attempts++; 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); 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 ============== // ============== LOOP ==============
void loop() { void loop() {
unsigned long now = millis(); unsigned long now = millis();
// WiFi reconnect logic (throttled) if (timeClient.update()) { // Object method: use .
if (!wifiReady && now - lastReconnect > 30000) { ntpOk = true;
lastReconnect = now; wallClock = timeClient.getEpochTime(); // FIXED: was timeClient->getEpochTime()
Serial.println("WiFi reconnect attempt...");
WiFi.reconnect();
if (WiFi.status() == WL_CONNECTED) {
wifiReady = true;
Serial.println("WiFi reconnected");
}
} }
// Poll ntfy (throttled, guarded) if (gracePeriod && now - bootMs >= 30000) {
if (wifiReady && now - lastPoll > 10000) { // 10 second min interval gracePeriod = false;
Serial.println("[BOOT] Grace ended");
}
if (now - lastPoll > 5000 && !gracePeriod && ntpOk && WiFi.status() == WL_CONNECTED) {
lastPoll = now; 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) ============== // ============== NTFY ==============
void pollNtfy() { void poll(const char* url, const char* name, String& lastId, void (*cb)(const String&)) {
Serial.println("Polling ntfy...");
HTTPClient http; HTTPClient http;
http.setTimeout(8000); // 8 second timeout http.setTimeout(8000);
http.setConnectTimeout(5000); // 5 second connect timeout
if (!http.begin(url)) return;
// Poll ALERT first
if (http.begin(ALERT_URL)) {
int code = http.GET(); int code = http.GET();
Serial.println("ALERT HTTP: " + String(code));
if (code == 200) { if (code == 200) {
String body = http.getString(); String body = http.getString();
Serial.println("ALERT body len: " + String(body.length())); parse(body, name, lastId, cb);
processBody(body, "ALERT");
} }
http.end(); 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) { void parse(const String& body, const char* name, String& lastId, void (*cb)(const String&)) {
// Simple line-by-line parsing, no heavy JSON
int pos = 0; int pos = 0;
while (pos < body.length()) { while (pos < body.length()) {
int end = body.indexOf('\n', pos); int end = body.indexOf('\n', pos);
if (end == -1) end = body.length(); if (end < 0) end = body.length();
String line = body.substring(pos, end); String line = body.substring(pos, end);
line.trim(); line.trim();
// Look for "message":"..." pattern if (line.length() > 10 && line.indexOf('{') >= 0) {
int msgStart = line.indexOf("\"message\":\""); StaticJsonDocument<512> doc;
if (msgStart >= 0) { if (!deserializeJson(doc, line)) {
msgStart += 11; // Skip "message":" const char* id = doc["id"];
int msgEnd = line.indexOf("\"", msgStart); const char* msg = doc["message"];
if (msgEnd > msgStart) { time_t t = doc["time"] | 0;
String msg = line.substring(msgStart, msgEnd);
// Unescape simple cases if (msg && strlen(msg) > 0) {
msg.replace("\\n", "\n"); String idStr = id ? String(id) : "";
msg.replace("\\\"", "\""); 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) { if (t > 0 && wallClock - t > 600) {
alertMessage = msg; Serial.println(String("[STALE] ") + name);
state = ALERTING; pos = end + 1;
drawAlert(); continue;
} }
else if (strcmp(type, "SILENCE") == 0) {
state = SILENT; Serial.println(String("[") + name + "] " + msgStr.substring(0, 40));
alertMessage = ""; lastId = idStr;
drawSilent(); cb(msgStr);
} }
} }
} }
pos = end + 1; pos = end + 1;
} }
} }
// ============== DISPLAY ============== // ============== ACTIONS ==============
void drawSilent() { void setAlert(const String& m) {
tft.fillScreen(0x0000); state = ALERTING;
tft.setTextColor(0xFFDF, 0x0000); alertMsg = m;
drawAlert(0x07D7);
}
void setSilent() {
state = SILENT;
alertMsg = "";
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextSize(2); tft.setTextSize(2);
tft.setTextDatum(4); tft.setTextDatum(MC_DATUM);
tft.drawString("SILENT", 160, 86); tft.drawString("SILENT", 160, 86);
} }
void drawAlert() { void handleAdmin(const String& m) {
tft.fillScreen(0x0000); Serial.println(String("[ADMIN] ") + m);
if (m == "SILENCE") setSilent();
else if (m == "REBOOT") ESP.restart();
}
// Blink color // ============== DISPLAY ==============
uint16_t color = (millis() / 500) % 2 ? 0x07D7 : 0xF81F; void drawAlert(uint16_t color) {
tft.setTextColor(color, 0x0000); if (ledOnly) return;
// Auto size tft.fillScreen(TFT_BLACK);
int size = 4; tft.setTextColor(color, TFT_BLACK);
if (alertMessage.length() > 8) size = 3;
if (alertMessage.length() > 16) size = 2;
tft.setTextSize(size); int sz = alertMsg.length() > 10 ? 3 : 4;
tft.setTextDatum(4); if (alertMsg.length() > 20) sz = 2;
// Simple wrap tft.setTextSize(sz);
if (alertMessage.length() > 20) { tft.setTextDatum(MC_DATUM);
int mid = alertMessage.length() / 2;
int split = alertMessage.lastIndexOf(' ', mid);
if (split < 0) split = mid;
tft.drawString(alertMessage.substring(0, split), 160, 70); if (alertMsg.length() > 15) {
tft.drawString(alertMessage.substring(split + 1), 160, 102); 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 { } else {
tft.drawString(alertMessage, 160, 86); tft.drawString(alertMsg, 160, 86);
} }
} }

View File

@@ -1,3 +1,4 @@
default_port: /dev/ttyACM0 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 default_programmer: esptool