diff --git a/android/app/src/main/assets/firmware/client/firmware.bin b/android/app/src/main/assets/firmware/client/firmware.bin index 2dd9d81..edd2861 100644 Binary files a/android/app/src/main/assets/firmware/client/firmware.bin and b/android/app/src/main/assets/firmware/client/firmware.bin differ diff --git a/docs/superpowers/plans/2026-08-04-client-ble-scan.md b/docs/superpowers/plans/2026-08-04-client-ble-scan.md new file mode 100644 index 0000000..b901d4a --- /dev/null +++ b/docs/superpowers/plans/2026-08-04-client-ble-scan.md @@ -0,0 +1,400 @@ +# Client BLE scan-based connection Implementation Plan + +> **Voor 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:** Client verbindt even snel en betrouwbaar als de Android app — via BLE-scan met UUID-prefix matching + ECDH-handshake via de bestaande `connectToServer()` functie. + +**Architecture:** Vervang hardcoded MAC-connectie door BLE-scan (service UUID + manufacturer data UUID-prefix), bewaar gevonden server-MAC in NVS voor snelle herconnectie, roep de reeds correcte `connectToServer()` aan voor UUID-verificatie + ECDH. + +**Tech Stack:** ESP32-S3, NimBLE-Arduino, Preferences (NVS) + +--- + +## Scope + +Één bestand: `src/client.cpp`. Geen nieuwe bestanden. + +**Wat verandert er:** +- `setup()`: alleen BLE init, geen direct connect meer +- `scan`-logica: nieuwe statemachine in `loop()` — scannen → gevonden → connectToServer() → herconnectie +- `MyScanCallbacks`: uitgebreid met UUID-prefix matching uit manufacturer data +- NVS: `server_addr` key voor opgeslagen BLE-adres (48-bit MAC als 6 bytes, little-endian in `NimBLEAddress` formaat) +- Dode code: `SCAN_TIMEOUT` define (ongebruikt, lijn 34) en `SCAN_INTERVAL` vervangen door nieuwe constanten + +**Wat blijft:** +- `connectToServer()` (lijn 239-293): ongewijzigd, hier gebeurt UUID-verify + ECDH +- `performEcdhHandshake()` (lijn 158-237): ongewijzigd, hier gebeurt de ECDH-handshake +- Alle serial-command handlers, touch-logica, relay-command sending + +--- + +### Task 1: Definieer nieuwe constanten en voeg NVS-key + globals toe + +**File:** `src/client.cpp` + +- [ ] **Step 1: Vervang ongebruikte constanten en voeg nieuwe toe** + +Verwijder `#define SCAN_TIMEOUT 5000` (ongebruikt) en voeg nieuwe constanten toe direct eronder: + +```cpp +#define SCAN_INTERVAL 6000 +#define SCAN_TIMEOUT 5000 // <-- verwijder deze regel +``` + +Vervang door: + +```cpp +#define SCAN_DURATION_MS 5000 // hoe lang scannen per poging +#define SCAN_INTERVAL_MS 8000 // tijd tussen scanpogingen +#define RECONNECT_DELAY_MS 1000 // wachttijd na disconnect voor herscan +``` + +- [ ] **Step 2: Voeg NVS-key en globals toe na `#define SCAN_INTERVAL` (lijn 33)** + +Voeg toe onder lijn 35 (`#define NVS_SERVER_NAME "server"`): + +```cpp +#define NVS_ADDR_NAME "addr" +``` + +Voeg toe onder `bool allProvisioned = false;` (lijn 42): + +```cpp +bool serverAddrStored = false; +NimBLEAddress storedServerAddr = NimBLEAddress(""); +``` + +Voeg toe onder `static bool serverFound = false;` (lijn 63): + +```cpp +static bool scanning = false; +static uint32_t scanStarted = 0; +static uint32_t disconnectTime = 0; +``` + +- [ ] **Step 3: Bouw en verifieer** + +```bash +pio run -e s3lite-client +``` + +Expected: Compileert (gebruikt nog niet, maar geen syntaxfouten). + +- [ ] **Step 4: Commit** + +```bash +git add src/client.cpp +git commit -m "feat(client): add scan constants, NVS addr key, and state globals" +``` + +--- + +### Task 2: Breid MyScanCallbacks uit met UUID-prefix matching + +**File:** `src/client.cpp` + +- [ ] **Step 1: Vervang MyScanCallbacks (lijn 70-78)** + +Vervang de bestaande klasse: + +```cpp +class MyScanCallbacks : public NimBLEScanCallbacks { + void onResult(const NimBLEAdvertisedDevice *advertisedDevice) override { + if (scanning && advertisedDevice->isAdvertisingService(NimBLEUUID(SERVICE_UUID))) { + std::string mfgData = advertisedDevice->getManufacturerData(); + if (mfgData.length() >= 4) { + if (memcmp(mfgData.data(), deviceUuid, 4) == 0) { + serverAddress = advertisedDevice->getAddress(); + serverFound = true; + ESP_LOGI(TAG, "BLE: server found at %s", serverAddress.toString().c_str()); + NimBLEDevice::getScan()->stop(); + } + } + } + } +}; +``` + +De UUID-prefix check (`memcmp(mfgData.data(), deviceUuid, 4)`) matcht exact met Android's `scanRecord.getManufacturerSpecificData(0xFFFF)` + `advPrefix.contentEquals(storedPrefix)` (BleRelayManager.kt lijn 122-129). + +- [ ] **Step 2: Bouw en verifieer** + +```bash +pio run -e s3lite-client +``` + +Expected: Compileert. + +- [ ] **Step 3: Commit** + +```bash +git add src/client.cpp +git commit -m "feat(client): extend MyScanCallbacks with UUID prefix matching" +``` + +--- + +### Task 3: scan + connect statemachine in loop() + +**File:** `src/client.cpp` + +- [ ] **Step 1: Herschrijf setup() lijn 346-363 — alleen BLE init, geen connect** + +Vervang het `if (allProvisioned)` blok in `setup()`: + +```cpp + if (allProvisioned) { + NimBLEDevice::init("SylDa_Client"); + bleReady = true; + + preferences.begin(NVS_NAMESPACE, true); + uint8_t addrBuf[6]; + size_t addrLen = preferences.getBytes(NVS_ADDR_NAME, addrBuf, 6); + preferences.end(); + if (addrLen == 6) { + storedServerAddr = NimBLEAddress(addrBuf, BLE_ADDR_RANDOM); + serverAddrStored = true; + ESP_LOGI(TAG, "BLE: stored server address %s", storedServerAddr.toString().c_str()); + } + + ESP_LOGI(TAG, "BLE: initialized, will scan for server"); + } +``` + +- [ ] **Step 2: Voeg scan-start functie toe vóór loop()** + +Voeg toe direct vóór `void loop()` (vóór lijn 368): + +```cpp +static void startScan() { + if (scanning) return; + NimBLEScan *pScan = NimBLEDevice::getScan(); + pScan->setScanCallbacks(new MyScanCallbacks()); + pScan->setActiveScan(true); + pScan->setInterval(32); + pScan->setWindow(16); + serverFound = false; + scanning = true; + scanStarted = millis(); + pScan->start(SCAN_DURATION_MS / 100, false); // duration in 10ms units + ESP_LOGI(TAG, "BLE: scanning started"); +} +``` + +- [ ] **Step 3: Vervang het reconnect-blok in loop() (lijn 550-568)** + +Vervang het `if (!connected && bleReady)` blok met de volledige scan+connect statemachine. Het oude blok (lijn 550-568) en de `statusLog` log (lijn 541-548) worden vervangen: + +```cpp + static uint32_t lastScanAttempt = 0; + static uint32_t statusLog = 0; + + if (bleReady && millis() - statusLog > 2000) { + ESP_LOGI(TAG, "BLE: connected=%d scanning=%d ecdh=%d serverFound=%d", + connected, scanning, ecdhReady, serverFound); + statusLog = millis(); + } + + if (!connected && bleReady) { + if (!scanning) { + uint32_t delayNeeded = disconnectTime > 0 ? RECONNECT_DELAY_MS : 0; + if (millis() - lastScanAttempt > SCAN_INTERVAL_MS) { + if (serverAddrStored) { + ESP_LOGI(TAG, "BLE: trying stored address %s", storedServerAddr.toString().c_str()); + if (pClient == nullptr) { + pClient = NimBLEDevice::createClient(); + pClient->setConnectTimeout(10); + pClient->setClientCallbacks(&clientCallbacks); + } + if (pClient->connect(storedServerAddr)) { + serverAddress = storedServerAddr; + if (connectToServer()) { + connected = true; + ESP_LOGI(TAG, "BLE: connected via stored address"); + } + } else { + ESP_LOGI(TAG, "BLE: stored address failed, starting scan"); + serverAddrStored = false; + startScan(); + } + } else { + startScan(); + } + lastScanAttempt = millis(); + } + } else { + if (serverFound) { + scanning = false; + ESP_LOGI(TAG, "BLE: connecting to found server at %s", serverAddress.toString().c_str()); + if (pClient != nullptr && pClient->isConnected()) { + pClient->disconnect(); + delay(100); + } + pClient = NimBLEDevice::createClient(); + pClient->setConnectTimeout(10); + pClient->setClientCallbacks(&clientCallbacks); + if (connectToServer()) { + connected = true; + uint8_t addrBuf[6]; + memcpy(addrBuf, serverAddress.getNative(), 6); + preferences.begin(NVS_NAMESPACE, false); + preferences.putBytes(NVS_ADDR_NAME, addrBuf, 6); + preferences.end(); + storedServerAddr = serverAddress; + serverAddrStored = true; + ESP_LOGI(TAG, "BLE: server address stored in NVS"); + } else { + ESP_LOGW(TAG, "BLE: connectToServer failed"); + } + } else if (millis() - scanStarted > SCAN_DURATION_MS) { + NimBLEDevice::getScan()->stop(); + scanning = false; + ESP_LOGI(TAG, "BLE: scan timeout, no server found"); + } + } + } +``` + +- [ ] **Step 4: Update disconnect handler — zet disconnectTime** + +In `onDisconnect` (lijn 85-93), voeg `disconnectTime = millis();` toe: + +```cpp + void onDisconnect(NimBLEClient *client, int reason) override { + connected = false; + disconnectTime = millis(); + relay1SendState = false; + relay2SendState = false; + pCmdChar = nullptr; + ecdhReady = false; + memset(aesKey, 0, AES_KEY_LEN); + ESP_LOGW(TAG, "BLE: disconnected (reason=%d)", reason); + } +``` + +- [ ] **Step 5: Bouw en verifieer** + +```bash +pio run -e s3lite-client +``` + +Expected: Compileert. + +- [ ] **Step 6: Commit** + +```bash +git add src/client.cpp +git commit -m "feat(client): replace hardcoded connect with scan-based discovery + connectToServer()" +``` + +--- + +### Task 4: Verwijder dode code + +**File:** `src/client.cpp` + +- [ ] **Step 1: Verwijder ongebruikte defines** + +Verwijder lijn 34 (`#define SCAN_TIMEOUT 5000`) als dat nog niet gebeurd is. + +- [ ] **Step 2: Verwijder ongebruikte variabele `lastConnectAttempt`** + +De oude `static uint32_t lastConnectAttempt = 0;` in `loop()` (lijn 541) is niet meer nodig — vervangen door `lastScanAttempt` in de nieuwe code. + +- [ ] **Step 3: Verwijder ongebruikte `MyScanCallbacks` instantiatie in `onResult`** + +De klasse `MyScanCallbacks` moet nu een constructor hebben of via `new` aangemaakt worden in `startScan()`. Check: NimBLE verwacht raw pointer, we gebruiken `new` (geheugenlek is acceptabel — callbacks leven de hele uptime). + +- [ ] **Step 4: Bouw en verifieer** + +```bash +pio run -e s3lite-client +``` + +Expected: Compileert zonder warnings over ongebruikte variabelen. + +- [ ] **Step 5: Commit** + +```bash +git add src/client.cpp +git commit -m "chore(client): remove dead code, unused defines and variables" +``` + +--- + +### Task 5: Bouw, flash, test + +- [ ] **Step 1: Bouw** + +```bash +pio run -e s3lite-client +``` + +- [ ] **Step 2: Flash client ESP32** + +Sluit de client USB aan, check poort: + +```bash +ls /dev/serial/by-id/ | grep espressif +``` + +Flash: + +```bash +pio run -e s3lite-client -t upload --upload-port /dev/ttyACM +``` + +- [ ] **Step 3: Monitor serial output** + +```bash +cat /dev/ttyACM +``` + +Expected: `BLE: scanning started`, daarna `BLE: server found at `, `BLE: server UUID verified, ECDH done, ready`. + +- [ ] **Step 4: Test relay commands** + +Druk op touch-sensors op de client. Verifieer dat relais op server schakelen. + +- [ ] **Step 5: Test herconnectie** + +Zet server uit en weer aan. Verifieer dat client automatisch herverbindt na server weer adverteert — via opgeslagen adres (snel) of via new scan. + +- [ ] **Step 6: Kopieer firmware naar Android assets** + +```bash +cp .pio/build/s3lite-client/bootloader.bin .pio/build/s3lite-client/partitions.bin .pio/build/s3lite-client/firmware.bin android/app/src/main/assets/firmware/client/ +``` + +- [ ] **Step 7: Commit en push** + +```bash +git add src/client.cpp android/app/src/main/assets/firmware/client/ +git commit -m "chore: update client firmware in Android assets" +git push +``` + +--- + +## Flow overzicht + +``` +setup() + └─ BLE init, laad stored server addr uit NVS + +loop() [niet connected] + ├─ stored addr beschikbaar? → direct connect poging + │ ├─ success → connectToServer() → UUID-verify + ECDH → connected! + │ └─ fail → startScan() + └─ geen stored addr → startScan() + +loop() [scanning] + ├─ serverFound → stop scan → pClient->connect() → connectToServer() + │ └─ success → sla MAC op in NVS → connected! + └─ scan timeout → stop scan, wacht SCAN_INTERVAL_MS, herstart + +onDisconnect() + └─ zet disconnectTime, cleanup state → herstart scan na RECONNECT_DELAY_MS +``` + +Dit is exact wat de Android app doet: scan → UUID-prefix match → connect → UUID-verify → ECDH. diff --git a/src/client.cpp b/src/client.cpp index 22ae341..99a4d99 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -66,6 +66,7 @@ bool otaEnded = false; static bool bleReady = false; static bool serverFound = false; static NimBLEAddress serverAddress; +static NimBLEAddress foundAddr; static bool scanning = false; static uint32_t scanStarted = 0; static uint32_t disconnectTime = 0; @@ -78,16 +79,19 @@ class MyScanCallbacks : public NimBLEScanCallbacks { void onResult(const NimBLEAdvertisedDevice *advertisedDevice) override { if (scanning && advertisedDevice->isAdvertisingService(NimBLEUUID(SERVICE_UUID))) { std::string mfgData = advertisedDevice->getManufacturerData(); - if (mfgData.length() >= 4) { - if (memcmp(mfgData.data(), deviceUuid, 4) == 0) { - serverAddress = advertisedDevice->getAddress(); + if (mfgData.length() >= 6) { + if (memcmp(mfgData.data() + 2, serverUuid, 4) == 0) { + foundAddr = advertisedDevice->getAddress(); serverFound = true; - ESP_LOGI(TAG, "BLE: server found at %s", serverAddress.toString().c_str()); - NimBLEDevice::getScan()->stop(); + ESP_LOGI(TAG, "BLE: server found at %s", foundAddr.toString().c_str()); } } } } + void onScanEnd(const NimBLEScanResults &results, int reason) override { + scanning = false; + ESP_LOGI(TAG, "BLE: scan ended (reason=%d, found=%d)", reason, serverFound); + } }; class MyClientCallbacks : public NimBLEClientCallbacks { @@ -253,7 +257,7 @@ static bool performEcdhHandshake(NimBLERemoteService *pSvc) { static bool connectToServer() { if (pClient == nullptr) { pClient = NimBLEDevice::createClient(); - pClient->setConnectTimeout(10); + pClient->setConnectTimeout(5000); pClient->setClientCallbacks(&clientCallbacks); } @@ -366,7 +370,10 @@ void setup() { size_t addrLen = preferences.getBytes(NVS_ADDR_NAME, addrBuf, 6); preferences.end(); if (addrLen == 6) { - storedServerAddr = NimBLEAddress(addrBuf, BLE_ADDR_RANDOM); + ble_addr_t addr; + addr.type = BLE_ADDR_PUBLIC; + memcpy(addr.val, addrBuf, 6); + storedServerAddr = NimBLEAddress(addr); serverAddrStored = true; ESP_LOGI(TAG, "BLE: stored server address %s", storedServerAddr.toString().c_str()); } @@ -574,7 +581,24 @@ void loop() { } if (!connected && bleReady) { - if (!scanning) { + if (serverFound && !scanning) { + serverFound = false; + serverAddress = foundAddr; + ESP_LOGI(TAG, "BLE: connecting to %s type=%d", serverAddress.toString().c_str(), serverAddress.getType()); + if (connectToServer()) { + connected = true; + uint8_t addrBuf[6]; + memcpy(addrBuf, serverAddress.getVal(), 6); + preferences.begin(NVS_NAMESPACE, false); + preferences.putBytes(NVS_ADDR_NAME, addrBuf, 6); + preferences.end(); + storedServerAddr = serverAddress; + serverAddrStored = true; + ESP_LOGI(TAG, "BLE: server address stored in NVS"); + } else { + ESP_LOGW(TAG, "BLE: connectToServer failed"); + } + } else if (!scanning) { if (millis() - lastScanAttempt > SCAN_INTERVAL_MS) { if (serverAddrStored) { ESP_LOGI(TAG, "BLE: trying stored address %s", storedServerAddr.toString().c_str()); @@ -593,30 +617,7 @@ void loop() { lastScanAttempt = millis(); } } else { - if (serverFound) { - scanning = false; - ESP_LOGI(TAG, "BLE: connecting to found server at %s", serverAddress.toString().c_str()); - if (pClient != nullptr && pClient->isConnected()) { - pClient->disconnect(); - delay(100); - } - pClient = NimBLEDevice::createClient(); - pClient->setConnectTimeout(10); - pClient->setClientCallbacks(&clientCallbacks); - if (connectToServer()) { - connected = true; - uint8_t addrBuf[6]; - memcpy(addrBuf, serverAddress.getVal(), 6); - preferences.begin(NVS_NAMESPACE, false); - preferences.putBytes(NVS_ADDR_NAME, addrBuf, 6); - preferences.end(); - storedServerAddr = serverAddress; - serverAddrStored = true; - ESP_LOGI(TAG, "BLE: server address stored in NVS"); - } else { - ESP_LOGW(TAG, "BLE: connectToServer failed"); - } - } else if (millis() - scanStarted > SCAN_DURATION_MS) { + if (millis() - scanStarted > SCAN_DURATION_MS) { NimBLEDevice::getScan()->stop(); scanning = false; ESP_LOGI(TAG, "BLE: scan timeout, no server found");