# SylDa NimBLE + HMAC — Design Spec Datum: 2025-07-31 Branch: nieuw, van `main` ## Doel Stabiele basis: 1× Atom S3 Lite als SylDa_Server + 1× Atom S3 Lite als Sylda_Client + Android app, met NimBLE, UUID identificatie, HMAC-SHA256 authenticatie. Later refactoren naar AES-GCM. ## Architectuur ``` ┌──────────────────────────────────────────────┐ │ Android App │ │ USB CDC: flash + provision (server & client) │ │ BLE: relay knoppen → HMAC → server │ │ OTA firmware update → server & client │ │ Radio buttons: [Server | Client] × [BLE|USB] │ └────────────────┬──────────────────────────────┘ │ BLE (client 2) │ USB CDC ▼ ▼ ┌─────────────────────────┐ ┌─────────────────────────┐ │ SylDa_Server │ │ Sylda_Client │ │ AtomS3 Lite │ │ AtomS3 Lite │ │ │◄─┤ │ │ Relais G1, G2 (ext) │ │ Touch1 G8, Touch2 G6 │ │ Touch1 G8, Touch2 G6 │ │ Button G41 │ │ Button G41 │ │ Relay-state → BLE HMAC │ │ OR-logica │ │ OTA (BLE+USB) │ │ OTA (BLE+USB) │ │ │ │ LED G35 (NeoPixel) │ │ LED G35 (NeoPixel) │ └─────────────────────────┘ └─────────────────────────┘ ``` ### Provisioning flow 1. Android app flasht firmware via USB CDC (EspFlasher) 2. Android leest server UUID via BLE READ 3. Android stuurt `SET_KEY`, `SET_ID`, `SET_SERVER` naar client via USB CDC 4. Server genereert eigen UUID bij eerste boot (NVS persistent) 5. Gedeelde HMAC key (32 bytes) = enige authenticatie voor beide clients 6. Server UUID in client NVS = verificatie bij connect (geen spoofing) 7. UUIDs + HMAC key overleven NVS bij firmware updates (partities 0x9000-0xDFFF) ## Hardware Beide devices: AtomS3 Lite (ESP32-S3), qio_qspi flash. ### Server | Pin | Functie | Type | |-----|---------|------| | G39 | RELAY1 | OUTPUT (externe relais module) | | G38 | RELAY2 | OUTPUT (externe relais module) | | G8 | TOUCH1 | INPUT_PULLUP | | G6 | TOUCH2 | INPUT_PULLUP | | G41 | BUTTON | INPUT (active low) | | G35 | LED | FastLED NeoPixel | ### Client | Pin | Functie | Type | |-----|---------|------| | G8 | TOUCH1 | INPUT_PULLUP | | G6 | TOUCH2 | INPUT_PULLUP | | G41 | BUTTON | INPUT (active low) | | G35 | LED | FastLED NeoPixel | ## Server firmware (`src/main.cpp`) Basis: `main` branch. NimBLE, UUID, HMAC, relays, touch, button, OTA, NVS. ### Connection tracking Max 2 verbindingen: client1 (AtomS3 Lite) + client2 (Android app). ```cpp bool bleRelay1[2] = {false, false}; bool bleRelay2[2] = {false, false}; bool connActive[2] = {false, false}; int connectedCount = 0; ``` ### Server callbacks ```cpp onConnect(pServer, connInfo): connActive[cid] = true; connectedCount++; if (connectedCount < 2) NimBLEDevice::startAdvertising(); onDisconnect(pServer, connInfo, reason): connActive[cid] = false; bleRelay1[cid] = false; bleRelay2[cid] = false; connectedCount--; NimBLEDevice::startAdvertising(); ``` CID mapping: via `NimBLEConnInfo::getConnHandle()` in callbacks. ConnHandle → index 0/1 in arrays. Eerste connect = index 0, tweede = index 1. Disconnect wist de entry en reset die index. ### Relay logica (OR over alle sources) ```cpp relay1 = touch1 || button || (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]); relay2 = touch2 || (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]); ``` OTA schakelt alle relays uit. LED = FastLED NeoPixel op G35 (geen digitalWrite meer). ### BLE characteristics | UUID | Type | Functie | |------|------|---------| | `4fafc201-...` | Service | | | `...f26d8` | WRITE_NR | Relay commands (HMAC) | | `...f26d9` | READ | Server UUID (identificatie) | | `...f26db` | READ | Firmware version | | `...f26da` | WRITE_NR + NOTIFY | OTA | ### Protocol HMAC-SHA256, 37-byte packets: `[cmd:1][seq:4][hmac:32]` | Cmd | Actie | |-----|-------| | 0x00 | Relay1 OFF | | 0x01 | Relay1 ON | | 0x02 | Relay2 OFF | | 0x03 | Relay2 ON | Seq = monotoon per connection, replay protection per client. ### USB CDC ``` KEY? → DEVICE_ID:<32 hex> + HMAC_KEY:<64 hex> SET_KEY:<64> → "OK" (slaat op in NVS) SET_ID:<32> → "OK" (slaat op in NVS) VERSION? → BUILD_NR: ``` ### FIRMWARE_VERSION Server: "6" Client: "1" Single source of truth: `version.txt` in project root. ``` server=6 client=1 ``` Build scripts lezen `version.txt`: - PlatformIO: `-DFIRMWARE_VERSION=` via `extra_scripts = pre:tools/pio_version_pre.py` - Android: `build.gradle` leest `version.txt` voor `EXPECTED_FIRMWARE_VERSION` ## Client firmware (`src/client.cpp`) Nieuw bestand op deze branch. HMAC, geen AES-GCM. ### BLE flow ``` 1. Scan (actief, interval=100, window=99) → filter op naam "SylDa_Server" 2. Connect → READ UUID characteristic 3. Verify: server UUID == geprovisioneerde serverUuid in NVS ✓ match → HMAC relay commands ✗ mismatch → disconnect, scan opnieuw ``` ### Relay logica (lokaal, stuurt eindresultaat) ```cpp relay1 = touch1 || button → stuurt 0x01/0x00 relay2 = touch2 → stuurt 0x02/0x03 ``` Debounce: 50ms, identiek aan server. ### HMAC packet build Zelfde 37-byte packet: `[cmd:1][seq:4][hmac:32]`, zelfde HMAC key. ### NVS ``` namespace "hmac": key "key" (32 bytes HMAC), "uuid" (16 bytes eigen UUID), "server" (16 bytes server UUID) ``` ### USB CDC ``` KEY? → DEVICE_ID:<32 hex> + HMAC_KEY:<64 hex> + SERVER_UUID:<32 hex> SET_KEY:<64> → "OK" SET_ID:<32> → "OK" SET_SERVER:<32> → "OK" VERSION? → BUILD_NR: ``` ### OTA Zelfde mechanisme als server, via BLE OTA characteristic. Chunks: 240 bytes. ## Android app Basis: `main` branch. Wijzigingen t.o.v. `main`: ### SettingsActivity Radio buttons: ``` [Server ▸ Client] × [BLE ▸ USB] Server + USB = flash + provision (SET_KEY, SET_ID) Server + BLE = relay knoppen + OTA + UUID READ Client + USB = flash + provision (SET_KEY, SET_ID, SET_SERVER) Client + BLE = OTA (via client UUID) ``` ### BleRelayManager Ongewijzigd — HMAC 37-byte packets, commands 0x00-0x03. ### EspFlasher Server én client firmware binaries. Assets gesplitst: ``` assets/firmware/server/{bootloader,partitions,boot_app0,firmware}.bin assets/firmware/client/{bootloader,partitions,boot_app0,firmware}.bin ``` ### PrefsHelper Extra keys: `deviceMode` (server/client), `serverUuid`, `clientUuid`. ## OTA protocol Blijft ongewijzigd van `main`. Chunk size: 240 bytes (past in MTU 248 single write). | Packet | Grootte | Inhoud | |--------|---------|--------| | START | 5 bytes | `[0x10][size:4]` | | Chunk | 4 + 240 bytes | `[seq:4][data:240]` | | Checksum | 32 bytes | SHA256 hash | | Response | 5 bytes | `[status:1][seq:4]` via notify | ## PlatformIO ```ini [env:s3lite-server] # AtomS3 Lite, qio_qspi, main.cpp [env:s3lite-client] # AtomS3 Lite, qio_qspi, DEVICE_ID=1, client.cpp upload_flags = --no-stub # beide envs (QSPI flash) ``` Beide envs identieke `qio_qspi` (beide AtomS3 Lite). ## Teststrategie 1. Server solo: touch/knop schakelen relais lokaal 2. Server + client: BLE relay commands via touch/knop op client 3. Server + client + Android: alle drie bronnen tegelijk, OR-logica 4. OTA: firmware update server én client via BLE 5. Reconnect: client + Android herstellen na disconnect 6. NVS persistentie: HMAC key + UUIDs overleven reboot ## Toekomst: AES-GCM refactor Na stabiele NimBLE+HMAC basis: vervang HMAC door AES-256-GCM (18-byte packets, encryptie + authenticatie). Code op `Sylda_Client` branch dient als referentie.