diff --git a/.arduino_config.lua b/.arduino_config.lua index d49453c..908b294 100644 --- a/.arduino_config.lua +++ b/.arduino_config.lua @@ -1,5 +1,5 @@ local M = {} -M.board = 'arduino:avr:uno' +M.board = 'esp32:esp32:esp32' M.port = '/dev/ttyUSB0' -M.baudrate =115200 +M.baudrate = 115200 return M diff --git a/boards/esp32-32e-4/DisplayDriverTFT.cpp b/boards/esp32-32e-4/DisplayDriverTFT.cpp index 92147a2..75f1bb3 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.cpp +++ b/boards/esp32-32e-4/DisplayDriverTFT.cpp @@ -17,7 +17,10 @@ void DisplayDriverTFT::begin() { Serial.printf("[TOUCH] Raw Z=%d (non-zero = controller detected)\n", z); Serial.flush(); - drawBoot(); + ScreenState st; + st.screen = ScreenID::BOOT; + st.bootStage = BootStage::SPLASH; + drawBoot(st); digitalWrite(PIN_LCD_BL, HIGH); Serial.println("[GFX] Backlight ON"); @@ -29,15 +32,16 @@ void DisplayDriverTFT::setBacklight(bool on) { digitalWrite(PIN_LCD_BL, on ? HIG // ── Rendering ─────────────────────────────────────────────── void DisplayDriverTFT::render(const ScreenState& st) { - if(st.screen != _lastScreen) { + if(st.screen != _lastScreen || (st.screen == ScreenID::BOOT && st.bootStage != _lastBootStage)) { _needsRedraw = true; _lastScreen = st.screen; + _lastBootStage = st.bootStage; } switch(st.screen) { case ScreenID::BOOT: if(_needsRedraw) { - drawBoot(); + drawBoot(st); _needsRedraw = false; } break; @@ -60,7 +64,9 @@ void DisplayDriverTFT::render(const ScreenState& st) { } } -void DisplayDriverTFT::drawBoot() { +void DisplayDriverTFT::drawBoot(const ScreenState& st) { + BootStage stage = st.bootStage; + _tft.fillScreen(TFT_BLACK); _tft.setTextColor(TFT_WHITE, TFT_BLACK); _tft.setTextSize(2); @@ -69,8 +75,29 @@ void DisplayDriverTFT::drawBoot() { _tft.setTextSize(1); _tft.setCursor(10, 40); _tft.print(BOARD_NAME); - _tft.setCursor(10, 60); - _tft.print("Booting..."); + + // Show boot stage status + _tft.setCursor(10, 70); + switch(stage) { + case BootStage::SPLASH: + _tft.print("Initializing..."); + break; + case BootStage::INIT_DISPLAY: + _tft.print("Display OK"); + break; + case BootStage::INIT_NETWORK: + _tft.print("Network init..."); + break; + case BootStage::CONNECTING_WIFI: + _tft.print("Connecting WiFi..."); + break; + case BootStage::READY: + _tft.print("All systems go!"); + break; + case BootStage::DONE: + _tft.print("Ready!"); + break; + } } void DisplayDriverTFT::drawAlert(const ScreenState& st) { diff --git a/boards/esp32-32e-4/DisplayDriverTFT.h b/boards/esp32-32e-4/DisplayDriverTFT.h index 5c388e5..c2fbecc 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.h +++ b/boards/esp32-32e-4/DisplayDriverTFT.h @@ -19,7 +19,7 @@ public: int height() override { return DISPLAY_HEIGHT; } private: - void drawBoot(); + void drawBoot(const ScreenState& st); void drawAlert(const ScreenState& st); void drawDashboard(const ScreenState& st); @@ -28,5 +28,6 @@ private: bool _holdActive = false; uint32_t _holdStartMs = 0; ScreenID _lastScreen = ScreenID::BOOT; + BootStage _lastBootStage = BootStage::SPLASH; bool _needsRedraw = true; }; diff --git a/boards/esp32-32e-4/esp32-32e-4.ino b/boards/esp32-32e-4/esp32-32e-4.ino index be5e792..065c08d 100644 --- a/boards/esp32-32e-4/esp32-32e-4.ino +++ b/boards/esp32-32e-4/esp32-32e-4.ino @@ -21,55 +21,29 @@ void setup() { } void loop() { - // ── Read touch ── + // Read touch TouchEvent evt = display.readTouch(); - // ── Touch debug ── + // Touch debug (useful for new boards) if(evt.pressed) { Serial.printf("[TOUCH] pressed: x=%d, y=%d\n", evt.x, evt.y); } - // ── State machine tick ── + // State machine tick logic.update(); - // ── Render current screen ── + // Render current screen display.render(logic.getScreenState()); - // ── Touch handling (tap gestures) ── - const ScreenState& st = logic.getScreenState(); - int tile = logic.handleTouch(evt); + // Handle tap gestures + logic.handleTouch(evt); - // ── Hold gesture (for silencing alerts) ── - static int holdStartX = -1; - static int holdStartY = -1; + // Handle hold-to-silence gesture + logic.updateHold(evt); - if(st.deviceState == DeviceState::ALERTING) { - HoldState h = display.updateHold(HOLD_TO_SILENCE_MS); - if(h.completed) { - logic.silenceAlert(); - holdStartX = -1; - holdStartY = -1; - } - if(h.started) { - holdStartX = evt.x; - holdStartY = evt.y; - } - if(holdStartX >= 0) { - display.updateHint(holdStartX, holdStartY, h.active); - } - } else { - holdStartX = -1; - holdStartY = -1; - } + // Serial console + logic.processSerial(); - // ── Serial console ── - if(Serial.available()) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - if(cmd.length() > 0) - logic.onSerialCommand(cmd); - } - - // Yield to WiFi/BT stack (prevents Task Watchdog timeout) - delay(10); -} \ No newline at end of file + // Yield to WiFi/BT stack + delay(LOOP_YIELD_MS); +} diff --git a/boards/esp32-32e/DisplayDriverTFT.cpp b/boards/esp32-32e/DisplayDriverTFT.cpp index acb22c5..07a8d13 100644 --- a/boards/esp32-32e/DisplayDriverTFT.cpp +++ b/boards/esp32-32e/DisplayDriverTFT.cpp @@ -11,7 +11,10 @@ void DisplayDriverTFT::begin() { Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT); - drawBoot(); + ScreenState st; + st.screen = ScreenID::BOOT; + st.bootStage = BootStage::SPLASH; + drawBoot(st); digitalWrite(PIN_LCD_BL, HIGH); Serial.println("[GFX] Backlight ON"); @@ -22,15 +25,16 @@ void DisplayDriverTFT::setBacklight(bool on) { digitalWrite(PIN_LCD_BL, on ? HIG // ── Rendering ─────────────────────────────────────────────── void DisplayDriverTFT::render(const ScreenState& st) { - if(st.screen != _lastScreen) { + if(st.screen != _lastScreen || (st.screen == ScreenID::BOOT && st.bootStage != _lastBootStage)) { _needsRedraw = true; _lastScreen = st.screen; + _lastBootStage = st.bootStage; } switch(st.screen) { case ScreenID::BOOT: if(_needsRedraw) { - drawBoot(); + drawBoot(st); _needsRedraw = false; } break; @@ -53,7 +57,9 @@ void DisplayDriverTFT::render(const ScreenState& st) { } } -void DisplayDriverTFT::drawBoot() { +void DisplayDriverTFT::drawBoot(const ScreenState& st) { + BootStage stage = st.bootStage; + _tft.fillScreen(TFT_BLACK); _tft.setTextColor(TFT_WHITE, TFT_BLACK); _tft.setTextSize(2); @@ -62,8 +68,29 @@ void DisplayDriverTFT::drawBoot() { _tft.setTextSize(1); _tft.setCursor(10, 40); _tft.print(BOARD_NAME); - _tft.setCursor(10, 60); - _tft.print("Booting..."); + + // Show boot stage status + _tft.setCursor(10, 70); + switch(stage) { + case BootStage::SPLASH: + _tft.print("Initializing..."); + break; + case BootStage::INIT_DISPLAY: + _tft.print("Display OK"); + break; + case BootStage::INIT_NETWORK: + _tft.print("Network init..."); + break; + case BootStage::CONNECTING_WIFI: + _tft.print("Connecting WiFi..."); + break; + case BootStage::READY: + _tft.print("All systems go!"); + break; + case BootStage::DONE: + _tft.print("Ready!"); + break; + } } void DisplayDriverTFT::drawAlert(const ScreenState& st) { diff --git a/boards/esp32-32e/DisplayDriverTFT.h b/boards/esp32-32e/DisplayDriverTFT.h index c8e1c18..f46c873 100644 --- a/boards/esp32-32e/DisplayDriverTFT.h +++ b/boards/esp32-32e/DisplayDriverTFT.h @@ -18,7 +18,7 @@ public: int height() override { return DISPLAY_HEIGHT; } private: - void drawBoot(); + void drawBoot(const ScreenState& st); void drawAlert(const ScreenState& st); void drawDashboard(const ScreenState& st); @@ -27,5 +27,6 @@ private: bool _holdActive = false; uint32_t _holdStartMs = 0; ScreenID _lastScreen = ScreenID::BOOT; + BootStage _lastBootStage = BootStage::SPLASH; bool _needsRedraw = true; }; diff --git a/boards/esp32-32e/esp32-32e.ino b/boards/esp32-32e/esp32-32e.ino index bf1217e..c1faf50 100644 --- a/boards/esp32-32e/esp32-32e.ino +++ b/boards/esp32-32e/esp32-32e.ino @@ -21,47 +21,24 @@ void setup() { } void loop() { - // ── Read touch ── + // Read touch TouchEvent evt = display.readTouch(); - // ── State machine tick ── + // State machine tick logic.update(); - // ── Render current screen ── + // Render current screen display.render(logic.getScreenState()); - // ── Touch handling (tap gestures) ── - const ScreenState& st = logic.getScreenState(); - int tile = logic.handleTouch(evt); + // Handle tap gestures + logic.handleTouch(evt); - // ── Hold gesture (for silencing alerts) ── - static int holdStartX = -1; - static int holdStartY = -1; + // Handle hold-to-silence gesture + logic.updateHold(evt); - if(st.deviceState == DeviceState::ALERTING) { - HoldState h = display.updateHold(HOLD_TO_SILENCE_MS); - if(h.completed) { - logic.silenceAlert(); - holdStartX = -1; - holdStartY = -1; - } - if(h.started) { - holdStartX = evt.x; - holdStartY = evt.y; - } - if(holdStartX >= 0) { - display.updateHint(holdStartX, holdStartY, h.active); - } - } else { - holdStartX = -1; - holdStartY = -1; - } + // Serial console + logic.processSerial(); - // ── Serial console ── - if(Serial.available()) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - if(cmd.length() > 0) - logic.onSerialCommand(cmd); - } + // Yield to WiFi/BT stack + delay(LOOP_YIELD_MS); } diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp index 9394ca2..45d8178 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp @@ -129,19 +129,16 @@ void DisplayDriverGFX::render(const ScreenState& state) { return; // Check if we need full redraw - if(state.screen != _lastScreen) { + if(state.screen != _lastScreen || (state.screen == ScreenID::BOOT && state.bootStage != _lastBootStage)) { _needsRedraw = true; _lastScreen = state.screen; + _lastBootStage = state.bootStage; } switch(state.screen) { case ScreenID::BOOT: if(_needsRedraw) { - _gfx->fillScreen(0x000000); - _gfx->setTextColor(0xFFFF); - _gfx->setTextSize(2); - _gfx->setCursor(10, 10); - _gfx->print("KLUBHAUS BOOT"); + drawBoot(state); _needsRedraw = false; } break; @@ -170,6 +167,43 @@ void DisplayDriverGFX::render(const ScreenState& state) { } } +void DisplayDriverGFX::drawBoot(const ScreenState& state) { + BootStage stage = state.bootStage; + + _gfx->fillScreen(0x000000); + _gfx->setTextColor(0xFFFF); + _gfx->setTextSize(2); + _gfx->setCursor(10, 10); + _gfx->print("KLUBHAUS"); + + _gfx->setTextSize(1); + _gfx->setCursor(10, 50); + _gfx->print(BOARD_NAME); + + // Show boot stage status + _gfx->setCursor(10, 80); + switch(stage) { + case BootStage::SPLASH: + _gfx->print("Initializing..."); + break; + case BootStage::INIT_DISPLAY: + _gfx->print("Display OK"); + break; + case BootStage::INIT_NETWORK: + _gfx->print("Network init..."); + break; + case BootStage::CONNECTING_WIFI: + _gfx->print("Connecting WiFi..."); + break; + case BootStage::READY: + _gfx->print("All systems go!"); + break; + case BootStage::DONE: + _gfx->print("Ready!"); + break; + } +} + void DisplayDriverGFX::drawAlert(const ScreenState& state) { uint32_t elapsed = millis() - state.alertStartMs; uint8_t pulse = static_cast(180.0f + 75.0f * sinf(elapsed / 300.0f)); diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h index bd88992..991579b 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h @@ -24,6 +24,7 @@ public: private: // Helper rendering functions + void drawBoot(const ScreenState& state); void drawAlert(const ScreenState& state); void drawDashboard(const ScreenState& state); @@ -34,5 +35,6 @@ private: // Screen tracking ScreenID _lastScreen = ScreenID::BOOT; + BootStage _lastBootStage = BootStage::SPLASH; bool _needsRedraw = true; }; diff --git a/boards/esp32-s3-lcd-43/esp32-s3-lcd-43.ino b/boards/esp32-s3-lcd-43/esp32-s3-lcd-43.ino index d5fa564..dd0298c 100644 --- a/boards/esp32-s3-lcd-43/esp32-s3-lcd-43.ino +++ b/boards/esp32-s3-lcd-43/esp32-s3-lcd-43.ino @@ -21,48 +21,21 @@ void setup() { } void loop() { - // ── Read touch ── + // Read touch TouchEvent evt = display.readTouch(); - // ── State machine tick ── + // State machine tick logic.update(); + + // Render current screen display.render(logic.getScreenState()); - // ── Touch handling (tap gestures) ── - const ScreenState& st = logic.getScreenState(); - int tile = logic.handleTouch(evt); + // Handle tap gestures + logic.handleTouch(evt); - // ── Hold gesture (for silencing alerts) ── - static int holdStartX = -1; - static int holdStartY = -1; + // Handle hold-to-silence gesture + logic.updateHold(evt); - if(st.deviceState == DeviceState::ALERTING) { - HoldState h = display.updateHold(HOLD_TO_SILENCE_MS); - if(h.completed) { - logic.silenceAlert(); - holdStartX = -1; - holdStartY = -1; - } - if(h.started) { - holdStartX = evt.x; - holdStartY = evt.y; - } - if(holdStartX >= 0) { - if(h.active) { - display.updateHint(holdStartX, holdStartY, true); - } else { - display.updateHint(holdStartX, holdStartY, false); - } - } - } else { - holdStartX = -1; - holdStartY = -1; - } - - if(Serial.available()) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - if(cmd.length() > 0) - logic.onSerialCommand(cmd); - } + // Serial console + logic.processSerial(); } diff --git a/libraries/KlubhausCore/src/Config.h b/libraries/KlubhausCore/src/Config.h index 5e8ab29..2299225 100644 --- a/libraries/KlubhausCore/src/Config.h +++ b/libraries/KlubhausCore/src/Config.h @@ -22,6 +22,11 @@ #define HINT_MIN_BRIGHTNESS 30 #define HINT_MAX_BRIGHTNESS 60 +// ── Loop yield (prevents Task Watchdog on ESP32) ── +#ifndef LOOP_YIELD_MS +#define LOOP_YIELD_MS 10 +#endif + // ── WiFi credential struct (populated in each board's secrets.h) ── struct WiFiCred { const char* ssid; diff --git a/libraries/KlubhausCore/src/DoorbellLogic.cpp b/libraries/KlubhausCore/src/DoorbellLogic.cpp index 43e9aa0..6839bbe 100644 --- a/libraries/KlubhausCore/src/DoorbellLogic.cpp +++ b/libraries/KlubhausCore/src/DoorbellLogic.cpp @@ -26,12 +26,22 @@ void DoorbellLogic::begin( Serial.println(F(" *** DEBUG MODE — _test topics ***")); Serial.println(F("========================================\n")); - // Display + // Stage 1: Display init + setBootStage(BootStage::INIT_DISPLAY); + { + ScreenState temp; + temp.bootStage = BootStage::SPLASH; + _display->render(temp); + } _display->begin(); + delay(LOOP_YIELD_MS); - // Network + // Stage 2: Network init + setBootStage(BootStage::INIT_NETWORK); _net.begin(creds, credCount); + // Stage 3: WiFi connect + setBootStage(BootStage::CONNECTING_WIFI); if(_net.isConnected()) { _net.syncNTP(); Serial.printf("[NET] WiFi:%s RSSI:%d IP:%s\n", _net.getSSID().c_str(), _net.getRSSI(), @@ -51,9 +61,16 @@ void DoorbellLogic::begin( Serial.printf("[CONFIG] SILENCE_URL: %s\n", _silenceUrl.c_str()); Serial.printf("[CONFIG] ADMIN_URL: %s\n", _adminUrl.c_str()); + // Stage 4: Ready + setBootStage(BootStage::READY); + delay(LOOP_YIELD_MS); + // Boot status flushStatus(String("BOOTED — ") + _net.getSSID() + " " + _net.getIP() + " RSSI:" + String(_net.getRSSI())); + + // Stage 5: Done (transition to OFF happens in finishBoot) + setBootStage(BootStage::DONE); } void DoorbellLogic::finishBoot() { @@ -304,3 +321,49 @@ int DoorbellLogic::handleTouch(const TouchEvent& evt) { return -1; } + +// ── Hold gesture for silencing ───────────────────────────────── + +bool DoorbellLogic::updateHold(const TouchEvent& evt) { + if(_state.deviceState != DeviceState::ALERTING) + return false; + + static int holdStartX = -1; + static int holdStartY = -1; + + HoldState h = _display->updateHold(HOLD_TO_SILENCE_MS); + + if(h.completed) { + silenceAlert(); + holdStartX = -1; + holdStartY = -1; + return true; + } + + if(h.started) { + holdStartX = evt.x; + holdStartY = evt.y; + } + + if(holdStartX >= 0) { + _display->updateHint(holdStartX, holdStartY, h.active); + } + + return false; +} + +// ── Serial console helper ─────────────────────────────────────── + +void DoorbellLogic::processSerial() { + if(Serial.available()) { + String cmd = Serial.readStringUntil('\n'); + cmd.trim(); + if(cmd.length() > 0) + onSerialCommand(cmd); + } +} + +void DoorbellLogic::setBootStage(BootStage stage) { + _state.bootStage = stage; + _display->render(_state); +} diff --git a/libraries/KlubhausCore/src/DoorbellLogic.h b/libraries/KlubhausCore/src/DoorbellLogic.h index 85311ae..c5a98ee 100644 --- a/libraries/KlubhausCore/src/DoorbellLogic.h +++ b/libraries/KlubhausCore/src/DoorbellLogic.h @@ -19,6 +19,8 @@ public: void finishBoot(); /// Serial debug console. void onSerialCommand(const String& cmd); + /// Process Serial input — call each loop iteration. + void processSerial(); const ScreenState& getScreenState() const { return _state; } @@ -27,6 +29,12 @@ public: void setScreen(ScreenID s); /// Handle touch input — returns dashboard tile index if tapped, or -1. int handleTouch(const TouchEvent& evt); + /// Handle hold gesture for silencing — call each loop iteration when alerting. + /// Returns true if hold completed and alert was silenced. + bool updateHold(const TouchEvent& evt); + + /// Set current boot stage (for staged boot sequence). + void setBootStage(BootStage stage); private: void pollTopics(); diff --git a/libraries/KlubhausCore/src/ScreenState.h b/libraries/KlubhausCore/src/ScreenState.h index 0108302..8581551 100644 --- a/libraries/KlubhausCore/src/ScreenState.h +++ b/libraries/KlubhausCore/src/ScreenState.h @@ -5,9 +5,12 @@ enum class DeviceState { BOOTED, SILENT, ALERTING, SILENCED }; enum class ScreenID { BOOT, OFF, ALERT, DASHBOARD }; +enum class BootStage { SPLASH, INIT_DISPLAY, INIT_NETWORK, CONNECTING_WIFI, READY, DONE }; + struct ScreenState { DeviceState deviceState = DeviceState::BOOTED; ScreenID screen = ScreenID::BOOT; + BootStage bootStage = BootStage::SPLASH; String alertTitle; String alertBody; @@ -52,3 +55,21 @@ inline const char* screenIdStr(ScreenID s) { } return "?"; } + +inline const char* bootStageStr(BootStage s) { + switch(s) { + case BootStage::SPLASH: + return "SPLASH"; + case BootStage::INIT_DISPLAY: + return "INIT_DISPLAY"; + case BootStage::INIT_NETWORK: + return "INIT_NETWORK"; + case BootStage::CONNECTING_WIFI: + return "CONNECTING_WIFI"; + case BootStage::READY: + return "READY"; + case BootStage::DONE: + return "DONE"; + } + return "?"; +} diff --git a/mise.toml b/mise.toml index 78eaf6b..9e708d4 100644 --- a/mise.toml +++ b/mise.toml @@ -2,6 +2,11 @@ # Klubhaus Doorbell — Multi-Target Build Harness # ═══════════════════════════════════════════════════════════ +# Required tools +[tools] +hk = "latest" +pkl = "latest" + # Usage: # BOARD=esp32-32e-4 mise run compile # compile for esp32-32e-4 # BOARD=esp32-32e-4 mise run upload # upload to esp32-32e-4 @@ -244,3 +249,6 @@ clang-format -i --style=file \ libraries/KlubhausCore/src/*.h \ libraries/KlubhausCore/*.properties """ + +[env] +BOARD = "esp32-32e-4"