Compare commits

...
Author SHA1 Message Date
eddy
3c87990a9c fix(client): connectTimeout in ms, NVS addr byte order, onScanEnd callback
All checks were successful
CI / build (push) Successful in 1m14s
- 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
eddy
a51936f819 feat(client): scan+connect statemachine replaces hardcoded MAC 2026-08-04 00:31:38 +02:00
eddy
67979ecce6 feat(client): extend MyScanCallbacks with UUID prefix matching 2026-08-04 00:29:32 +02:00
eddy
f07d305621 feat(client): add scan constants, NVS addr key, and state globals 2026-08-04 00:29:07 +02:00
3 changed files with 493 additions and 35 deletions

View file

@ -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<client_poort>
```
- [ ] **Step 3: Monitor serial output**
```bash
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**
```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.

View file

@ -29,9 +29,11 @@ CRGB leds[NUM_LEDS];
#define NVS_PSK_NAME "psk"
#define NVS_UUID_NAME "uuid"
#define NVS_SERVER_NAME "server"
#define NVS_ADDR_NAME "addr"
#define DEBOUNCE_MS 50
#define SCAN_INTERVAL 6000
#define SCAN_TIMEOUT 5000
#define SCAN_DURATION_MS 5000
#define SCAN_INTERVAL_MS 8000
#define RECONNECT_DELAY_MS 1000
uint8_t psk[PSK_LEN];
uint8_t deviceUuid[ESP_UUID_LEN];
@ -40,6 +42,8 @@ bool pskProvisioned = false;
bool uuidProvisioned = false;
bool serverProvisioned = false;
bool allProvisioned = false;
bool serverAddrStored = false;
NimBLEAddress storedServerAddr;
Preferences preferences;
static bool connected = false;
@ -62,6 +66,10 @@ 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;
static bool ecdhNotifyReceived = false;
static uint8_t ecdhNotifyBuf[ECDH_PKT_SIZE];
@ -69,12 +77,21 @@ static size_t ecdhNotifyLen = 0;
class MyScanCallbacks : public NimBLEScanCallbacks {
void onResult(const NimBLEAdvertisedDevice *advertisedDevice) override {
if (advertisedDevice->isAdvertisingService(NimBLEUUID(SERVICE_UUID))) {
serverAddress = advertisedDevice->getAddress();
serverFound = true;
NimBLEDevice::getScan()->stop();
if (scanning && advertisedDevice->isAdvertisingService(NimBLEUUID(SERVICE_UUID))) {
std::string mfgData = advertisedDevice->getManufacturerData();
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", 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 {
@ -84,6 +101,7 @@ class MyClientCallbacks : public NimBLEClientCallbacks {
}
void onDisconnect(NimBLEClient *client, int reason) override {
connected = false;
disconnectTime = millis();
relay1SendState = false;
relay2SendState = false;
pCmdChar = nullptr;
@ -239,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);
}
@ -346,25 +364,40 @@ void setup() {
if (allProvisioned) {
NimBLEDevice::init("SylDa_Client");
bleReady = true;
ESP_LOGI(TAG, "BLE: initialized, connecting...");
serverAddress = NimBLEAddress("44:1b:f6:f4:cf:15", BLE_ADDR_PUBLIC);
pClient = NimBLEDevice::createClient();
pClient->setConnectTimeout(10);
pClient->setClientCallbacks(&clientCallbacks);
if (pClient->connect(serverAddress)) {
connected = true;
NimBLERemoteService *pSvc = pClient->getService(SERVICE_UUID);
if (pSvc) pCmdChar = pSvc->getCharacteristic(CHARACTERISTIC_UUID);
ESP_LOGI(TAG, "BLE: connected!");
} else {
ESP_LOGE(TAG, "BLE: connect failed");
preferences.begin(NVS_NAMESPACE, true);
uint8_t addrBuf[6];
size_t addrLen = preferences.getBytes(NVS_ADDR_NAME, addrBuf, 6);
preferences.end();
if (addrLen == 6) {
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());
}
ESP_LOGI(TAG, "BLE: initialized, will scan for server");
}
ESP_LOGI(TAG, "Setup done.");
}
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);
ESP_LOGI(TAG, "BLE: scanning started");
}
void loop() {
if (Serial.available()) {
static String serialBuf;
@ -538,32 +571,57 @@ void loop() {
}
}
static uint32_t lastConnectAttempt = 0;
static uint32_t lastScanAttempt = 0;
static uint32_t statusLog = 0;
if (bleReady && millis() - statusLog > 2000) {
ESP_LOGI(TAG, "BLE: connected=%d, pClient=%p, isConn=%d",
connected, (void*)pClient, pClient ? pClient->isConnected() : 0);
ESP_LOGI(TAG, "BLE: connected=%d scanning=%d ecdh=%d serverFound=%d",
connected, scanning, ecdhReady, serverFound);
statusLog = millis();
}
if (!connected && bleReady) {
if (millis() - lastConnectAttempt > SCAN_INTERVAL) {
ESP_LOGI(TAG, "BLE: connecting to server...");
if (pClient == nullptr) {
pClient = NimBLEDevice::createClient();
pClient->setConnectTimeout(10);
pClient->setClientCallbacks(&clientCallbacks);
}
if (pClient->connect(serverAddress)) {
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;
NimBLERemoteService *pSvc = pClient->getService(SERVICE_UUID);
if (pSvc) pCmdChar = pSvc->getCharacteristic(CHARACTERISTIC_UUID);
ESP_LOGI(TAG, "BLE: connected!");
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: connect failed");
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());
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 (millis() - scanStarted > SCAN_DURATION_MS) {
NimBLEDevice::getScan()->stop();
scanning = false;
ESP_LOGI(TAG, "BLE: scan timeout, no server found");
}
lastConnectAttempt = millis();
}
}