snapshot
This commit is contained in:
@@ -1,420 +1,214 @@
|
|||||||
/*
|
/*
|
||||||
* KLUBHAUS ALERT DEVICE v3.9.1-minimal
|
* KLUBHAUS ALERT v3.9.2-STABLE
|
||||||
*
|
* Ultra-defensive: minimal HTTP, no NTP dependency, graceful degradation
|
||||||
* Minimal fix for boot loop: message age filtering + ID deduplication
|
|
||||||
* Preserves original code structure from v3.8.x
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#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 <NTPClient.h>
|
|
||||||
#include <WiFiUdp.h>
|
|
||||||
|
|
||||||
// ============== CONFIGURATION ==============
|
// ============== CONFIG ==============
|
||||||
String wifiSSID = "Dobro Veče";
|
const char* WIFI_SSID = "Dobro Veče";
|
||||||
String wifiPassword = "goodnight";
|
const char* WIFI_PASS = "goodnight";
|
||||||
|
|
||||||
const char* ALERT_TOPIC = "https://ntfy.sh/ALERT_klubhaus_topic/json?since=all";
|
const char* ALERT_URL = "https://ntfy.sh/ALERT_klubhaus_topic/json?since=all";
|
||||||
const char* SILENCE_TOPIC = "https://ntfy.sh/SILENCE_klubhaus_topic/json?since=all";
|
const char* SILENCE_URL = "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";
|
|
||||||
|
|
||||||
const unsigned long POLL_INTERVAL_MS = 5000;
|
|
||||||
const unsigned long BLINK_INTERVAL_MS = 500;
|
|
||||||
const unsigned long BUTTON_DEBOUNCE_MS = 50;
|
|
||||||
const unsigned long BUTTON_LONG_PRESS_MS = 1000;
|
|
||||||
const unsigned long BUTTON_DOUBLE_PRESS_MS = 300;
|
|
||||||
const unsigned long STALE_MESSAGE_THRESHOLD_S = 600; // 10 minutes (in seconds!)
|
|
||||||
const unsigned long BOOT_GRACE_PERIOD_MS = 30000;
|
|
||||||
const unsigned long NTP_SYNC_INTERVAL_MS = 3600000;
|
|
||||||
|
|
||||||
const int SCREEN_WIDTH = 320;
|
|
||||||
const int SCREEN_HEIGHT = 172;
|
|
||||||
|
|
||||||
const uint16_t COLOR_NEON_TEAL = 0x07D7;
|
|
||||||
const uint16_t COLOR_HOT_FUCHSIA = 0xF81F;
|
|
||||||
const uint16_t COLOR_COCAINE_WHITE = 0xFFDF;
|
|
||||||
const uint16_t COLOR_MIDNIGHT_BLACK = 0x0000;
|
|
||||||
const uint16_t COLOR_MINT_FLASH = 0x67F5;
|
|
||||||
|
|
||||||
// ============== GLOBALS ==============
|
// ============== GLOBALS ==============
|
||||||
TFT_eSPI tft = TFT_eSPI();
|
TFT_eSPI tft;
|
||||||
WiFiUDP ntpUDP;
|
|
||||||
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, NTP_SYNC_INTERVAL_MS);
|
|
||||||
|
|
||||||
enum DeviceState { STATE_SILENT, STATE_ALERTING, STATE_SENDING };
|
enum State { SILENT, ALERTING };
|
||||||
DeviceState currentState = STATE_SILENT;
|
State state = SILENT;
|
||||||
String currentMessage = "";
|
String alertMessage = "";
|
||||||
bool displayOn = true;
|
|
||||||
bool ledModeOnly = false;
|
|
||||||
|
|
||||||
const int BUTTON_PIN = 0;
|
|
||||||
unsigned long buttonLastPress = 0;
|
|
||||||
unsigned long buttonPressStart = 0;
|
|
||||||
int buttonPressCount = 0;
|
|
||||||
bool buttonPressed = false;
|
|
||||||
|
|
||||||
unsigned long lastBlinkToggle = 0;
|
|
||||||
bool blinkState = false;
|
|
||||||
uint16_t currentAlertColor = COLOR_NEON_TEAL;
|
|
||||||
|
|
||||||
unsigned long bootTime = 0;
|
|
||||||
bool inBootGracePeriod = true;
|
|
||||||
bool ntpSynced = false;
|
|
||||||
|
|
||||||
HTTPClient http;
|
|
||||||
unsigned long lastPoll = 0;
|
unsigned long lastPoll = 0;
|
||||||
|
unsigned long lastReconnect = 0;
|
||||||
// ============== NEW: DEDUPLICATION ==============
|
unsigned long bootMillis = 0;
|
||||||
String lastProcessedId = "";
|
bool wifiReady = false;
|
||||||
time_t lastKnownEpoch = 0;
|
|
||||||
|
|
||||||
// ============== SETUP ==============
|
// ============== SETUP ==============
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
while (!Serial && millis() < 3000) { ; }
|
delay(1000); // Let USB stabilize
|
||||||
|
|
||||||
bootTime = millis();
|
bootMillis = millis();
|
||||||
|
Serial.println("\n=== KLUBHAUS v3.9.2-STABLE ===");
|
||||||
Serial.println("[BOOT] KLUBHAUS ALERT v3.9.1-minimal");
|
Serial.println("Boot at millis=" + String(bootMillis));
|
||||||
Serial.println("[BOOT] Fix: message age filter + ID dedup");
|
|
||||||
|
|
||||||
|
// Init display early for feedback
|
||||||
tft.init();
|
tft.init();
|
||||||
tft.setRotation(1);
|
tft.setRotation(1);
|
||||||
tft.fillScreen(COLOR_MIDNIGHT_BLACK);
|
tft.fillScreen(0x0000);
|
||||||
tft.setTextColor(COLOR_COCAINE_WHITE, COLOR_MIDNIGHT_BLACK);
|
tft.setTextColor(0xFFDF, 0x0000);
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(4); // MC_DATUM
|
||||||
tft.setTextSize(2);
|
tft.setTextSize(2);
|
||||||
tft.drawString("KLUBHAUS", SCREEN_WIDTH/2, SCREEN_HEIGHT/2 - 20);
|
tft.drawString("BOOTING...", 160, 86);
|
||||||
tft.setTextSize(1);
|
|
||||||
tft.drawString("v3.9.1-minimal", SCREEN_WIDTH/2, SCREEN_HEIGHT/2 + 10);
|
|
||||||
tft.drawString("Booting...", SCREEN_WIDTH/2, SCREEN_HEIGHT/2 + 25);
|
|
||||||
|
|
||||||
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
// Connect WiFi with timeout
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||||
|
|
||||||
WiFi.begin(wifiSSID.c_str(), wifiPassword.c_str());
|
Serial.print("WiFi connecting");
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
|
while (WiFi.status() != WL_CONNECTED && attempts < 50) {
|
||||||
delay(500);
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
attempts++;
|
attempts++;
|
||||||
|
if (attempts % 10 == 0) {
|
||||||
|
Serial.println("\nWiFi status: " + String(WiFi.status()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeClient.begin();
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
if (timeClient.update()) {
|
wifiReady = true;
|
||||||
ntpSynced = true;
|
Serial.println("\nWiFi OK: " + WiFi.localIP().toString());
|
||||||
lastKnownEpoch = timeClient.getEpochTime();
|
tft.drawString("WIFI OK", 160, 110);
|
||||||
Serial.println("[BOOT] NTP synced: " + String(lastKnownEpoch));
|
} else {
|
||||||
|
Serial.println("\nWiFi FAILED, continuing offline");
|
||||||
|
tft.drawString("WIFI FAIL", 160, 110);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.println("[BOOT] Setup complete, grace period: 30s");
|
delay(1000);
|
||||||
|
drawSilent();
|
||||||
|
Serial.println("Setup complete, entering loop");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== LOOP ==============
|
// ============== LOOP ==============
|
||||||
void loop() {
|
void loop() {
|
||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
|
|
||||||
if (Serial.available()) {
|
// WiFi reconnect logic (throttled)
|
||||||
String cmd = Serial.readStringUntil('\n');
|
if (!wifiReady && now - lastReconnect > 30000) {
|
||||||
cmd.trim();
|
lastReconnect = now;
|
||||||
if (cmd == "CLEAR_DEDUP") {
|
Serial.println("WiFi reconnect attempt...");
|
||||||
lastProcessedId = "";
|
WiFi.reconnect();
|
||||||
Serial.println("[CMD] Deduplication cleared");
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
wifiReady = true;
|
||||||
|
Serial.println("WiFi reconnected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeClient.update()) {
|
// Poll ntfy (throttled, guarded)
|
||||||
ntpSynced = true;
|
if (wifiReady && now - lastPoll > 10000) { // 10 second min interval
|
||||||
lastKnownEpoch = timeClient.getEpochTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inBootGracePeriod && (now - bootTime >= BOOT_GRACE_PERIOD_MS)) {
|
|
||||||
inBootGracePeriod = false;
|
|
||||||
Serial.println("[BOOT] Grace period ended");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (now - lastPoll >= POLL_INTERVAL_MS) {
|
|
||||||
lastPoll = now;
|
lastPoll = now;
|
||||||
if (WiFi.status() == WL_CONNECTED && ntpSynced) {
|
pollNtfy();
|
||||||
pollTopic(ALERT_TOPIC, handleAlertMessage, "ALERT");
|
|
||||||
pollTopic(SILENCE_TOPIC, handleSilenceMessage, "SILENCE");
|
|
||||||
pollTopic(ADMIN_TOPIC, handleAdminMessage, "ADMIN");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleButton();
|
delay(100); // Yield to RTOS
|
||||||
updateDisplay();
|
|
||||||
|
|
||||||
delay(10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== POLLING ==============
|
// ============== NTFY POLLING (DEFENSIVE) ==============
|
||||||
void pollTopic(const char* url, void (*handler)(const String&), const char* topicName) {
|
void pollNtfy() {
|
||||||
http.begin(url);
|
Serial.println("Polling ntfy...");
|
||||||
http.setTimeout(10000);
|
|
||||||
|
|
||||||
int httpCode = http.GET();
|
HTTPClient http;
|
||||||
if (httpCode == HTTP_CODE_OK) {
|
http.setTimeout(8000); // 8 second timeout
|
||||||
String response = http.getString();
|
http.setConnectTimeout(5000); // 5 second connect timeout
|
||||||
if (response.length() > 0) {
|
|
||||||
parseMessages(response, topicName, handler);
|
// 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();
|
||||||
}
|
}
|
||||||
http.end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== PARSING WITH AGE FILTER & DEDUP ==============
|
void processBody(const String& body, const char* type) {
|
||||||
void parseMessages(String& response, const char* topicName, void (*handler)(const String&)) {
|
// Simple line-by-line parsing, no heavy JSON
|
||||||
// Grace period: ignore everything
|
int pos = 0;
|
||||||
if (inBootGracePeriod) {
|
while (pos < body.length()) {
|
||||||
return;
|
int end = body.indexOf('\n', pos);
|
||||||
}
|
if (end == -1) end = body.length();
|
||||||
|
|
||||||
// No time sync: can't validate, skip to be safe
|
String line = body.substring(pos, end);
|
||||||
if (!ntpSynced || lastKnownEpoch == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int lineStart = 0;
|
|
||||||
while (lineStart < response.length()) {
|
|
||||||
int lineEnd = response.indexOf('\n', lineStart);
|
|
||||||
if (lineEnd == -1) lineEnd = response.length();
|
|
||||||
|
|
||||||
String line = response.substring(lineStart, lineEnd);
|
|
||||||
line.trim();
|
line.trim();
|
||||||
|
|
||||||
if (line.length() > 0 && line.indexOf('{') >= 0) {
|
// Look for "message":"..." pattern
|
||||||
StaticJsonDocument<1024> doc;
|
int msgStart = line.indexOf("\"message\":\"");
|
||||||
DeserializationError err = deserializeJson(doc, line);
|
if (msgStart >= 0) {
|
||||||
|
msgStart += 11; // Skip "message":"
|
||||||
|
int msgEnd = line.indexOf("\"", msgStart);
|
||||||
|
if (msgEnd > msgStart) {
|
||||||
|
String msg = line.substring(msgStart, msgEnd);
|
||||||
|
|
||||||
if (!err) {
|
// Unescape simple cases
|
||||||
const char* msgId = doc["id"];
|
msg.replace("\\n", "\n");
|
||||||
const char* message = doc["message"];
|
msg.replace("\\\"", "\"");
|
||||||
time_t msgTime = doc["time"] | 0; // ntfy sends epoch SECONDS
|
|
||||||
|
|
||||||
if (message && strlen(message) > 0) {
|
Serial.println(String(type) + " msg: " + msg);
|
||||||
String msgStr = String(message);
|
|
||||||
String idStr = msgId ? String(msgId) : "";
|
|
||||||
|
|
||||||
// DEDUP: skip if same ID already processed
|
if (strcmp(type, "ALERT") == 0 && msg.length() > 0) {
|
||||||
if (idStr.length() > 0 && idStr == lastProcessedId) {
|
alertMessage = msg;
|
||||||
Serial.println("[DEDUP] Skipping duplicate: " + idStr.substring(0, 8));
|
state = ALERTING;
|
||||||
lineStart = lineEnd + 1;
|
drawAlert();
|
||||||
continue;
|
}
|
||||||
}
|
else if (strcmp(type, "SILENCE") == 0) {
|
||||||
|
state = SILENT;
|
||||||
// AGE CHECK: compare message time to current wall clock
|
alertMessage = "";
|
||||||
if (msgTime > 0) {
|
drawSilent();
|
||||||
time_t ageSeconds = lastKnownEpoch - msgTime;
|
|
||||||
|
|
||||||
if (ageSeconds > (time_t)STALE_MESSAGE_THRESHOLD_S) {
|
|
||||||
Serial.println("[STALE] Rejecting " + String(ageSeconds) + "s old message: " + msgStr.substring(0, 20));
|
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VALID: process and record ID
|
|
||||||
Serial.println("[" + String(topicName) + "] Processing: " + msgStr.substring(0, 40));
|
|
||||||
|
|
||||||
if (idStr.length() > 0) {
|
|
||||||
lastProcessedId = idStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
handler(msgStr);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
|
pos = end + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== HANDLERS ==============
|
|
||||||
void handleAlertMessage(const String& message) {
|
|
||||||
if (currentState == STATE_ALERTING && currentMessage == message) return;
|
|
||||||
|
|
||||||
currentState = STATE_ALERTING;
|
|
||||||
currentMessage = message;
|
|
||||||
displayOn = true;
|
|
||||||
|
|
||||||
publishStatus("ALERTING", message);
|
|
||||||
|
|
||||||
if (!ledModeOnly) {
|
|
||||||
drawAlertScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleSilenceMessage(const String& message) {
|
|
||||||
currentState = STATE_SILENT;
|
|
||||||
currentMessage = "";
|
|
||||||
displayOn = true;
|
|
||||||
|
|
||||||
publishStatus("SILENT", "silenced");
|
|
||||||
|
|
||||||
if (!ledModeOnly) {
|
|
||||||
drawSilentScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleAdminMessage(const String& message) {
|
|
||||||
Serial.println("[ADMIN] Command: " + message);
|
|
||||||
|
|
||||||
if (message == "MODE_SCREEN") {
|
|
||||||
ledModeOnly = false;
|
|
||||||
if (currentState == STATE_SILENT) drawSilentScreen();
|
|
||||||
else drawAlertScreen();
|
|
||||||
}
|
|
||||||
else if (message == "MODE_LED") {
|
|
||||||
ledModeOnly = true;
|
|
||||||
tft.fillScreen(COLOR_MIDNIGHT_BLACK);
|
|
||||||
}
|
|
||||||
else if (message == "SILENCE") {
|
|
||||||
handleSilenceMessage("admin");
|
|
||||||
}
|
|
||||||
else if (message == "PING") {
|
|
||||||
publishStatus("PONG", "ping");
|
|
||||||
}
|
|
||||||
else if (message == "REBOOT") {
|
|
||||||
Serial.println("[ADMIN] Rebooting...");
|
|
||||||
delay(100);
|
|
||||||
ESP.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============== STATUS ==============
|
|
||||||
void publishStatus(const char* state, const String& message) {
|
|
||||||
if (WiFi.status() != WL_CONNECTED) return;
|
|
||||||
|
|
||||||
StaticJsonDocument<256> doc;
|
|
||||||
doc["state"] = state;
|
|
||||||
doc["message"] = message;
|
|
||||||
doc["timestamp"] = (long long)timeClient.getEpochTime() * 1000LL;
|
|
||||||
doc["led_mode"] = ledModeOnly;
|
|
||||||
|
|
||||||
String payload;
|
|
||||||
serializeJson(doc, payload);
|
|
||||||
|
|
||||||
HTTPClient statusHttp;
|
|
||||||
statusHttp.begin(STATUS_TOPIC);
|
|
||||||
statusHttp.addHeader("Content-Type", "application/json");
|
|
||||||
statusHttp.POST(payload);
|
|
||||||
statusHttp.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============== DISPLAY ==============
|
// ============== DISPLAY ==============
|
||||||
void updateDisplay() {
|
void drawSilent() {
|
||||||
if (ledModeOnly) {
|
tft.fillScreen(0x0000);
|
||||||
if (currentState == STATE_ALERTING) {
|
tft.setTextColor(0xFFDF, 0x0000);
|
||||||
unsigned long now = millis();
|
|
||||||
if (now - lastBlinkToggle >= BLINK_INTERVAL_MS) {
|
|
||||||
lastBlinkToggle = now;
|
|
||||||
blinkState = !blinkState;
|
|
||||||
digitalWrite(2, blinkState ? HIGH : LOW);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentState == STATE_ALERTING) {
|
|
||||||
unsigned long now = millis();
|
|
||||||
if (now - lastBlinkToggle >= BLINK_INTERVAL_MS) {
|
|
||||||
lastBlinkToggle = now;
|
|
||||||
blinkState = !blinkState;
|
|
||||||
currentAlertColor = blinkState ? COLOR_NEON_TEAL : COLOR_HOT_FUCHSIA;
|
|
||||||
drawAlertScreen();
|
|
||||||
digitalWrite(2, blinkState ? HIGH : LOW);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawAlertScreen() {
|
|
||||||
if (ledModeOnly) return;
|
|
||||||
|
|
||||||
tft.fillScreen(COLOR_MIDNIGHT_BLACK);
|
|
||||||
tft.setTextColor(currentAlertColor, COLOR_MIDNIGHT_BLACK);
|
|
||||||
|
|
||||||
int textSize = 4;
|
|
||||||
if (currentMessage.length() > 10) textSize = 3;
|
|
||||||
if (currentMessage.length() > 20) textSize = 2;
|
|
||||||
|
|
||||||
tft.setTextSize(textSize);
|
|
||||||
tft.setTextDatum(MC_DATUM);
|
|
||||||
|
|
||||||
if (currentMessage.length() > 15) {
|
|
||||||
int mid = currentMessage.length() / 2;
|
|
||||||
int spacePos = currentMessage.lastIndexOf(' ', mid);
|
|
||||||
if (spacePos < 0) spacePos = mid;
|
|
||||||
tft.drawString(currentMessage.substring(0, spacePos), SCREEN_WIDTH/2, SCREEN_HEIGHT/2 - 15);
|
|
||||||
tft.drawString(currentMessage.substring(spacePos + 1), SCREEN_WIDTH/2, SCREEN_HEIGHT/2 + 15);
|
|
||||||
} else {
|
|
||||||
tft.drawString(currentMessage, SCREEN_WIDTH/2, SCREEN_HEIGHT/2);
|
|
||||||
}
|
|
||||||
|
|
||||||
tft.setTextSize(1);
|
|
||||||
tft.setTextColor(COLOR_MINT_FLASH, COLOR_MIDNIGHT_BLACK);
|
|
||||||
tft.setTextDatum(TL_DATUM);
|
|
||||||
tft.drawString("ALERT", 5, 5);
|
|
||||||
tft.setTextDatum(TR_DATUM);
|
|
||||||
tft.drawString(timeClient.getFormattedTime(), SCREEN_WIDTH - 5, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawSilentScreen() {
|
|
||||||
if (ledModeOnly) return;
|
|
||||||
|
|
||||||
tft.fillScreen(COLOR_MIDNIGHT_BLACK);
|
|
||||||
tft.setTextColor(COLOR_COCAINE_WHITE, COLOR_MIDNIGHT_BLACK);
|
|
||||||
tft.setTextSize(2);
|
tft.setTextSize(2);
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(4);
|
||||||
tft.drawString("SILENT", SCREEN_WIDTH/2, SCREEN_HEIGHT/2);
|
tft.drawString("SILENT", 160, 86);
|
||||||
|
|
||||||
tft.setTextSize(1);
|
|
||||||
tft.setTextColor(COLOR_MINT_FLASH, COLOR_MIDNIGHT_BLACK);
|
|
||||||
tft.setTextDatum(TL_DATUM);
|
|
||||||
tft.drawString("KLUBHAUS", 5, 5);
|
|
||||||
tft.setTextDatum(TR_DATUM);
|
|
||||||
tft.drawString(timeClient.getFormattedTime(), SCREEN_WIDTH - 5, 5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== BUTTON ==============
|
void drawAlert() {
|
||||||
void handleButton() {
|
tft.fillScreen(0x0000);
|
||||||
bool reading = (digitalRead(BUTTON_PIN) == LOW);
|
|
||||||
|
|
||||||
if (reading != buttonPressed) {
|
// Blink color
|
||||||
if (reading) {
|
uint16_t color = (millis() / 500) % 2 ? 0x07D7 : 0xF81F;
|
||||||
buttonPressStart = millis();
|
tft.setTextColor(color, 0x0000);
|
||||||
|
|
||||||
if (millis() - buttonLastPress < BUTTON_DOUBLE_PRESS_MS) {
|
// Auto size
|
||||||
buttonPressCount++;
|
int size = 4;
|
||||||
} else {
|
if (alertMessage.length() > 8) size = 3;
|
||||||
buttonPressCount = 1;
|
if (alertMessage.length() > 16) size = 2;
|
||||||
}
|
|
||||||
buttonLastPress = millis();
|
|
||||||
|
|
||||||
if (buttonPressCount == 2) {
|
tft.setTextSize(size);
|
||||||
ledModeOnly = !ledModeOnly;
|
tft.setTextDatum(4);
|
||||||
buttonPressCount = 0;
|
|
||||||
if (ledModeOnly) {
|
|
||||||
tft.fillScreen(COLOR_MIDNIGHT_BLACK);
|
|
||||||
} else {
|
|
||||||
if (currentState == STATE_SILENT) drawSilentScreen();
|
|
||||||
else drawAlertScreen();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unsigned long duration = millis() - buttonPressStart;
|
|
||||||
|
|
||||||
if (duration >= BUTTON_DEBOUNCE_MS && duration < BUTTON_LONG_PRESS_MS) {
|
// Simple wrap
|
||||||
if (currentState == STATE_ALERTING) {
|
if (alertMessage.length() > 20) {
|
||||||
handleSilenceMessage("button");
|
int mid = alertMessage.length() / 2;
|
||||||
} else {
|
int split = alertMessage.lastIndexOf(' ', mid);
|
||||||
handleAlertMessage("TEST ALERT");
|
if (split < 0) split = mid;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonPressed = reading;
|
tft.drawString(alertMessage.substring(0, split), 160, 70);
|
||||||
|
tft.drawString(alertMessage.substring(split + 1), 160, 102);
|
||||||
|
} else {
|
||||||
|
tft.drawString(alertMessage, 160, 86);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user