From bb9701dac506b453be0a8f5ca3f04e7a370cbaae Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 2 Aug 2026 18:15:09 +0200 Subject: [PATCH 1/5] docs: LED stable connection design spec (#4) --- ...2026-08-02-led-stable-connection-design.md | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-02-led-stable-connection-design.md diff --git a/docs/superpowers/specs/2026-08-02-led-stable-connection-design.md b/docs/superpowers/specs/2026-08-02-led-stable-connection-design.md new file mode 100644 index 0000000..04ff8e0 --- /dev/null +++ b/docs/superpowers/specs/2026-08-02-led-stable-connection-design.md @@ -0,0 +1,97 @@ +# LED-signalering: stabiele verbinding + +**Issue:** [#4](https://git.eddydevink.nl/eddy/Project_SylDa/issues/4) — Leds mogen uit na een minuut stabiele verbinding +**Datum:** 2026-08-02 +**Status:** goedgekeurd + +## Doel + +Uniforme LED-signalering voor zowel server als client. De LED dient als debug-indicator tijdens opstarten en bij verbindingsproblemen. Na 60 seconden continue stabiele verbinding schakelt de LED uit. + +## Kleurenschema + +Identiek voor server (`main.cpp`) en client (`client.cpp`): + +| State | LED | Prioriteit | +|-------|-----|-----------| +| OTA in progress | Groen knipperend (100ms interval) | 1 (hoogst) | +| OTA completed, waiting confirm | Geel vast | 1 | +| Geen verbinding | Rood | 2 | +| Verbinding actief (< 60s) | Geel | 3 | +| Verbinding stabiel (>= 60s) | **Uit** | 4 (laagst) | +| Relais-flash (overlay) | Groen 150ms | overlay | + +Relais-flash is een overlay: toont kort groen bij verandering in relais-status, daarna keert LED terug naar de kleur die hoort bij de huidige verbindingsstatus. + +## Timer-logica + +- Timer (`connectionStableMs`) start zodra de **eerste** verbinding tot stand komt +- `connectionStableMs` telt op zolang er minstens 1 verbinding is (connectedCount > 0 voor server, connected == true voor client) +- Nieuwe verbindingen die later bijkomen resetten de timer **niet** +- Bij disconnect van een verbinding terwijl er nog andere verbindingen zijn: timer loopt door +- Pas als **alle** verbindingen wegvallen: `connectionStableMs` = 0, LED direct rood +- Drempel: `STABLE_LED_TIMEOUT_MS = 60000` + +### Server-specifiek + +- `connectedCount`: aantal actieve BLE-clients (0-2) +- Timer loopt als `connectedCount > 0` +- Alle clients disconnect = reset + +### Client-specifiek + +- `connected`: boolean, of client verbonden is met server +- Timer loopt als `connected == true` +- Disconnect = reset + +## Relais-flash mechanisme + +- Detectie: vergelijk huidige relais-state met vorige loop-iteratie +- Bij verandering: zet `relayFlashUntil = millis() + 150` +- Zolang `millis() < relayFlashUntil`: LED groen (overschrijft verbindingskleur) +- Flash reset de 60s timer **niet** +- **Server:** detecteert verandering in `relay1State || relay2State` (combinatie van touch, button, en BLE-commands) +- **Client:** detecteert verandering in `relay1State || relay2State` (alleen touch/button) + +## OTA (beide firmwares) + +OTA heeft hoogste prioriteit en override alle andere LED-states: + +- **OTA in progress** (`otaInProgress == true`): groen knipperend, 100ms interval +- **OTA completed, waiting confirm** (`otaEnded == true && !otaConfirmed`): geel vast +- **OTA confirm timeout** of **OTA error**: valt terug naar normale verbindings-LED +- **OTA success + reboot**: groen vast, dan reboot + +Client heeft OTA-logica nog niet geïmplementeerd, maar de LED-code wordt alvast voorbereid met dezelfde OTA-override structuur zodat toekomstige OTA-implementatie direct past. + +## LED-update volgorde in loop() + +``` +1. Bepaal base-kleur op basis van verbindingsstatus (rood/geel/uit) +2. Controleer relais-flash: als actief -> groen +3. Controleer OTA-override: als actief -> OTA-kleur +4. FastLED.show() +``` + +## Wijzigingen per bestand + +### `src/main.cpp` (server) + +- Toevoegen: `STABLE_LED_TIMEOUT_MS`, `connectionStableMs`, `relayFlashUntil`, `lastRelayActive` +- Aanpassen: LED-logica in `loop()` — base-kleur uit `connectionStableMs` i.p.v. `connectedCount > 0` +- Toevoegen: relais-flash detectie +- Behouden: OTA LED-override (werkt al, prioriteit blijft hoogst) +- Verwijderen: setup LED-blinks (drie groene knipperingen) — vervangen door rood (geen verbinding) + +### `src/client.cpp` (client) + +- Toevoegen: `STABLE_LED_TIMEOUT_MS`, `connectionStableMs`, `relayFlashUntil`, `lastRelayActive` +- Aanpassen: LED-logica in `loop()` — base-kleur uit `connectionStableMs`, geen aparte relais-kleur meer +- Toevoegen: relais-flash detectie +- Toevoegen: OTA-override structuur (placeholder, `otaInProgress` default `false`) +- Setup: LED rood i.p.v. direct rood (blijft rood, maar nu als onderdeel van uniform schema) + +### `src/version.h` + +- Server versie bump naar "16" +- Client versie bump naar "2" From ff99fe0a900d6e67b818a215f5c11d4a039db9b6 Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 2 Aug 2026 18:19:20 +0200 Subject: [PATCH 2/5] docs: LED stable connection implementation plan (#4) --- .../plans/2026-08-02-led-stable-connection.md | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-02-led-stable-connection.md diff --git a/docs/superpowers/plans/2026-08-02-led-stable-connection.md b/docs/superpowers/plans/2026-08-02-led-stable-connection.md new file mode 100644 index 0000000..17ecb21 --- /dev/null +++ b/docs/superpowers/plans/2026-08-02-led-stable-connection.md @@ -0,0 +1,292 @@ +# LED Stable Connection Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Uniform LED signaling for server and client — LED acts as debug indicator, turns off after 60s stable connection. + +**Architecture:** Shared LED state machine: OTA overrides all, then relay-flash overlay, then connection-status base color (Red=disconnected, Yellow=connected<60s, Off=stable>=60s). Both firmwares use identical logic. + +**Tech Stack:** C++/Arduino, FastLED, NimBLE, ESP32-S3 (PlatformIO) + +--- + +## File Map + +| File | Action | Purpose | +|------|--------|---------| +| `src/main.cpp` | Modify | Server LED logic, new globals, remove setup blinks | +| `src/client.cpp` | Modify | Client LED logic, new globals, OTA placeholders | +| `src/version.h` | Modify | Version bumps (server 15→16, client 1→2) | + +--- + +### Task 1: Server LED logic + +**Files:** +- Modify: `src/main.cpp` + +- [ ] **Step 1: Add new global variables after line 56 (after `int connectedCount = 0;`)** + +```cpp +#define STABLE_LED_TIMEOUT_MS 60000 +uint32_t connectionStableMs = 0; +uint32_t relayFlashUntil = 0; +``` + +- [ ] **Step 2: Remove `ledOtaBusy()` function (lines 181-183)** + +Delete this function — it will be inlined in the new LED logic: + +```cpp +static void ledOtaBusy() { + leds[0] = (millis() / 100) % 2 ? CRGB::Green : CRGB::Black; +} +``` + +Remove all 3 lines. + +- [ ] **Step 3: Replace setup LED blinks (lines 414-422) with solid red** + +Replace: + +```cpp + for (int i = 0; i < 3; i++) { + leds[0] = CRGB::Green; + FastLED.show(); + delay(200); + leds[0] = CRGB::Black; + FastLED.show(); + delay(200); + } + Serial.println("LED blink done, starting BLE..."); +``` + +With: + +```cpp + leds[0] = CRGB::Red; + FastLED.show(); + Serial.println("LED red (no connection), starting BLE..."); +``` + +- [ ] **Step 4: Remove `leds[0] = CRGB::Yellow;` from otaReady handler (line 553)** + +In the otaReady block (lines 542-558), remove line 553 `leds[0] = CRGB::Yellow;`. The wait-for-confirm LED is now handled by the main LED logic checking `otaInProgress && otaEnded`. + +- [ ] **Step 5: Replace the LED block in loop() (lines 599-615) with new logic** + +Replace lines 599-615: + +```cpp + if (!otaInProgress) { + relay1State = t1Active || btnActive || + (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]); + relay2State = t2Active || + (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]); + + leds[0] = (connectedCount > 0) ? CRGB::Green : CRGB::Black; + } else { + relay1State = false; + relay2State = false; + ledOtaBusy(); + } + + digitalWrite(RELAY1_PIN, relay1State ? HIGH : LOW); + digitalWrite(RELAY2_PIN, relay2State ? HIGH : LOW); + + FastLED.show(); +``` + +With: + +```cpp + if (!otaInProgress) { + relay1State = t1Active || btnActive || + (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]); + relay2State = t2Active || + (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]); + } else { + relay1State = false; + relay2State = false; + } + + // --- LED logic --- + CRGB ledColor; + + if (otaInProgress) { + ledColor = otaEnded + ? CRGB::Yellow + : (((millis() / 100) % 2) ? CRGB::Green : CRGB::Black); + } else { + if (connectedCount > 0) { + if (connectionStableMs == 0) connectionStableMs = millis(); + } else { + connectionStableMs = 0; + } + + if (connectedCount > 0) { + ledColor = (millis() - connectionStableMs >= STABLE_LED_TIMEOUT_MS) + ? CRGB::Black + : CRGB::Yellow; + } else { + ledColor = CRGB::Red; + } + + static bool lastRelayActive = false; + bool relayActive = relay1State || relay2State; + if (relayActive != lastRelayActive) { + relayFlashUntil = millis() + 150; + } + lastRelayActive = relayActive; + + if (millis() < relayFlashUntil) { + ledColor = CRGB::Green; + } + } + + leds[0] = ledColor; + + digitalWrite(RELAY1_PIN, relay1State ? HIGH : LOW); + digitalWrite(RELAY2_PIN, relay2State ? HIGH : LOW); + + FastLED.show(); +``` + +- [ ] **Step 6: Commit server changes** + +```bash +git add src/main.cpp +git commit -m "feat(server): uniform LED signaling — off after 60s stable connection" +``` + +--- + +### Task 2: Client LED logic + +**Files:** +- Modify: `src/client.cpp` + +- [ ] **Step 1: Add new global variables after line 48 (after `static uint32_t seq = 0;`)** + +```cpp +#define STABLE_LED_TIMEOUT_MS 60000 +uint32_t connectionStableMs = 0; +uint32_t relayFlashUntil = 0; +bool otaInProgress = false; +bool otaEnded = false; +``` + +- [ ] **Step 2: Replace the LED block in loop() (lines 368-375) with new logic** + +Replace lines 368-375: + +```cpp + if (relay1State || relay2State) { + leds[0] = CRGB::Green; + } else if (connected) { + leds[0] = CRGB::Blue; + } else { + leds[0] = CRGB::Red; + } + FastLED.show(); +``` + +With: + +```cpp + // --- LED logic --- + CRGB ledColor; + + if (otaInProgress) { + ledColor = otaEnded + ? CRGB::Yellow + : (((millis() / 100) % 2) ? CRGB::Green : CRGB::Black); + } else { + if (connected) { + if (connectionStableMs == 0) connectionStableMs = millis(); + } else { + connectionStableMs = 0; + } + + if (connected) { + ledColor = (millis() - connectionStableMs >= STABLE_LED_TIMEOUT_MS) + ? CRGB::Black + : CRGB::Yellow; + } else { + ledColor = CRGB::Red; + } + + static bool lastRelayActive = false; + bool relayActive = relay1State || relay2State; + if (relayActive != lastRelayActive) { + relayFlashUntil = millis() + 150; + } + lastRelayActive = relayActive; + + if (millis() < relayFlashUntil) { + ledColor = CRGB::Green; + } + } + + leds[0] = ledColor; + FastLED.show(); +``` + +- [ ] **Step 3: Commit client changes** + +```bash +git add src/client.cpp +git commit -m "feat(client): uniform LED signaling — off after 60s stable connection" +``` + +--- + +### Task 3: Version bumps + +**Files:** +- Modify: `src/version.h` + +- [ ] **Step 1: Bump versions** + +Replace: + +```cpp +#define FIRMWARE_VERSION_SERVER "15" +#define FIRMWARE_VERSION_CLIENT "1" +``` + +With: + +```cpp +#define FIRMWARE_VERSION_SERVER "16" +#define FIRMWARE_VERSION_CLIENT "2" +``` + +- [ ] **Step 2: Commit version bump** + +```bash +git add src/version.h +git commit -m "chore: bump server v16, client v2" +``` + +--- + +### Task 4: Build verification + +**Files:** +- None (build only) + +- [ ] **Step 1: Build server firmware** + +Run: `pio run -e s3lite-server` +Expected: SUCCESS, no errors. + +- [ ] **Step 2: Build client firmware** + +Run: `pio run -e s3lite-client` +Expected: SUCCESS, no errors. + +- [ ] **Step 3: Verify both targets compile cleanly** + +Run: `pio run` +Expected: Both environments compile SUCCESS. From d9ac8a578b1690b4473e2e3122fd179005430bf7 Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 2 Aug 2026 18:20:30 +0200 Subject: [PATCH 3/5] =?UTF-8?q?feat(server):=20uniform=20LED=20signaling?= =?UTF-8?q?=20=E2=80=94=20off=20after=2060s=20stable=20connection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.cpp | 60 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index ea51c1a..1b1b728 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,6 +55,10 @@ uint32_t lastSeq[MAX_CLIENTS] = {0, 0}; uint16_t connHandles[MAX_CLIENTS] = {0xFFFF, 0xFFFF}; int connectedCount = 0; +#define STABLE_LED_TIMEOUT_MS 60000 +uint32_t connectionStableMs = 0; +uint32_t relayFlashUntil = 0; + bool otaInProgress = false; esp_ota_handle_t otaHandle = 0; const esp_partition_t *otaPartition = nullptr; @@ -178,10 +182,6 @@ static void parseHexKey(const String &hex, uint8_t *out) { } } -static void ledOtaBusy() { - leds[0] = (millis() / 100) % 2 ? CRGB::Green : CRGB::Black; -} - static void otaReset() { otaInProgress = false; otaHandle = 0; @@ -411,15 +411,9 @@ void setup() { pinMode(RELAY1_PIN, OUTPUT); pinMode(RELAY2_PIN, OUTPUT); - for (int i = 0; i < 3; i++) { - leds[0] = CRGB::Green; - FastLED.show(); - delay(200); - leds[0] = CRGB::Black; - FastLED.show(); - delay(200); - } - Serial.println("LED blink done, starting BLE..."); + leds[0] = CRGB::Red; + FastLED.show(); + Serial.println("LED red (no connection), starting BLE..."); NimBLEDevice::init("SylDa_Server"); NimBLEDevice::setMTU(248); @@ -550,8 +544,6 @@ void loop() { Serial.println("OTA: esp_ota_end OK, setting otaEnded=true, waiting for 0xFF confirm"); otaEnded = true; otaEndedAt = millis(); - leds[0] = CRGB::Yellow; - FastLED.show(); } otaReady = false; Serial.printf("OTA: otaReady cleared — otaReady=%d, otaEnded=%d, otaConfirmed=%d, otaInProgress=%d\n", @@ -601,14 +593,46 @@ void loop() { (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]); relay2State = t2Active || (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]); - - leds[0] = (connectedCount > 0) ? CRGB::Green : CRGB::Black; } else { relay1State = false; relay2State = false; - ledOtaBusy(); } + CRGB ledColor; + + if (otaInProgress) { + ledColor = otaEnded + ? CRGB::Yellow + : (((millis() / 100) % 2) ? CRGB::Green : CRGB::Black); + } else { + if (connectedCount > 0) { + if (connectionStableMs == 0) connectionStableMs = millis(); + } else { + connectionStableMs = 0; + } + + if (connectedCount > 0) { + ledColor = (millis() - connectionStableMs >= STABLE_LED_TIMEOUT_MS) + ? CRGB::Black + : CRGB::Yellow; + } else { + ledColor = CRGB::Red; + } + + static bool lastRelayActive = false; + bool relayActive = relay1State || relay2State; + if (relayActive != lastRelayActive) { + relayFlashUntil = millis() + 150; + } + lastRelayActive = relayActive; + + if (millis() < relayFlashUntil) { + ledColor = CRGB::Green; + } + } + + leds[0] = ledColor; + digitalWrite(RELAY1_PIN, relay1State ? HIGH : LOW); digitalWrite(RELAY2_PIN, relay2State ? HIGH : LOW); From ebaaa8cc4b6105686077031a59fe9fdb601cf682 Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 2 Aug 2026 18:20:55 +0200 Subject: [PATCH 4/5] =?UTF-8?q?feat(client):=20uniform=20LED=20signaling?= =?UTF-8?q?=20=E2=80=94=20off=20after=2060s=20stable=20connection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client.cpp | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 59757b7..addf69a 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -47,6 +47,12 @@ static bool relay1SendState = false; static bool relay2SendState = false; static uint32_t seq = 0; +#define STABLE_LED_TIMEOUT_MS 60000 +uint32_t connectionStableMs = 0; +uint32_t relayFlashUntil = 0; +bool otaInProgress = false; +bool otaEnded = false; + static bool bleReady = false; static bool serverFound = false; static NimBLEAddress serverAddress; @@ -365,13 +371,40 @@ void loop() { bool relay1State = t1Active || btnActive; bool relay2State = t2Active; - if (relay1State || relay2State) { - leds[0] = CRGB::Green; - } else if (connected) { - leds[0] = CRGB::Blue; + CRGB ledColor; + + if (otaInProgress) { + ledColor = otaEnded + ? CRGB::Yellow + : (((millis() / 100) % 2) ? CRGB::Green : CRGB::Black); } else { - leds[0] = CRGB::Red; + if (connected) { + if (connectionStableMs == 0) connectionStableMs = millis(); + } else { + connectionStableMs = 0; + } + + if (connected) { + ledColor = (millis() - connectionStableMs >= STABLE_LED_TIMEOUT_MS) + ? CRGB::Black + : CRGB::Yellow; + } else { + ledColor = CRGB::Red; + } + + static bool lastRelayActive = false; + bool relayActive = relay1State || relay2State; + if (relayActive != lastRelayActive) { + relayFlashUntil = millis() + 150; + } + lastRelayActive = relayActive; + + if (millis() < relayFlashUntil) { + ledColor = CRGB::Green; + } } + + leds[0] = ledColor; FastLED.show(); if (connected && pCmdChar != nullptr) { From 6d027fd35b19b55fe846c6cdedd290da42116350 Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 2 Aug 2026 18:21:05 +0200 Subject: [PATCH 5/5] chore: bump server v16, client v2 --- src/version.h | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/version.h diff --git a/src/version.h b/src/version.h new file mode 100644 index 0000000..9418701 --- /dev/null +++ b/src/version.h @@ -0,0 +1,3 @@ +// Auto-generated from version.txt — DO NOT EDIT +#define FIRMWARE_VERSION_SERVER "16" +#define FIRMWARE_VERSION_CLIENT "2"