This commit is contained in:
2026-02-13 21:42:32 -08:00
parent fa6bf6fd6a
commit dc62265fbb

View File

@@ -1,11 +1,16 @@
/* /*
* KLUBHAUS ALERT v4.1 — Touch Edition * KLUBHAUS ALERT v4.2 — Touch Edition
* *
* Target: Waveshare ESP32-S3-Touch-LCD-4.3 (non-B) * Target: Waveshare ESP32-S3-Touch-LCD-4.3 (non-B)
* *
* v4.1 fixes: * v4.2 fixes:
* - OPI PSRAM mode (double bandwidth — fixes WiFi+RGB coexistence)
* - Bounce buffer for RGB DMA
* - Fixed variadic lambda crash in drawStatusScreen * - Fixed variadic lambda crash in drawStatusScreen
* - Single tap for all interactions * - Single tap for all interactions
* - Deferred status publishing (no nested HTTPS)
* - Per-topic message dedup
* - since=10s (no stale message replay)
* - DEBUG_MODE with _test topic suffix * - DEBUG_MODE with _test topic suffix
*/ */
@@ -13,26 +18,26 @@
#include <Wire.h> #include <Wire.h>
#include <WiFi.h> #include <WiFi.h>
#include <WiFiMulti.h> #include <WiFiMulti.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <NTPClient.h> #include <NTPClient.h>
#include <WiFiUdp.h> #include <WiFiUdp.h>
#include <Arduino_GFX_Library.h> #include <Arduino_GFX_Library.h>
#include <WiFiClientSecure.h>
// ===================================================================== // =====================================================================
// DEBUG MODE — set to true to use _test topic suffix // DEBUG MODE — set to 1 to use _test topic suffix
// ===================================================================== // =====================================================================
#define DEBUG_MODE 0 #define DEBUG_MODE 1
// ===================================================================== // =====================================================================
// WiFi // WiFi
// ===================================================================== // =====================================================================
struct WiFiCred { const char *ssid; const char *pass; }; struct WiFiCred { const char *ssid; const char *pass; };
WiFiCred wifiNetworks[] = { WiFiCred wifiNetworks[] = {
{ "Dobro Veče", "goodnight" }, { "Dobro Veče", "goodnight" },
{ "berylpunk", "dhgwilliam" }, { "berylpunk", "dhgwilliam" },
// { "iot-2GHz", "lesson-greater" }, // { "iot-2GHz", "lesson-greater" }, // blocks outbound TCP
}; };
const int NUM_WIFI = sizeof(wifiNetworks) / sizeof(wifiNetworks[0]); const int NUM_WIFI = sizeof(wifiNetworks) / sizeof(wifiNetworks[0]);
WiFiMulti wifiMulti; WiFiMulti wifiMulti;
@@ -56,7 +61,7 @@ WiFiMulti wifiMulti;
// ===================================================================== // =====================================================================
// Timing // Timing
// ===================================================================== // =====================================================================
#define POLL_INTERVAL_MS 5000 #define POLL_INTERVAL_MS 15000
#define BLINK_INTERVAL_MS 500 #define BLINK_INTERVAL_MS 500
#define STALE_MSG_THRESHOLD_S 600 #define STALE_MSG_THRESHOLD_S 600
#define NTP_SYNC_INTERVAL_MS 3600000 #define NTP_SYNC_INTERVAL_MS 3600000
@@ -178,7 +183,7 @@ bool gt911_read(int16_t *x, int16_t *y) {
} }
// ===================================================================== // =====================================================================
// RGB Display — correct pins for 4.3 non-B // RGB Display — 4.3 non-B with bounce buffer for WiFi coexistence
// ===================================================================== // =====================================================================
Arduino_ESP32RGBPanel *rgbpanel = new Arduino_ESP32RGBPanel( Arduino_ESP32RGBPanel *rgbpanel = new Arduino_ESP32RGBPanel(
5, 3, 46, 7, 5, 3, 46, 7,
@@ -187,7 +192,11 @@ Arduino_ESP32RGBPanel *rgbpanel = new Arduino_ESP32RGBPanel(
14, 38, 18, 17, 10, 14, 38, 18, 17, 10,
0, 8, 4, 8, 0, 8, 4, 8,
0, 8, 4, 8, 0, 8, 4, 8,
1, 16000000 1, 16000000, // ← back to 16MHz
false, // useBigEndian
0, // de_idle_high
0, // pclk_idle_high
8000 // bounce_buffer_size_px
); );
Arduino_RGB_Display *gfx = new Arduino_RGB_Display( Arduino_RGB_Display *gfx = new Arduino_RGB_Display(
@@ -206,27 +215,27 @@ NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, NTP_SYNC_INTERVAL_MS);
enum DeviceState { STATE_SILENT, STATE_ALERTING, STATE_WAKE }; enum DeviceState { STATE_SILENT, STATE_ALERTING, STATE_WAKE };
DeviceState currentState = STATE_SILENT; DeviceState currentState = STATE_SILENT;
String currentMessage = ""; String currentMessage = "";
String lastAlertId = ""; String lastAlertId = "";
String lastSilenceId = ""; String lastSilenceId = "";
String lastAdminId = ""; String lastAdminId = "";
time_t lastKnownEpoch = 0; time_t lastKnownEpoch = 0;
bool ntpSynced = false; bool ntpSynced = false;
unsigned long bootTime = 0; unsigned long bootTime = 0;
bool inBootGrace = true; bool inBootGrace = true;
unsigned long lastPoll = 0; unsigned long lastPoll = 0;
unsigned long lastBlinkToggle = 0; unsigned long lastBlinkToggle = 0;
bool blinkState = false; bool blinkState = false;
unsigned long alertStart = 0; unsigned long alertStart = 0;
unsigned long wakeStart = 0; unsigned long wakeStart = 0;
// Deferred status publishing — avoids nested HTTPS connections bool pendingStatus = false;
bool pendingStatus = false; String pendingStatusState = "";
String pendingStatusState = ""; String pendingStatusMsg = "";
String pendingStatusMsg = "";
bool networkOK = false;
// ===================================================================== // =====================================================================
// Drawing Helpers // Drawing Helpers
@@ -239,7 +248,6 @@ void drawCentered(const char *txt, int y, int sz, uint16_t col) {
gfx->print(txt); gfx->print(txt);
} }
// Safe line drawing — replaces the variadic lambda that caused crashes
void drawInfoLine(int x, int y, uint16_t col, const char *text) { void drawInfoLine(int x, int y, uint16_t col, const char *text) {
gfx->setTextSize(2); gfx->setTextSize(2);
gfx->setTextColor(col); gfx->setTextColor(col);
@@ -309,8 +317,9 @@ void drawStatusScreen() {
drawInfoLine(x, y, COL_WHITE, buf); drawInfoLine(x, y, COL_WHITE, buf);
y += spacing; y += spacing;
snprintf(buf, sizeof(buf), "Up: %lu min Heap: %d KB", snprintf(buf, sizeof(buf), "Up: %lu min Heap: %d KB PSRAM: %d KB",
(millis() - bootTime) / 60000, ESP.getFreeHeap() / 1024); (millis() - bootTime) / 60000, ESP.getFreeHeap() / 1024,
ESP.getFreePsram() / 1024);
drawInfoLine(x, y, COL_WHITE, buf); drawInfoLine(x, y, COL_WHITE, buf);
y += spacing; y += spacing;
@@ -319,9 +328,10 @@ void drawStatusScreen() {
drawInfoLine(x, y, COL_WHITE, buf); drawInfoLine(x, y, COL_WHITE, buf);
y += spacing; y += spacing;
snprintf(buf, sizeof(buf), "State: %s", snprintf(buf, sizeof(buf), "State: %s Net: %s",
currentState == STATE_SILENT ? "SILENT" : currentState == STATE_SILENT ? "SILENT" :
currentState == STATE_ALERTING ? "ALERTING" : "WAKE"); currentState == STATE_ALERTING ? "ALERTING" : "WAKE",
networkOK ? "OK" : "FAIL");
uint16_t stCol = currentState == STATE_ALERTING ? COL_RED : uint16_t stCol = currentState == STATE_ALERTING ? COL_RED :
currentState == STATE_SILENT ? COL_GREEN : COL_NEON_TEAL; currentState == STATE_SILENT ? COL_GREEN : COL_NEON_TEAL;
drawInfoLine(x, y, stCol, buf); drawInfoLine(x, y, stCol, buf);
@@ -377,6 +387,7 @@ void flushStatus() {
pendingStatusState = ""; pendingStatusState = "";
pendingStatusMsg = ""; pendingStatusMsg = "";
} }
// ===================================================================== // =====================================================================
// Message Parsing — per-topic dedup // Message Parsing — per-topic dedup
// ===================================================================== // =====================================================================
@@ -412,13 +423,11 @@ void parseMessages(String &response, const char *topicName,
String msgStr = String(message); String msgStr = String(message);
String idStr = msgId ? String(msgId) : ""; String idStr = msgId ? String(msgId) : "";
// Per-topic dedup
if (idStr.length() > 0 && idStr == lastId) { if (idStr.length() > 0 && idStr == lastId) {
lineStart = lineEnd + 1; lineStart = lineEnd + 1;
continue; continue;
} }
// Age check
if (msgTime > 0) { if (msgTime > 0) {
time_t age = lastKnownEpoch - msgTime; time_t age = lastKnownEpoch - msgTime;
if (age > (time_t)STALE_MSG_THRESHOLD_S) { if (age > (time_t)STALE_MSG_THRESHOLD_S) {
@@ -440,20 +449,17 @@ void parseMessages(String &response, const char *topicName,
} }
// ===================================================================== // =====================================================================
// ntfy Polling — with diagnostics and proper HTTPS // Network Diagnostics
// ===================================================================== // =====================================================================
#include <WiFiClientSecure.h>
bool networkOK = false;
void checkNetwork() { void checkNetwork() {
Serial.printf("[NET] WiFi: %s RSSI: %d IP: %s\n", Serial.printf("[NET] WiFi: %s RSSI: %d IP: %s\n",
WiFi.SSID().c_str(), WiFi.RSSI(), WiFi.localIP().toString().c_str()); WiFi.SSID().c_str(), WiFi.RSSI(), WiFi.localIP().toString().c_str());
Serial.printf("[NET] GW: %s DNS: %s\n", Serial.printf("[NET] GW: %s DNS: %s\n",
WiFi.gatewayIP().toString().c_str(), WiFi.dnsIP().toString().c_str()); WiFi.gatewayIP().toString().c_str(), WiFi.dnsIP().toString().c_str());
Serial.printf("[NET] Heap: %d KB PSRAM free: %d KB\n",
ESP.getFreeHeap() / 1024, ESP.getFreePsram() / 1024);
Serial.flush(); Serial.flush();
// Test DNS
IPAddress ip; IPAddress ip;
if (!WiFi.hostByName("ntfy.sh", ip)) { if (!WiFi.hostByName("ntfy.sh", ip)) {
Serial.println("[NET] *** DNS FAILED ***"); Serial.println("[NET] *** DNS FAILED ***");
@@ -462,35 +468,24 @@ void checkNetwork() {
} }
Serial.printf("[NET] ntfy.sh -> %s\n", ip.toString().c_str()); Serial.printf("[NET] ntfy.sh -> %s\n", ip.toString().c_str());
// Test TCP connectivity on port 80 WiFiClientSecure tls;
WiFiClient testClient; tls.setInsecure();
Serial.println("[NET] Testing TCP to ntfy.sh:80..."); Serial.println("[NET] Testing TLS to ntfy.sh:443...");
Serial.flush(); Serial.flush();
if (testClient.connect("ntfy.sh", 80, 5000)) { if (tls.connect("ntfy.sh", 443, 15000)) {
Serial.println("[NET] TCP port 80 OK!"); Serial.println("[NET] TLS OK!");
testClient.stop(); tls.stop();
networkOK = true; networkOK = true;
} else { } else {
Serial.println("[NET] *** TCP port 80 BLOCKED ***"); Serial.println("[NET] *** TLS FAILED ***");
networkOK = false;
} }
// Test TCP connectivity on port 443
WiFiClient testClient2;
Serial.println("[NET] Testing TCP to ntfy.sh:443...");
Serial.flush();
if (testClient2.connect("ntfy.sh", 443, 5000)) {
Serial.println("[NET] TCP port 443 OK!");
testClient2.stop();
networkOK = true;
} else {
Serial.println("[NET] *** TCP port 443 BLOCKED ***");
}
Serial.flush(); Serial.flush();
} }
#include <WiFiClientSecure.h> // =====================================================================
// ntfy Polling
// =====================================================================
void pollTopic(const char *url, void pollTopic(const char *url,
void (*handler)(const String&), void (*handler)(const String&),
const char *topicName, const char *topicName,
@@ -519,6 +514,7 @@ void pollTopic(const char *url,
} }
http.end(); http.end();
} }
// ===================================================================== // =====================================================================
// Message Handlers // Message Handlers
// ===================================================================== // =====================================================================
@@ -560,11 +556,12 @@ void handleAdminMessage(const String &message) {
else if (message == "status") { else if (message == "status") {
char buf[256]; char buf[256];
snprintf(buf, sizeof(buf), snprintf(buf, sizeof(buf),
"State:%s WiFi:%s RSSI:%d Heap:%dKB Up:%lus", "State:%s WiFi:%s RSSI:%d Heap:%dKB Up:%lus Net:%s",
currentState == STATE_SILENT ? "SILENT" : currentState == STATE_SILENT ? "SILENT" :
currentState == STATE_ALERTING ? "ALERT" : "WAKE", currentState == STATE_ALERTING ? "ALERT" : "WAKE",
WiFi.SSID().c_str(), WiFi.RSSI(), WiFi.SSID().c_str(), WiFi.RSSI(),
ESP.getFreeHeap() / 1024, (millis() - bootTime) / 1000); ESP.getFreeHeap() / 1024, (millis() - bootTime) / 1000,
networkOK ? "OK" : "FAIL");
queueStatus("STATUS", buf); queueStatus("STATUS", buf);
} }
else if (message == "wake") { else if (message == "wake") {
@@ -575,23 +572,23 @@ void handleAdminMessage(const String &message) {
} }
else if (message == "REBOOT") { else if (message == "REBOOT") {
queueStatus("REBOOTING", "admin"); queueStatus("REBOOTING", "admin");
flushStatus();
delay(200); delay(200);
ESP.restart(); ESP.restart();
} }
} }
// ===================================================================== // =====================================================================
// Touch — trigger on finger DOWN only, not while held // Touch — trigger on finger DOWN only
// ===================================================================== // =====================================================================
void handleTouch() { void handleTouch() {
int16_t tx, ty; int16_t tx, ty;
bool touching = gt911_read(&tx, &ty); bool touching = gt911_read(&tx, &ty);
static bool wasTouching = false; static bool wasTouching = false;
static unsigned long lastAction = 0; static unsigned long lastAction = 0;
unsigned long now = millis(); unsigned long now = millis();
// Only act on the transition: NOT touching → touching (new finger down)
if (touching && !wasTouching) { if (touching && !wasTouching) {
if (now - lastAction >= TOUCH_DEBOUNCE_MS) { if (now - lastAction >= TOUCH_DEBOUNCE_MS) {
lastAction = now; lastAction = now;
@@ -692,16 +689,17 @@ void setup() {
bootTime = millis(); bootTime = millis();
Serial.println("\n========================================"); Serial.println("\n========================================");
Serial.println(" KLUBHAUS ALERT v4.1 — Touch Edition"); Serial.println(" KLUBHAUS ALERT v4.2 — Touch Edition");
#if DEBUG_MODE #if DEBUG_MODE
Serial.println(" *** DEBUG MODE — _test topics ***"); Serial.println(" *** DEBUG MODE — _test topics ***");
#endif #endif
Serial.printf(" Grace period: %d ms\n", BOOT_GRACE_MS); Serial.printf(" Grace period: %d ms\n", BOOT_GRACE_MS);
Serial.println("========================================"); Serial.println("========================================");
Serial.printf("PSRAM: %d MB\n", ESP.getPsramSize() / (1024 * 1024)); Serial.printf("Heap: %d KB PSRAM: %d KB\n",
ESP.getFreeHeap() / 1024, ESP.getPsramSize() / 1024);
if (ESP.getPsramSize() == 0) { if (ESP.getPsramSize() == 0) {
Serial.println("PSRAM required!"); Serial.println("PSRAM required! Check FQBN has PSRAM=opi");
while (true) delay(1000); while (true) delay(1000);
} }
@@ -711,14 +709,17 @@ void setup() {
gfx->fillScreen(COL_BLACK); gfx->fillScreen(COL_BLACK);
drawCentered("KLUBHAUS", 120, 6, COL_NEON_TEAL); drawCentered("KLUBHAUS", 120, 6, COL_NEON_TEAL);
drawCentered("ALERT", 200, 6, COL_HOT_FUCHSIA); drawCentered("ALERT", 200, 6, COL_HOT_FUCHSIA);
drawCentered("v4.1 Touch", 300, 2, COL_DARK_GRAY); drawCentered("v4.2 Touch", 300, 2, COL_DARK_GRAY);
#if DEBUG_MODE #if DEBUG_MODE
drawCentered("DEBUG MODE", 340, 2, COL_YELLOW); drawCentered("DEBUG MODE", 340, 2, COL_YELLOW);
#endif #endif
delay(1500); delay(1500);
connectWiFi(); connectWiFi();
if (WiFi.isConnected()) checkNetwork();
if (WiFi.isConnected()) {
checkNetwork();
}
timeClient.begin(); timeClient.begin();
if (timeClient.update()) { if (timeClient.update()) {
@@ -727,7 +728,7 @@ if (WiFi.isConnected()) checkNetwork();
Serial.printf("[NTP] Synced: %ld\n", lastKnownEpoch); Serial.printf("[NTP] Synced: %ld\n", lastKnownEpoch);
} }
queueStatus("BOOTED", "v4.1 Touch Edition"); queueStatus("BOOTED", "v4.2 Touch Edition");
currentState = STATE_SILENT; currentState = STATE_SILENT;
setBacklight(false); setBacklight(false);
@@ -744,11 +745,14 @@ void loop() {
String cmd = Serial.readStringUntil('\n'); String cmd = Serial.readStringUntil('\n');
cmd.trim(); cmd.trim();
if (cmd == "CLEAR_DEDUP") { if (cmd == "CLEAR_DEDUP") {
lastAlertId = ""; lastAlertId = "";
lastSilenceId = ""; lastSilenceId = "";
lastAdminId = ""; lastAdminId = "";
Serial.println("[CMD] Dedup cleared (all topics)"); Serial.println("[CMD] Dedup cleared (all topics)");
} }
else if (cmd == "NET") {
checkNetwork();
}
} }
if (timeClient.update()) { if (timeClient.update()) {
@@ -770,25 +774,21 @@ void loop() {
} }
} }
// Poll ntfy.sh if (now - lastPoll >= POLL_INTERVAL_MS) {
if (now - lastPoll >= POLL_INTERVAL_MS) { lastPoll = now;
lastPoll = now; if (WiFi.isConnected() && ntpSynced) {
if (WiFi.isConnected() && ntpSynced) { pollTopic(ALERT_URL, handleAlertMessage, "ALERT", lastAlertId);
if (!networkOK) checkNetwork(); // diagnose on first poll pollTopic(SILENCE_URL, handleSilenceMessage, "SILENCE", lastSilenceId);
pollTopic(ALERT_URL, handleAlertMessage, "ALERT", lastAlertId); pollTopic(ADMIN_URL, handleAdminMessage, "ADMIN", lastAdminId);
pollTopic(SILENCE_URL, handleSilenceMessage, "SILENCE", lastSilenceId); }
pollTopic(ADMIN_URL, handleAdminMessage, "ADMIN", lastAdminId);
} }
}
// Send any queued status AFTER polling connections are closed flushStatus();
flushStatus();
switch (currentState) { switch (currentState) {
case STATE_ALERTING: case STATE_ALERTING:
if (now - alertStart > ALERT_TIMEOUT_MS) { if (now - alertStart > ALERT_TIMEOUT_MS) {
handleSilenceMessage("timeout"); handleSilenceMessage("timeout");
queueStatus("SILENT", "auto-timeout");
break; break;
} }
if (now - lastBlinkToggle >= BLINK_INTERVAL_MS) { if (now - lastBlinkToggle >= BLINK_INTERVAL_MS) {
@@ -812,13 +812,15 @@ flushStatus();
static unsigned long lastHB = 0; static unsigned long lastHB = 0;
if (now - lastHB >= 30000) { if (now - lastHB >= 30000) {
lastHB = now; lastHB = now;
Serial.printf("[%lus] %s | WiFi:%s | heap:%dKB\n", Serial.printf("[%lus] %s | WiFi:%s | heap:%dKB | net:%s\n",
now / 1000, now / 1000,
currentState == STATE_SILENT ? "SILENT" : currentState == STATE_SILENT ? "SILENT" :
currentState == STATE_ALERTING ? "ALERT" : "WAKE", currentState == STATE_ALERTING ? "ALERT" : "WAKE",
WiFi.isConnected() ? "OK" : "DOWN", WiFi.isConnected() ? "OK" : "DOWN",
ESP.getFreeHeap() / 1024); ESP.getFreeHeap() / 1024,
networkOK ? "OK" : "FAIL");
} }
delay(20); delay(20);
} }