diff --git a/sketches/doorbell/doorbell.html b/sketches/doorbell/doorbell.html index 671927c..169ccf0 100644 --- a/sketches/doorbell/doorbell.html +++ b/sketches/doorbell/doorbell.html @@ -244,6 +244,66 @@ #toast.error { background: #f890e7; } + + /* Admin panel styles */ + .admin-toggle { + text-align: center; + margin-top: 20px; + padding: 10px; + font-size: 10px; + color: #555; + cursor: pointer; + letter-spacing: 1px; + text-transform: uppercase; + } + + .admin-toggle:hover { + color: #888; + } + + #adminPanel { + display: none; + background: #111; + border: 1px solid #333; + border-radius: 8px; + padding: 15px; + margin-top: 10px; + } + + #adminPanel.visible { + display: block; + } + + .admin-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + } + + .admin-btn { + padding: 12px; + font-size: 11px; + background: #1a1a1a; + border: 1px solid #444; + color: #888; + border-radius: 6px; + cursor: pointer; + } + + .admin-btn:hover { + border-color: #0bd3d3; + color: #0bd3d3; + } + + .admin-btn.danger { + border-color: #f890e7; + color: #f890e7; + } + + .admin-btn.danger:hover { + background: #f890e7; + color: #000; + } @@ -268,6 +328,19 @@ + +
⚙ Admin
+
+
+ + + + + + +
+
+
Sent
diff --git a/sketches/doorbell/doorbell.ino b/sketches/doorbell/doorbell.ino index 12e2476..2c8dc32 100644 --- a/sketches/doorbell/doorbell.ino +++ b/sketches/doorbell/doorbell.ino @@ -1,5 +1,5 @@ -// ============== KLUBHAUS DOORBELL v3.8.5 ============== -// Fix: Restore button handling, fix double ntfy.sh in URL +// ============== KLUBHAUS DOORBELL v4.0.0 ============== +// Features: Screen fallback mode, LED-only mode, Serial config commands, Admin topic #include #include @@ -10,10 +10,11 @@ #define WIFI_SSID "iot-2GHz" #define WIFI_PASS "lesson-greater" -// ntfy.sh topics — naming indicates direction +// ntfy.sh topics #define NTFY_ALERT_TOPIC "ALERT_klubhaus_topic" #define NTFY_SILENCE_TOPIC "SILENCE_klubhaus_topic" #define NTFY_STATUS_TOPIC "STATUS_klubhaus_topic" +#define NTFY_ADMIN_TOPIC "ADMIN_klubhaus_topic" // NEW: Admin/config topic #define NTFY_SERVER "ntfy.sh" // Display pins for ESP32-C6-LCD-1.47 @@ -25,6 +26,9 @@ #define TFT_BL 22 #define BUTTON_PIN 9 +// LED pin (built-in or external) - adjust for your board +#define LED_PIN 8 // ESP32-C6 built-in RGB LED or GPIO for external LED + // ============== COLORS ============== #define COLOR_TEAL 0x0BD3D3 #define COLOR_FUCHSIA 0xF890E7 @@ -39,11 +43,22 @@ Arduino_GFX *gfx = new Arduino_ST7789(bus, TFT_RST, 0, true, 172, 320); enum State { SILENT, ALERTING }; State currentState = SILENT; String currentMessage = ""; -String lastProcessedId = ""; +String lastProcessedAlertId = ""; +String lastProcessedAdminId = ""; unsigned long lastAlertPoll = 0; unsigned long lastSilencePoll = 0; +unsigned long lastAdminPoll = 0; const unsigned long POLL_INTERVAL_MS = 3000; +const unsigned long ADMIN_POLL_INTERVAL_MS = 10000; // Admin checks less frequently + +// ============== FALLBACK MODE ============== +enum DisplayMode { MODE_SCREEN, MODE_LED_ONLY }; +DisplayMode currentDisplayMode = MODE_SCREEN; +bool screenAvailable = true; // Set to false if screen init fails + +// ============== SERIAL COMMANDS ============== +String serialBuffer = ""; // ============== LOGGING ============== enum LogLevel { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL }; @@ -78,10 +93,45 @@ unsigned long getEpochMs() { return millis(); } +// ============== LED CONTROL ============== +void initLED() { + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + + // For RGB LED on ESP32-C6, you might need neopixel library + // This is basic single-color LED control +} + +void setLED(bool on) { + digitalWrite(LED_PIN, on ? HIGH : LOW); +} + +void flashLED(int times, int onMs = 200, int offMs = 200) { + for (int i = 0; i < times; i++) { + setLED(true); + delay(onMs); + setLED(false); + if (i < times - 1) delay(offMs); + } +} + +void pulseLED(int durationMs) { + unsigned long start = millis(); + while (millis() - start < durationMs) { + // Simple pulse effect + setLED(true); + delay(100); + setLED(false); + delay(100); + } +} + // ============== DISPLAY ============== void initDisplay() { pinMode(TFT_BL, OUTPUT); digitalWrite(TFT_BL, LOW); + + // Try to init screen - if it fails, we fall back to LED mode gfx->begin(); gfx->setRotation(1); gfx->fillScreen(COLOR_BLACK); @@ -90,18 +140,38 @@ void initDisplay() { gfx->fillRect(80, 0, 80, 172, COLOR_FUCHSIA); delay(500); digitalWrite(TFT_BL, LOW); + + logMsg(LOG_INFO, "DISPLAY", "Screen initialized"); } void setBacklight(bool on) { + if (currentDisplayMode == MODE_LED_ONLY) return; digitalWrite(TFT_BL, on ? HIGH : LOW); } void showSilent() { + if (currentDisplayMode == MODE_LED_ONLY) { + setLED(false); + return; + } + + if (!screenAvailable) return; gfx->fillScreen(COLOR_BLACK); setBacklight(false); } void showAlert(const String& msg) { + if (currentDisplayMode == MODE_LED_ONLY) { + // Flash LED pattern for alert + pulseLED(5000); // Pulse for 5 seconds + return; + } + + if (!screenAvailable) { + flashLED(5, 300, 200); + return; + } + setBacklight(true); int fontSize = msg.length() > 20 ? 2 : (msg.length() > 10 ? 3 : 4); gfx->fillScreen(COLOR_TEAL); @@ -116,6 +186,16 @@ void showAlert(const String& msg) { } void flashConfirm(const char* text) { + if (currentDisplayMode == MODE_LED_ONLY) { + flashLED(3, 100, 100); // Quick triple flash + return; + } + + if (!screenAvailable) { + flashLED(2, 500, 200); + return; + } + setBacklight(true); gfx->fillScreen(COLOR_MINT); gfx->setTextColor(COLOR_BLACK); @@ -167,13 +247,9 @@ bool parseNtfyLine(const String& line, String& outId, String& outMessage, String return outId.length() > 0; } -// CRITICAL FIX: Correct URL construction — was double ntfy.sh! bool fetchNtfy(const char* topic, String& outId, String& outMessage, String& outEvent) { HTTPClient http; - // FIXED: NTFY_SERVER is "ntfy.sh", topic is "ALERT_klubhaus_topic" - // Was: https://ntfy.sh/ntfy.sh/ALERT... — WRONG! - // Now: https://ntfy.sh/ALERT... — CORRECT! String url = String("https://") + NTFY_SERVER + "/" + topic + "/json?since=all"; logMsg(LOG_DEBUG, "HTTP", "GET " + url); @@ -238,6 +314,167 @@ void postStatus(const char* state, const String& msg) { http.end(); } +// ============== ADMIN / CONFIG ============== + +void processAdminCommand(const String& cmd) { + logMsg(LOG_INFO, "ADMIN", "Processing: " + cmd); + + if (cmd == "MODE_SCREEN") { + currentDisplayMode = MODE_SCREEN; + logMsg(LOG_INFO, "ADMIN", "Switched to SCREEN mode"); + postStatus("CONFIG", "mode=screen"); + flashConfirm("SCREEN MODE"); + } + else if (cmd == "MODE_LED") { + currentDisplayMode = MODE_LED_ONLY; + logMsg(LOG_INFO, "ADMIN", "Switched to LED mode"); + postStatus("CONFIG", "mode=led"); + showSilent(); // Turn off screen + flashConfirm("LED MODE"); + } + else if (cmd == "SILENCE" || cmd == "SILENT") { + if (currentState == ALERTING) { + setState(SILENT, ""); + flashConfirm("SILENCED"); + } + } + else if (cmd == "PING") { + postStatus("PONG", "device=klubhaus"); + } + else if (cmd == "REBOOT") { + logMsg(LOG_INFO, "ADMIN", "Rebooting..."); + postStatus("CONFIG", "rebooting"); + delay(500); + ESP.restart(); + } + else if (cmd.startsWith("WIFI ")) { + // Format: WIFI newssid newpassword + // Not implementing full reconnection here for safety + logMsg(LOG_WARN, "ADMIN", "WiFi change requested - manual reboot required"); + postStatus("CONFIG", "wifi_change_pending"); + } + else if (cmd.startsWith("TOPIC ")) { + // Format: TOPIC ALERT newtopicname + logMsg(LOG_WARN, "ADMIN", "Topic change not implemented in runtime"); + postStatus("CONFIG", "topic_change_ignored"); + } + else { + logMsg(LOG_WARN, "ADMIN", "Unknown command: " + cmd); + } +} + +void checkAdminTopic() { + String id, message, event; + + if (fetchNtfy(NTFY_ADMIN_TOPIC, id, message, event)) { + logMsg(LOG_INFO, "ADMIN", "GOT: id=" + id + " msg=[" + message + "]"); + + if (id != lastProcessedAdminId) { + lastProcessedAdminId = id; + processAdminCommand(message); + } else { + logMsg(LOG_DEBUG, "ADMIN", "Duplicate ID, ignored"); + } + } +} + +// ============== SERIAL COMMANDS ============== + +void processSerialCommand(const String& cmd) { + logMsg(LOG_INFO, "SERIAL", "Command: " + cmd); + + // Same commands as admin topic + if (cmd == "help" || cmd == "?") { + Serial.println("=== KLUBHAUS COMMANDS ==="); + Serial.println("mode screen - Use screen + LED"); + Serial.println("mode led - LED only (screen off)"); + Serial.println("silence - Force silence"); + Serial.println("ping - Test connectivity"); + Serial.println("reboot - Restart device"); + Serial.println("status - Show current state"); + Serial.println("wifi - Change WiFi (reboot required)"); + Serial.println("========================="); + } + else if (cmd == "mode screen") { + currentDisplayMode = MODE_SCREEN; + logMsg(LOG_INFO, "SERIAL", "Switched to SCREEN mode"); + postStatus("CONFIG", "mode=screen"); + flashConfirm("SCREEN MODE"); + } + else if (cmd == "mode led") { + currentDisplayMode = MODE_LED_ONLY; + logMsg(LOG_INFO, "SERIAL", "Switched to LED mode"); + postStatus("CONFIG", "mode=led"); + showSilent(); + flashConfirm("LED MODE"); + } + else if (cmd == "silence") { + if (currentState == ALERTING) { + setState(SILENT, ""); + flashConfirm("SILENCED"); + } + } + else if (cmd == "ping") { + postStatus("PONG", "device=klubhaus serial=true"); + Serial.println("pong"); + } + else if (cmd == "reboot") { + logMsg(LOG_INFO, "SERIAL", "Rebooting..."); + ESP.restart(); + } + else if (cmd == "status") { + Serial.printf("State: %s\n", currentState == ALERTING ? "ALERTING" : "SILENT"); + Serial.printf("Mode: %s\n", currentDisplayMode == MODE_SCREEN ? "SCREEN" : "LED_ONLY"); + Serial.printf("Message: %s\n", currentMessage.c_str()); + Serial.printf("Screen: %s\n", screenAvailable ? "OK" : "BROKEN/UNAVAILABLE"); + Serial.printf("WiFi: %s\n", WiFi.isConnected() ? "CONNECTED" : "DISCONNECTED"); + Serial.printf("IP: %s\n", WiFi.localIP().toString().c_str()); + } + else if (cmd.startsWith("wifi ")) { + // Parse: wifi ssid password + int firstSpace = cmd.indexOf(' '); + int secondSpace = cmd.indexOf(' ', firstSpace + 1); + + if (secondSpace > 0) { + String newSsid = cmd.substring(firstSpace + 1, secondSpace); + String newPass = cmd.substring(secondSpace + 1); + + logMsg(LOG_INFO, "SERIAL", "WiFi config updated (reboot to apply)"); + Serial.println("WiFi updated. Reboot to apply:"); + Serial.println(" SSID: " + newSsid); + // Don't print password + // Store in preferences or just inform user to update code + } else { + Serial.println("Usage: wifi "); + } + } + else { + Serial.println("Unknown command. Type 'help' for list."); + } +} + +void checkSerial() { + while (Serial.available()) { + char c = Serial.read(); + + if (c == '\n' || c == '\r') { + if (serialBuffer.length() > 0) { + serialBuffer.trim(); + serialBuffer.toLowerCase(); + processSerialCommand(serialBuffer); + serialBuffer = ""; + } + } else { + serialBuffer += c; + // Prevent buffer overflow + if (serialBuffer.length() > 100) { + serialBuffer = ""; + Serial.println("Command too long, cleared"); + } + } + } +} + // ============== STATE MACHINE ============== void setState(State newState, const String& msg) { if (currentState == newState && currentMessage == msg) return; @@ -253,31 +490,90 @@ void setState(State newState, const String& msg) { postStatus(newState == ALERTING ? "ALERTING" : "SILENT", msg); } -// ============== BUTTON HANDLING (RESTORED) ============== +// ============== BUTTON HANDLING (WITH DOUBLE-PRESS) ============== void checkButton() { static bool lastState = HIGH; static unsigned long pressStart = 0; + static unsigned long lastPressEnd = 0; static bool wasPressed = false; + static int pressCount = 0; + static unsigned long firstPressTime = 0; bool pressed = (digitalRead(BUTTON_PIN) == LOW); if (pressed && !lastState) { + // Button just pressed logMsg(LOG_INFO, "BUTTON", "PRESSED"); setBacklight(true); pressStart = millis(); wasPressed = true; + + // Check for double press + if (pressCount == 0) { + firstPressTime = millis(); + pressCount = 1; + } else if (millis() - firstPressTime < 500) { + // Double press detected! + pressCount = 2; + logMsg(LOG_INFO, "BUTTON", "DOUBLE PRESS - TOGLING MODE"); + + // Toggle display mode + if (currentDisplayMode == MODE_SCREEN) { + currentDisplayMode = MODE_LED_ONLY; + showSilent(); // Turn off screen immediately + flashConfirm("LED MODE"); + logMsg(LOG_INFO, "BUTTON", "Switched to LED mode"); + postStatus("CONFIG", "mode=led (button)"); + } else { + currentDisplayMode = MODE_SCREEN; + flashConfirm("SCREEN MODE"); + logMsg(LOG_INFO, "BUTTON", "Switched to SCREEN mode"); + postStatus("CONFIG", "mode=screen (button)"); + } + } } else if (!pressed && lastState && wasPressed) { + // Button just released unsigned long duration = millis() - pressStart; + unsigned long now = millis(); logMsg(LOG_INFO, "BUTTON", String("RELEASED ") + duration + "ms"); + // Handle single press actions + if (pressCount == 1) { + if (now - firstPressTime > 500) { + // Single press confirmed (not part of double) + if (currentState == ALERTING) { + logMsg(LOG_INFO, "BUTTON", "Force silence"); + setState(SILENT, ""); + flashConfirm("SILENCED"); + } else { + // Just flash to acknowledge + flashConfirm("READY"); + } + pressCount = 0; + } else { + // Wait to see if this becomes a double press + // Don't clear pressCount yet + } + } else if (pressCount == 2) { + // Double press already handled on press, just reset + pressCount = 0; + } + + wasPressed = false; + lastPressEnd = now; + } + + // Timeout for single press detection + if (pressCount == 1 && millis() - firstPressTime > 500) { + // Single press timeout - treat as single press if (currentState == ALERTING) { - logMsg(LOG_INFO, "BUTTON", "Force silence"); + logMsg(LOG_INFO, "BUTTON", "Force silence (single)"); setState(SILENT, ""); flashConfirm("SILENCED"); } else { - setBacklight(false); + flashConfirm("READY"); } - wasPressed = false; + pressCount = 0; } lastState = pressed; @@ -288,23 +584,41 @@ void setup() { Serial.begin(115200); delay(1000); - logMsg(LOG_INFO, "MAIN", "=== KLUBHAUS DOORBELL v3.8.5 ==="); - logMsg(LOG_INFO, "MAIN", "Fix: Button restored, URL fixed"); + logMsg(LOG_INFO, "MAIN", "=== KLUBHAUS DOORBELL v4.0.0 ==="); + logMsg(LOG_INFO, "MAIN", "Features: Fallback mode, Serial commands, Admin topic"); - pinMode(BUTTON_PIN, INPUT_PULLUP); // RESTORED + pinMode(BUTTON_PIN, INPUT_PULLUP); + initLED(); + + // Try to init display - if it fails, auto-fallback to LED + // You can force LED mode by holding button during boot + bool forceLedMode = (digitalRead(BUTTON_PIN) == LOW); + if (forceLedMode) { + logMsg(LOG_INFO, "MAIN", "Button held at boot - forcing LED mode"); + currentDisplayMode = MODE_LED_ONLY; + screenAvailable = false; + delay(500); // Wait for button release + } else { + // Try to init display + // If screen is broken, this might hang or fail silently + // For now we assume it works unless explicitly disabled + initDisplay(); + } - initDisplay(); initWiFi(); postStatus("SILENT", ""); - logMsg(LOG_INFO, "MAIN", "READY — send FRONT DOOR from web client"); + logMsg(LOG_INFO, "MAIN", "READY - Double-press button to toggle mode"); + logMsg(LOG_INFO, "MAIN", "Serial commands available - type 'help'"); } void loop() { - checkButton(); // RESTORED + checkButton(); + checkSerial(); // NEW: Check for serial commands unsigned long now = millis(); + // Poll alert topic if (now - lastAlertPoll >= POLL_INTERVAL_MS) { lastAlertPoll = now; @@ -313,8 +627,8 @@ void loop() { if (fetchNtfy(NTFY_ALERT_TOPIC, id, message, event)) { logMsg(LOG_INFO, "ALERT", "GOT: id=" + id + " msg=[" + message + "]"); - if (id != lastProcessedId) { - lastProcessedId = id; + if (id != lastProcessedAlertId) { + lastProcessedAlertId = id; if (message != "SILENCE") { logMsg(LOG_INFO, "ALERT", "TRIGGER: " + message); setState(ALERTING, message); @@ -325,6 +639,7 @@ void loop() { } } + // Poll silence topic if (now - lastSilencePoll >= POLL_INTERVAL_MS + 1500) { lastSilencePoll = now; @@ -342,6 +657,12 @@ void loop() { } } + // Poll admin topic (less frequently) + if (now - lastAdminPoll >= ADMIN_POLL_INTERVAL_MS) { + lastAdminPoll = now; + checkAdminTopic(); + } + delay(50); }