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