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.