Project_SylDa/docs/superpowers/plans/2026-08-04-client-ble-scan.md
eddy 3c87990a9c
All checks were successful
CI / build (push) Successful in 1m14s
fix(client): connectTimeout in ms, NVS addr byte order, onScanEnd callback
- setConnectTimeout(5000ms) instead of 10ms (NimBLE-Arduino v2 uses ms)
- Fix NVS stored address: use ble_addr_t struct constructor to avoid
  double byte-reversal, and correct type BLE_ADDR_PUBLIC
- Use onScanEnd callback to defer connect until scan fully terminated
- Separate foundAddr to avoid race between scan callback and loop
2026-08-04 01:00:35 +02:00

12 KiB

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:

#define SCAN_INTERVAL 6000
#define SCAN_TIMEOUT  5000   // <-- verwijder deze regel

Vervang door:

#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"):

#define NVS_ADDR_NAME   "addr"

Voeg toe onder bool allProvisioned = false; (lijn 42):

bool serverAddrStored = false;
NimBLEAddress storedServerAddr = NimBLEAddress("");

Voeg toe onder static bool serverFound = false; (lijn 63):

static bool scanning = false;
static uint32_t scanStarted = 0;
static uint32_t disconnectTime = 0;
  • Step 3: Bouw en verifieer
pio run -e s3lite-client

Expected: Compileert (gebruikt nog niet, maar geen syntaxfouten).

  • Step 4: Commit
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:

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
pio run -e s3lite-client

Expected: Compileert.

  • Step 3: Commit
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():

    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):

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:

    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:

    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
pio run -e s3lite-client

Expected: Compileert.

  • Step 6: Commit
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
pio run -e s3lite-client

Expected: Compileert zonder warnings over ongebruikte variabelen.

  • Step 5: Commit
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
pio run -e s3lite-client
  • Step 2: Flash client ESP32

Sluit de client USB aan, check poort:

ls /dev/serial/by-id/ | grep espressif

Flash:

pio run -e s3lite-client -t upload --upload-port /dev/ttyACM<client_poort>
  • Step 3: Monitor serial output
cat /dev/ttyACM<client_poort>

Expected: BLE: scanning started, daarna BLE: server found at <mac>, 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
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
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.