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

8.1 KiB
Raw Permalink Blame History

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

bool bleRelay1[2] = {false, false};
bool bleRelay2[2] = {false, false};
bool connActive[2] = {false, false};
int connectedCount = 0;

Server callbacks

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)

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)

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

[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.