Project_SylDa/docs/superpowers/specs/2025-07-31-sylda-nimble-hmac-design.md

269 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:<n>
```
### 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=<n>` 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:<n>
```
### 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.