- 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
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 meerscan-logica: nieuwe statemachine inloop()— scannen → gevonden → connectToServer() → herconnectieMyScanCallbacks: uitgebreid met UUID-prefix matching uit manufacturer data- NVS:
server_addrkey voor opgeslagen BLE-adres (48-bit MAC als 6 bytes, little-endian inNimBLEAddressformaat) - Dode code:
SCAN_TIMEOUTdefine (ongebruikt, lijn 34) enSCAN_INTERVALvervangen door nieuwe constanten
Wat blijft:
connectToServer()(lijn 239-293): ongewijzigd, hier gebeurt UUID-verify + ECDHperformEcdhHandshake()(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
MyScanCallbacksinstantiatie inonResult
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.