Compare commits

...
Author SHA1 Message Date
eddy
6d027fd35b chore: bump server v16, client v2
All checks were successful
CI / firmware (pull_request) Successful in 1m1s
CI / android (pull_request) Successful in 48s
2026-08-02 18:21:05 +02:00
eddy
ebaaa8cc4b feat(client): uniform LED signaling — off after 60s stable connection 2026-08-02 18:20:55 +02:00
eddy
d9ac8a578b feat(server): uniform LED signaling — off after 60s stable connection 2026-08-02 18:20:30 +02:00
eddy
ff99fe0a90 docs: LED stable connection implementation plan (#4) 2026-08-02 18:19:20 +02:00
eddy
bb9701dac5 docs: LED stable connection design spec (#4) 2026-08-02 18:15:09 +02:00
5 changed files with 472 additions and 23 deletions

View file

@ -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.

View file

@ -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"

View file

@ -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) {

View file

@ -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);

3
src/version.h Normal file
View file

@ -0,0 +1,3 @@
// Auto-generated from version.txt — DO NOT EDIT
#define FIRMWARE_VERSION_SERVER "16"
#define FIRMWARE_VERSION_CLIENT "2"