docs: README volledig bijgewerkt — hardware, BLE, combined flow, serial output, NL vertalingen, ESP→module

This commit is contained in:
eddy 2026-07-29 23:31:51 +02:00
commit 36a20899be
2 changed files with 150 additions and 184 deletions

View file

@ -7,7 +7,7 @@ ESP32-S3-PICO-1-N8R8 (Atom S3R Voice), 2 relais, 2 touch sensors (JR223B), BLE,
- **Board:** Atom S3R Voice (ESP32-S3-PICO-1-N8R8), 16MB OPI flash - **Board:** Atom S3R Voice (ESP32-S3-PICO-1-N8R8), 16MB OPI flash
- **Pinnen:** G1=RELAY1, G2=RELAY2, G8+pullup=TOUCH1, G6+pullup=TOUCH2, G41=BUTTON (active low), G46=LED - **Pinnen:** G1=RELAY1, G2=RELAY2, G8+pullup=TOUCH1, G6+pullup=TOUCH2, G41=BUTTON (active low), G46=LED
- **Touch:** DC3-24V Capacitieve Touch Schakelaar JR223B (GEEN TTP223). Minder C op punt A = gevoeliger = meer false triggers. - **Touch:** DC3-24V Capacitieve Touch Schakelaar JR223B (GEEN TTP223). Minder C op punt A = gevoeliger = meer false triggers.
- **BLE:** "AtomS3R-Relay", service `4fafc201-1fb5-459e-8fcc-c5c9c331914b` - **BLE:** "SylDa_Server", service `4fafc201-1fb5-459e-8fcc-c5c9c331914b`
- **BLE cmds:** 0x00=relay1 OFF, 0x01=ON, 0x02=relay2 ON, 0x03=OFF, HMAC_PKT_SIZE=37 (1 cmd + 4 seq + 32 hmac) - **BLE cmds:** 0x00=relay1 OFF, 0x01=ON, 0x02=relay2 ON, 0x03=OFF, HMAC_PKT_SIZE=37 (1 cmd + 4 seq + 32 hmac)
- **NVS:** namespace `hmac`, key `key` — HMAC key 32 bytes, NVS op 0x9000-0xDFFF (partitie tabel) - **NVS:** namespace `hmac`, key `key` — HMAC key 32 bytes, NVS op 0x9000-0xDFFF (partitie tabel)
@ -21,7 +21,7 @@ ESP32-S3-PICO-1-N8R8 (Atom S3R Voice), 2 relais, 2 touch sensors (JR223B), BLE,
- CDC USB serial (`ARDUINO_USB_CDC_ON_BOOT=1`), QIO OPI flash - CDC USB serial (`ARDUINO_USB_CDC_ON_BOOT=1`), QIO OPI flash
- Upload laptop: `pio run -t upload`, port `/dev/ttyACM0`, 115200 baud - Upload laptop: `pio run -t upload`, port `/dev/ttyACM0`, 115200 baud
- NVS: namespace "hmac", key "key" (32 bytes HMAC) - NVS: namespace "hmac", key "key" (32 bytes HMAC)
- BLE advertising "AtomS3R-Relay", HMAC-verified relay commands - BLE advertising "SylDa_Server", HMAC-verified relay commands
## Android app — `android/` ## Android app — `android/`

330
README.md
View file

@ -1,6 +1,6 @@
# Project SylDa # Project SylDa
BLE-gestuurde relais-switch voor de M5Stack Atom VoiceS3R (ESP32-S3) met een bijbehorende Android-app. Het relais kan worden bediend via een capacitieve touch-sensor (open-drain) óf via Bluetooth Low Energy vanaf een Android-telefoon. BLE-gestuurde relais-switch voor de Atom S3R Voice (ESP32-S3-PICO-1-N8R8, 16MB OPI flash) met een bijbehorende Android-app. De relais kunnen worden bediend via capacitieve touch-sensoren (JR223B) óf via Bluetooth Low Energy vanaf een Android-telefoon.
De BLE-verbinding is beveiligd met **HMAC-SHA256 authenticatie** (application-level handtekening met replay-protectie). BLE-link encryptie/bonding is bewust niet toegepast — HMAC biedt authenticatie en replay-bescherming op applicatieniveau, voldoende voor een relais-aan/uit commando. De BLE-verbinding is beveiligd met **HMAC-SHA256 authenticatie** (application-level handtekening met replay-protectie). BLE-link encryptie/bonding is bewust niet toegepast — HMAC biedt authenticatie en replay-bescherming op applicatieniveau, voldoende voor een relais-aan/uit commando.
@ -8,34 +8,38 @@ De BLE-verbinding is beveiligd met **HMAC-SHA256 authenticatie** (application-le
| Component | Details | | Component | Details |
|---|---| |---|---|
| Board | M5Stack Atom VoiceS3R (SKU: C126-ECHO) | | Board | Atom S3R Voice (ESP32-S3-PICO-1-N8R8) |
| SoC | ESP32-S3-PICO-1-N8R8 (8MB Flash, 8MB PSRAM) | | SoC | ESP32-S3-PICO-1-N8R8 (8MB Flash, 8MB PSRAM, 16MB OPI flash) |
| Touch sensor | Open-drain sensor op **G38** (3.3V, interne pullup) | | Relais 1 | Op **G1** |
| Relais / LED | Op **G1** (3.3V GPIO output) | | Relais 2 | Op **G2** |
| Touch sensor 1 | DC3-24V Capacitieve Touch Schakelaar JR223B op **G8** (interne pullup) |
| Touch sensor 2 | DC3-24V Capacitieve Touch Schakelaar JR223B op **G6** (interne pullup) |
| Knop | Op **G41** (active low) |
| Status LED | Op **G46** (knippert 3x bij opstarten) | | Status LED | Op **G46** (knippert 3x bij opstarten) |
### Pinout (Atom VoiceS3R onderste header) ### Pinout
``` ```
G38 G39 3.3V GND 5V G5 G6 G7 G8 G1 = RELAY1
G2 = RELAY2
G6 = TOUCH2 (INPUT_PULLUP)
G8 = TOUCH1 (INPUT_PULLUP)
G41 = BUTTON (INPUT, active low)
G46 = LED (OUTPUT)
``` ```
- **G38** — Touch sensor input (active LOW, interne pullup ingeschakeld)
- **G1** — Relais output (HIGH = aan, LOW = uit)
- **G46** — Status LED (3x knipperen bij opstarten, daarna uit)
### Aansluiting ### Aansluiting
``` ```
Sensor GND → GND Touch sensor GND → GND
Sensor OUT → G38 Touch sensor OUT → G8 / G6
Sensor VCC → 3.3V Touch sensor VCC → 3.3V
Relais + → G1 Relais + → G1 / G2
Relais - → GND Relais - → GND
``` ```
> **Let op:** De ESP32-S3 interne pullup is ~45kΩ. Bij lange draden of elektrische ruis een externe 4.7kΩ–10kΩ pullup naar 3.3V plaatsen. > **JR223B touch sensor:** Minder C op punt A = gevoeliger = meer false triggers. Bij lange draden of elektrische ruis een externe pullup plaatsen.
## Beveiliging ## Beveiliging
@ -43,63 +47,63 @@ De relay is beveiligd tegen ongeautoriseerde toegang via HMAC-SHA256 authenticat
### HMAC-SHA256 Authenticatie (application-level) ### HMAC-SHA256 Authenticatie (application-level)
- Zowel de ESP firmware als de Android app delen een **32-byte shared secret** (HMAC key), die via USB provisioning op beide apparaten terechtkomt - Zowel de module firmware als de Android app delen een **32-byte shared secret** (HMAC key), die via USB provisioning op beide apparaten terechtkomt
- Elk commando wordt ondertekend met `HMAC-SHA256(key, command || sequence)` - Elk commando wordt ondertekend met `HMAC-SHA256(key, command || sequence)`
- De ESP verifieert de handtekening voordat het relais schakelt - De module verifieert de handtekening voordat het relais schakelt
- Een **sequence counter** voorkomt replay-aanvallen: elk packet moet een strikt oplopend volgnummer hebben - Een **sequence counter** voorkomt replay-aanvallen: elk packet moet een strikt oplopend volgnummer hebben
#### Packet formaat (37 bytes) #### Packet formaat (37 bytes)
``` ```
Offset Grootte Veld Beschrijving Offset Grootte Veld Beschrijving
0 1 byte command 0x01 = relais aan, 0x00 = relais uit 0 1 byte command 0x00=relay1 OFF, 0x01=ON, 0x02=relay2 ON, 0x03=OFF
1 4 bytes sequence Big-endian uint32, strikt oplopend (start bij 1) 1 4 bytes sequence Big-endian uint32, strikt oplopend (start bij 1)
5 32 bytes hmac HMAC-SHA256(key, command || sequence) 5 32 bytes hmac HMAC-SHA256(key, command || sequence)
``` ```
De sequence counter reset naar 0 bij elke (re)connectie op de ESP. De app start bij 1 om te zorgen dat het eerste packet altijd geaccepteerd wordt (`1 > 0`). Replay van een eerder packet wordt geweerd omdat `seq <= lastSeq`. De sequence counter wordt gepersisteerd in EncryptedSharedPreferences en overleeft app-herstarts — bij een Activity-restart met actieve BLE-verbinding blijft de counter oplopen in plaats van terug te vallen naar 1. De sequence counter reset naar 0 bij elke (re)connectie op de module. De app start bij 1 om te zorgen dat het eerste packet altijd geaccepteerd wordt (`1 > 0`). Replay van een eerder packet wordt geweerd omdat `seq <= lastSeq`. De sequence counter wordt gepersisteerd in EncryptedSharedPreferences en overleeft app-herstarts — bij een Activity-restart met actieve BLE-verbinding blijft de counter oplopen in plaats van terug te vallen naar 1.
#### Key management #### Key management
- De HMAC key wordt **niet meegecompileerd** — de ESP start zonder key en wordt geprovisioneerd via USB - De HMAC key wordt **niet meegecompileerd** — de module start zonder key en wordt geprovisioneerd via USB
- De key wordt opgeslagen in **NVS** (Non-Volatile Storage) op de ESP via de Arduino `Preferences` library (survives reboots en power-cycles) - De key wordt opgeslagen in **NVS** (Non-Volatile Storage) op de module via de Arduino `Preferences` library (namespace `hmac`, key `key`, partitie op 0x9000-0xDFFF)
- De app slaat de key op in **EncryptedSharedPreferences** (AES256-GCM versleuteld, overleeft app-herstarts) - De app slaat de key op in **EncryptedSharedPreferences** (AES256-GCM versleuteld, overleeft app-herstarts)
- **USB provisioning via Instellingen:** Open Instellingen → Sleutel Uitwisselen. Sluit de ESP via USB-OTG aan. De app stuurt `KEY?\n`: - **USB provisioning:** Open Instellingen → Firmware bijwerken. De app checkt eerst de firmware versie (`VERSION?`) en flasht indien nodig. Na het flashen (of als versie al actueel is) volgt automatisch de key exchange:
- Als de ESP al een key heeft → ESP antwoordt `HMAC_KEY:<hex>` → app slaat op lokaal op - Als de module al een key heeft → module antwoordt `HMAC_KEY:<hex>` → app slaat op lokaal op
- Als de ESP nog geen key heeft → ESP antwoordt `NO_KEY` → app genereert willekeurige 32-byte key via `SecureRandom` → stuurt `SET_KEY:<hex>\n` → ESP slaat op in NVS en antwoordt `OK` → app slaat op lokaal op - Als de module nog geen key heeft → module antwoordt `NO_KEY` → app genereert willekeurige 32-byte key via `SecureRandom` → stuurt `SET_KEY:<hex>\n` en `SET_ID:<hex>\n` → module slaat op in NVS → app slaat op lokaal op
- Beide paden tonen dezelfde toast: **"Beveiligingssleutel gelezen"**
- Zo kan dezelfde app gebruikt worden voor meerdere ESP modules — elk met een eigen key
#### USB Serial Protocol #### USB Serial Protocol
``` ```
App → ESP: KEY?\n App → module: VERSION?\n
ESP → App: HMAC_KEY:C3F17B...AF\n (als key bekend) module → App: BUILD_NR:1\n (firmware versie check)
ESP → App: NO_KEY\n (als ESP nog geen key heeft)
App → ESP: SET_KEY:C3F17B...AF\n (nieuwe key schrijven) App → module: KEY?\n
ESP → App: OK\n (bevestiging) module → App: DEVICE_ID:B874...D7F\n (module UUID)
module → App: HMAC_KEY:F2F1...642\n (HMAC key)
module → App: NO_KEY\n (als module nog geen key heeft)
App → module: SET_KEY:F2F1...642\n (nieuwe key schrijven)
module → App: OK\n (bevestiging)
App → module: SET_ID:B874...D7F\n (UUID schrijven)
module → App: OK\n (bevestiging)
``` ```
De ESP luistert in `loop()` voor USB serial data. Bij `KEY?\n` antwoordt hij met de hex key (`HMAC_KEY:<hex>`) of `NO_KEY`. Bij `SET_KEY:<64-hex>\n` slaat hij de key op in NVS en antwoordt `OK`. #### Provisioning flow
#### Provisioning flow (via Instellingen)
``` ```
Android app ESP32 (USB CDC) Android app ESP32 (USB CDC)
─────────── ─────────────── ─────────── ───────────────
User taps "Sleutel Uitwisselen" User taps "Firmware bijwerken"
Request USB permission ◄────── Android dialog Check version: VERSION?\n ───────►
Open CDC-ACM connection ◄─────────── BUILD_NR:<version>\n
Send KEY?\n ──────────────────► (flash if needed, module reboots)
┌─ if key in NVS ──► HMAC_KEY:<hex>\n Key exchange: KEY?\n ──────────►
│ App saves to prefs → toast ◄─────────── HMAC_KEY:<hex>\n
│ DEVICE_ID:<hex>\n
└─ if no key ──────► NO_KEY\n (of NO_KEY\n als leeg)
Generate 32 random bytes │ (if NO_KEY) SET_KEY + SET_ID ─► Save to NVS
Send SET_KEY:<hex>\n ─────────► (store in NVS)
◄──────────────────────────── OK\n
App saves to prefs → toast
``` ```
### Beveiligingsanalyse ### Beveiligingsanalyse
@ -112,8 +116,8 @@ De ESP luistert in `loop()` voor USB serial data. Bij `KEY?\n` antwoordt hij met
| **Brute-force key** | 32-byte key = 2^256 mogelijkheden — onhaalbaar | | **Brute-force key** | 32-byte key = 2^256 mogelijkheden — onhaalbaar |
| **Sniffen** | Pakketten zijn onversleuteld; geen confidentiality. Alleen commando's (aan/uit) zijn zichtbaar, niet de key. | | **Sniffen** | Pakketten zijn onversleuteld; geen confidentiality. Alleen commando's (aan/uit) zijn zichtbaar, niet de key. |
| **Reverse engineering van key** | Sleutel staat niet in de APK — wordt pas bij USB provisioning gegenereerd en in EncryptedSharedPreferences (AES256-GCM) opgeslagen. Extractie vereist fysieke toegang tot de telefoon én root. | | **Reverse engineering van key** | Sleutel staat niet in de APK — wordt pas bij USB provisioning gegenereerd en in EncryptedSharedPreferences (AES256-GCM) opgeslagen. Extractie vereist fysieke toegang tot de telefoon én root. |
| **Key extractie via USB** | Fysieke USB-toegang tot de ESP geeft `KEY?\n` de huidige key terug. Bescherming: fysieke toegang tot het board beperken. | | **Key extractie via USB** | Fysieke USB-toegang tot de module geeft `KEY?\n` de huidige key terug. Bescherming: fysieke toegang tot het board beperken. |
| **NVS wiping** | `pio run -t erase` wist alle flash waaronder NVS — ESP is daarna leeg en kan opnieuw geprovisioneerd worden. | | **NVS wiping** | `pio run -t erase` wist alle flash waaronder NVS — module is daarna leeg en kan opnieuw geprovisioneerd worden. |
### Waarom geen BLE-link encryptie? ### Waarom geen BLE-link encryptie?
@ -121,51 +125,53 @@ BLE-link encryptie (bonding/Just Works) vereist complexe pairing flows die op An
## Firmware ## Firmware
De firmware is geschreven in C++ met behulp van PlatformIO en de Arduino-framework. BLE wordt verzorgd door de ESP32 BLE library, HMAC door mbedtls (ingebouwd in ESP-IDF). De firmware is geschreven in C++ met behulp van PlatformIO en de Arduino-framework. BLE wordt verzorgd door de ESP32 NimBLE library, HMAC door mbedtls (ingebouwd in ESP-IDF).
### Logica ### Logica
Het relais gaat aan als **één van beide** bronnen actief is: Elk relais gaat aan als **één van beide** bronnen actief is:
| Bron | Voorwaarde | Resultaat | | Relais | Bron | Voorwaarde | Resultaat |
|---|---|---| |---|---|---|---|
| Touch sensor | G38 = LOW (aanraken) | Relais aan | | Relais 1 | Touch sensor 1 | G8 = LOW (aanraken) | Relais 1 aan |
| BLE command | App stuurt HMAC-ondertekend `0x01` | Relais aan | | Relais 1 | BLE command | App stuurt HMAC-ondertekend `0x01` | Relais 1 aan |
| Geen van beide | G38 = HIGH + geen BLE command | Relais uit | | Relais 2 | Touch sensor 2 | G6 = LOW (aanraken) | Relais 2 aan |
| Relais 2 | BLE command | App stuurt HMAC-ondertekend `0x02` | Relais 2 aan |
| Beide | Geen van beide | Geen touch + geen BLE command | Relais uit |
De touch sensor en BLE werken onafhankelijk. Als de sensor losgelaten wordt en de BLE-knop niet ingedrukt is, gaat het relais uit. De touch sensoren en BLE werken onafhankelijk. Als de sensor losgelaten wordt en de BLE-knop niet ingedrukt is, gaat het relais uit.
### BLE GATT ### BLE GATT
| Eigenschap | UUID | | Eigenschap | UUID |
|---|---| |---|---|
| Service | `4fafc201-1fb5-459e-8fcc-c5c9c331914b` | | Service | `4fafc201-1fb5-459e-8fcc-c5c9c331914b` |
| Characteristic | `beb5483e-36e1-4688-b7f5-ea07361f26d8` | | Relay Characteristic | `beb5483e-36e1-4688-b7f5-ea07361f26d8` |
| UUID Characteristic | `beb5483e-36e1-4688-b7f5-ea07361f26d9` |
De characteristic ondersteunt `WRITE_NR` (Write Without Response) en `NOTIFY`. Writes worden geaccepteerd zonder bonding — de HMAC-handtekening op applicatieniveau verzorgt de authenticatie. `WRITE_NR` zorgt voor minimale latency: de app kan direct na ON ook OFF sturen zonder te wachten op een ACK. De relay characteristic ondersteunt `WRITE_NR` (Write Without Response) en `NOTIFY`. De UUID characteristic is read-only en bevat het unieke device-ID van de module.
### Key provisioning ### Key provisioning
De firmware wordt zonder HMAC key gecompileerd. Bij de eerste opstart print de ESP: De firmware wordt zonder HMAC key gecompileerd. Bij de eerste opstart print de module:
``` ```
No HMAC key provisioned — connect USB and run SET_KEY No HMAC key provisioned — connect USB and run SET_KEY
``` ```
Sluit de ESP via USB aan op de telefoon met de app geïnstalleerd. De app genereert automatisch een willekeurige key en stuurt deze naar de ESP. Na provisioning print de ESP bij opstarten: Sluit de module via USB aan op de telefoon met de app geïnstalleerd. De app genereert automatisch een willekeurige key en stuurt deze naar de module. Na provisioning print de module bij opstarten:
``` ```
HMAC key loaded from NVS HMAC key loaded from NVS
HMAC Key: C3F17B...AF
``` ```
**Key wissen (factory reset):** Om de ESP terug te zetten naar ongeprovisioneerd: **Key wissen (factory reset):** Om de module terug te zetten naar ongeprovisioneerd:
```bash ```bash
pio run -t erase pio run -t erase
``` ```
Dit wist alle flash inclusief NVS. Na opnieuw flashen start de ESP weer zonder key. Dit wist alle flash inclusief NVS. Na opnieuw flashen start de module weer zonder key.
### Serial monitor ### Serial monitor
@ -176,27 +182,25 @@ Baudrate: **115200** (USB CDC). De firmware stuurt heartbeat-statussen elke 5 se
=== Atom VoiceS3R BLE Relay === === Atom VoiceS3R BLE Relay ===
HMAC key loaded from NVS HMAC key loaded from NVS
LED blink done, starting BLE... LED blink done, starting BLE...
BLE: advertising started as 'AtomS3R-Relay' BLE: advertising started as 'SylDa_Server' (start=1, isAdv=1)
HMAC Key: C3F17B612B93E82C56B055FA9B04E84664ACE1EC4E889F2763F271B65A5B54AF
Setup done. Setup done.
[heartbeat] relay=OFF touch=no ble=disconnected [heartbeat] R1=0 R2=0 t1=1 t2=1 btn=1 ble1=0 ble2=0
BLE: device connected BLE: device connected
Relay ON (touch=YES, ble=connected) BLE: relay1 ON (seq=1)
BLE: relay ON (seq=1) BLE: relay1 OFF (seq=2)
BLE: relay OFF (seq=2)
``` ```
**Zonder key (nieuwe/gewiste ESP):** **Zonder key (nieuwe/gewiste module):**
``` ```
=== Atom VoiceS3R BLE Relay === === Atom VoiceS3R BLE Relay ===
No HMAC key provisioned — connect USB and run SET_KEY No HMAC key provisioned — connect USB and run SET_KEY
LED blink done, starting BLE... LED blink done, starting BLE...
BLE: advertising started as 'AtomS3R-Relay' BLE: advertising started as 'SylDa_Server' (start=1, isAdv=1)
No HMAC key — use USB SET_KEY to provision No HMAC key — use USB SET_KEY to provision
Setup done. Setup done.
``` ```
In deze staat accepteert de ESP wel BLE-verbindingen, maar elk HMAC-packet faalt verificatie omdat er geen key is om mee te vergelijken. In deze staat accepteert de module wel BLE-verbindingen, maar elk HMAC-packet faalt verificatie omdat er geen key is om mee te vergelijken.
### Uploaden ### Uploaden
@ -214,14 +218,13 @@ Na het uploaden treedt er een harde reset op. Als het board in download-mode bli
### Firmware flashen via de app ### Firmware flashen via de app
De firmware kan ook rechtstreeks via de Android-app worden geflashed, zonder computer: De firmware kan rechtstreeks via de Android-app worden geflashed, geen computer nodig:
1. Zet de ESP in download-modus (houd reset ~2s in tot groene LED) 1. Sluit de module via USB-OTG aan op de telefoon
2. Sluit de ESP via USB-OTG aan op de telefoon 2. Open de app → Instellingen → Firmware bijwerken
3. Open de app → Instellingen → Firmware Flashen 3. De app checkt de firmware versie. Als de versie actueel is, wordt het flashen overgeslagen
4. De app flasht bootloader, partitions, en firmware via USB 4. Is de firmware verouderd, dan flasht de app bootloader, partitions, en firmware
5. Dialog verdwijnt automatisch wanneer de USB wordt losgekoppeld 5. Na het flashen herstart de module en volgt automatisch de key exchange
6. Sluit de ESP opnieuw aan → Sleutel Uitwisselen
De firmware binaries worden via `tools/copy_firmware.sh` naar de app assets gekopieerd. De firmware binaries worden via `tools/copy_firmware.sh` naar de app assets gekopieerd.
@ -229,40 +232,31 @@ De firmware binaries worden via `tools/copy_firmware.sh` naar de app assets geko
### Features ### Features
- **Auto-connect:** De app scant automatisch bij opstarten naar het BLE-apparaat "AtomS3R-Relay" - **Auto-connect:** De app scant automatisch bij opstarten naar de BLE service UUID `4fafc201-1fb5-459e-8fcc-c5c9c331914b` ("SylDa_Server")
- **Auto-reconnect:** Bij wegvallen van de verbinding wordt na 2 seconden opnieuw gescant - **Auto-reconnect:** Bij wegvallen van de verbinding wordt na 2 seconden opnieuw gescant
- **Scan retry:** Bij scan timeout wordt de HOLD-knop een "Opnieuw scannen" knop — tik om opnieuw te scannen zonder de app te herstarten - **Scan retry:** Bij scan timeout wordt relais 1 een "Opnieuw zoeken" knop — tik om opnieuw te scannen
- **Hold-to-activate:** Houd de knop ingedrukt voor relais aan, loslaten voor relais uit (zelfde gedrag als de sensor) - **Hold-to-activate:** Houd de knop ingedrukt voor relais aan, loslaten voor relais uit
- **Write Without Response:** Gebruikt `WRITE_TYPE_NO_RESPONSE` voor minimale latency — ON en OFF kunnen direct achter elkaar worden verstuurd zonder ACK-wachttijd - **Twee onafhankelijke relais:** Twee knoppen, elk met eigen HMAC commando code
- **Write Without Response:** Gebruikt `WRITE_TYPE_NO_RESPONSE` voor minimale latency
- **MTU negotiatie:** Vraagt MTU 256 aan na service discovery (voor 37-byte HMAC packets) - **MTU negotiatie:** Vraagt MTU 256 aan na service discovery (voor 37-byte HMAC packets)
- **HMAC authenticatie:** Elk commando wordt ondertekend met HMAC-SHA256 + sequence counter - **HMAC authenticatie:** Elk commando wordt ondertekend met HMAC-SHA256 + sequence counter
- **USB provisioning via Instellingen:** Via Instellingen → Sleutel Uitwisselen leest de app de bestaande key uit, of genereert een nieuwe key als de ESP er nog geen heeft. Beide scenario's tonen de toast _"Beveiligingssleutel gelezen"_ - **Key exchange geïntegreerd:** De "Firmware bijwerken" knop checkt versie, flasht indien nodig, en wisselt automatisch sleutels uit
- **Firmware flashen via app:** De app kan de ESP32 firmware rechtstreeks via USB flashen (download-modus), geen computer nodig - **Thema-selector:** Instellingen → Weergave kiest Systeem, Licht of Donker
- **Instellingenscherm:** Losse Activity voor sleutel uitwisseling, firmware flashen, en buildnummer weergave - **Permissies:** Vraagt BLUETOOTH_SCAN en BLUETOOTH_CONNECT aan. Bij weigeren wordt de knop uitgeschakeld en verwezen naar Instellingen
- **Permissies:** Vraagt BLUETOOTH_SCAN, BLUETOOTH_CONNECT en ACCESS_FINE_LOCATION aan (Android 12+). Bij weigeren wordt de knop uitgeschakeld en wordt de gebruiker doorverwezen naar Instellingen
- **BLE write feedback:** `onCharacteristicWrite` callback logt succes/falen; bij falen wordt "Communicatiefout" getoond
- **GATT status logging:** Verbindingsstatus, service discovery en MTU-negotiatie loggen foutstatussen (`GATT_SUCCESS` check)
- **Scan annulering:** BLE-scan wordt betrouwbaar gestopt in `onPause` via gedeelde `scanRunnable` + `removeCallbacks`
- **Cancellable firmware flash:** Flash-proces kan geannuleerd worden via de annuleerknop; `EspFlasher` gebruikt een `@Volatile cancelled` flag
- **Key exchange robuustheid:** 5 retries (was 3), 1000ms delay (was 500ms), duidelijkere foutmelding bij falen
- **Thema-selector:** Instellingen → Weergave kiest Systeem (volgt telefoon), Licht of Donker. App gebruikt `Theme.AppCompat.DayNight` met teal accent-kleuren. Android's systeem-lettergrootte (`sp`) en hoog-contrast instellingen worden gerespecteerd
### Instellingenscherm ### Instellingenscherm
1. Open de app 1. Open de app → tik op **instellingen** (tandwiel-icoon rechtsboven)
2. Tik op het **tandwiel-icoon** rechtsboven 2. **Firmware bijwerken:** Sluit de module aan via USB. De app checkt de firmware versie, flasht indien nodig, en wisselt daarna automatisch sleutels uit. Eén knop voor de hele flow.
3. De app toont twee acties: **Sleutel Uitwisselen** en **Firmware Flashen** 3. **Weergave:** Kies Systeem (volgt telefoon), Licht of Donker
4. **Sleutel Uitwisselen:** Sluit de ESP via USB-OTG aan. De app leest de key uit (als de ESP er al een heeft) of genereert een nieuwe key en provisioneert de ESP. Bij falen: toast _"Sleutel uitwisselen mislukt — probeer opnieuw"_. Bij succes: _"Beveiligingssleutel gelezen"_ 4. **Relais labels:** Stel eigen namen in voor de twee relais knoppen
5. **Firmware Flashen:** Zet de ESP in download-modus (houd reset 2s in tot groene LED). Sluit aan via USB. De app flasht de firmware met voortgangsbalk. Het flash-proces kan geannuleerd worden. Dialog verdwijnt automatisch bij loskoppelen USB.
6. **Weergave:** Kies Systeem (volgt telefoon), Licht of Donker. De app herstart met het gekozen thema.
7. Onderaan staat het **Buildnummer** (timestamp van de build)
De key wordt opgeslagen in EncryptedSharedPreferences (AES256-GCM versleuteld) en overleeft app-herstarts. Bij geen key toont de hoofdapp "Geen HMAC sleutel - open Instellingen" en wordt er niet gescant. De key wordt opgeslagen in EncryptedSharedPreferences (AES256-GCM versleuteld) en overleeft app-herstarts. Bij geen key toont de hoofdapp "Geen sleutel — open Instellingen" en wordt er niet gescant.
### Hoofdscherm UI ### Hoofdscherm UI
- **Tandwiel-icoon** rechtsboven → opent instellingenscherm - **instellingen** tekst + tandwiel-icoon rechtsboven → opent instellingenscherm
- **HOLD-knop** centrum → houd ingedrukt voor relais aan - **Twee knoppen** centrum → hold-to-activate voor relais 1 en 2
- **Status tekst** linksonder (14sp) → toont verbindingsstatus - **Status tekst** linksonder (14sp) → toont verbindingsstatus
### Bouwen ### Bouwen
@ -295,57 +289,35 @@ adb -s <ip>:5555 install -r android/app/build/outputs/apk/debug/app-debug.apk
## Eerste keer setup ## Eerste keer setup
1. **Firmware compileren:** 1. **App op de telefoon installeren:**
```bash
pio run
bash tools/copy_firmware.sh
```
2. **App op de telefoon installeren:**
```bash ```bash
cd android cd android
JAVA_HOME=/home/linuxbrew/.linuxbrew/opt/openjdk@21/libexec ./gradlew installDebug JAVA_HOME=/home/linuxbrew/.linuxbrew/opt/openjdk@21/libexec ./gradlew installDebug
``` ```
3. **Firmware flashen (kies één):** 2. **Firmware flashen & sleutel uitwisselen (één flow):**
- Sluit de module via USB-OTG aan op de telefoon
- Open de app → Instellingen → Firmware bijwerken
- De app checkt versie, flasht indien nodig, en wisselt sleutels uit — volledig automatisch
**Optie A — Via de app (aanbevolen, geen computer nodig na eerste installatie):** 3. **Gebruiken:** De app scant automatisch, verbindt, en toont "Verbonden". Houd een knop ingedrukt om het relais te schakelen.
- Zet de ESP in download-modus (houd reset ~2s in tot groene LED)
- Sluit de ESP via USB-OTG aan op de telefoon
- Open de app → Instellingen → Firmware Flashen
- Dialog verdwijnt automatisch bij loskoppelen USB
**Optie B — Via computer:** ### Nieuwe module, bestaande telefoon
```bash
pio run -t upload
```
4. **Sleutel uitwisselen:** - Sluit de nieuwe module via USB aan → Firmware bijwerken → app flasht firmware en genereert nieuwe key
- Sluit de ESP via USB-OTG aan op de telefoon (niet in download-modus)
- Open de app → Instellingen → Sleutel Uitwisselen
- De app leest de bestaande key uit of genereert een nieuwe
- Toast: _"Beveiligingssleutel gelezen"_
5. **Gebruiken:** De app scant automatisch, verbindt, en toont "Connected". Houd de HOLD-knop ingedrukt om het relais te schakelen. ### Bestaande module, nieuwe telefoon
### Nieuwe ESP, bestaande telefoon - Sluit de module via USB-OTG aan op de nieuwe telefoon
- Firmware bijwerken → app herkent bestaande firmware, slaat key over, leest bestaande key uit
- Sluit de nieuwe ESP via USB aan → app genereert nieuwe key (overschrijft vorige)
- Flash eerst de firmware (download-modus → Firmware Flashen), daarna sleutel uitwisselen
### Bestaande ESP, nieuwe telefoon
- Sluit de ESP via USB-OTG aan op de nieuwe telefoon
- De app leest de bestaande key uit de ESP en slaat lokaal op
- Toast: _"Beveiligingssleutel gelezen"_
## Projectstructuur ## Projectstructuur
``` ```
sylvia-touch-relay/ Project_SylDa/
├── platformio.ini # PlatformIO configuratie (ESP32-S3) ├── platformio.ini # PlatformIO configuratie (ESP32-S3)
├── src/ ├── src/
│ └── main.cpp # Firmware (BLE + HMAC + NVS + USB serial + sensor + relais) │ └── main.cpp # Firmware (BLE + HMAC + NVS + USB serial + touch + relais)
├── tools/ ├── tools/
│ └── copy_firmware.sh # Kopieert firmware binaries naar app assets │ └── copy_firmware.sh # Kopieert firmware binaries naar app assets
├── android/ ├── android/
@ -353,37 +325,37 @@ sylvia-touch-relay/
│ ├── settings.gradle # Gradle settings │ ├── settings.gradle # Gradle settings
│ ├── gradle.properties │ ├── gradle.properties
│ ├── gradlew # Gradle wrapper │ ├── gradlew # Gradle wrapper
│ ├── gradle/wrapper/
│ │ └── gradle-wrapper.properties
│ └── app/ │ └── app/
│ ├── build.gradle # App module config (esp32-flash-lib, minSdk 26) │ ├── build.gradle # App module (nl.eddydevink.sylda, minSdk 26)
│ └── src/main/ │ └── src/main/
│ ├── AndroidManifest.xml │ ├── AndroidManifest.xml
│ ├── assets/firmware/ # Gecompileerde firmware binaries │ ├── assets/firmware/
│ │ ├── bootloader.bin │ │ ├── bootloader.bin
│ │ ├── partitions.bin │ │ ├── partitions.bin
│ │ ├── firmware.bin │ │ ├── firmware.bin
│ │ └── boot_app0.bin │ │ └── boot_app0.bin
│ ├── java/com/eddy/atomrelay/ │ ├── java/nl/eddydevink/sylda/
│ │ ├── MainActivity.kt # BLE client + HMAC signing + write feedback │ │ ├── MainActivity.kt
│ │ ├── SettingsActivity.kt # Key exchange + firmware flash UI │ │ ├── SettingsActivity.kt
│ │ ├── EspFlasher.kt # Cancellable ESP32 firmware flasher via USB │ │ ├── BleRelayManager.kt
│ │ ├── UsbSerialTransport.kt # USB serial transport voor flasher │ │ ├── BleStatus.kt
│ │ ├── UsbUtils.kt # Gedeelde USB utility functions │ │ ├── EspFlasher.kt
│ │ ├── PrefsHelper.kt # EncryptedSharedPreferences utility (AES256-GCM) │ │ ├── UsbSerialTransport.kt
│ │ └── BuildConfig # Buildnummer (timestamp) │ │ ├── UsbUtils.kt
│ └── res/ │ │ └── PrefsHelper.kt
│ ├── drawable/ │ ├── res/
│ │ ├── ic_eye_open.xml # Oog open icoon │ │ ├── drawable/
│ │ ├── ic_eye_closed.xml # Oog dicht icoon │ │ │ └── ic_settings.xml
│ │ └── ic_settings.xml # Tandwiel icoon │ │ ├── layout/
│ ├── layout/ │ │ │ ├── activity_main.xml
│ │ ├── activity_main.xml # Hoofd UI (FrameLayout) │ │ │ ├── activity_settings.xml
│ │ ├── activity_settings.xml # Key exchange + flash UI │ │ │ ├── dialog_waiting.xml
│ │ ├── dialog_waiting.xml # Spinner dialog (vervangt ProgressDialog) │ │ │ └── dialog_progress.xml
│ │ └── dialog_progress.xml # Voortgangsbalk dialog (vervangt ProgressDialog) │ │ ├── values/strings.xml
│ └── values/strings.xml │ │ └── xml/
│ │ └── device_filter.xml
├── .gitignore ├── .gitignore
├── LICENSE
└── README.md └── README.md
``` ```
@ -403,25 +375,19 @@ De app gebruikt `Theme.AppCompat.DayNight` in plaats van het eerder geforceerde
## Technische notities ## Technische notities
- De Atom VoiceS3R gebruikt de ES8311 audio codec op G45/G0/G48/G4/G3/G17/G11, IR TX op G47, en de knop op G41. Deze pinnen zijn niet beschikbaar voor eigen gebruik. - De Atom S3R Voice gebruikt de ES8311 audio codec op G45/G0/G48/G4/G3/G17/G11, IR TX op G47, en de knop op G41. Deze pinnen zijn niet beschikbaar voor eigen gebruik (behalve G41 als input).
- G1 en G2 zitten op de HY2.0-4P Grove poort. G5-G8, G38, G39 zitten op de onderste expansie-header. - G1 en G2 zitten op de HY2.0-4P Grove poort. G5-G8, G38, G39 zitten op de onderste expansie-header.
- De firmware gebruikt geen M5Unified library — alleen Arduino core + ESP32 BLE + mbedtls. Dit maakt de serial output betrouwbaarder via USB CDC. - De firmware gebruikt geen M5Unified library — alleen Arduino core + ESP32 NimBLE + mbedtls.
- `ARDUINO_USB_CDC_ON_BOOT=1` is ingeschakeld voor virtuele serial via USB. - `ARDUINO_USB_CDC_ON_BOOT=1` is ingeschakeld voor virtuele serial via USB.
- De HMAC key wordt opgeslagen in NVS (Non-Volatile Storage) via de Arduino `Preferences` library. De key wordt niet meegecompileerd maar via USB provisioning door de Android app op de ESP gezet. Sleuteloverdracht werkt in beide richtingen: de app kan een key naar de ESP schrijven (nieuwe ESP) of een bestaande key uitlezen (bestaande ESP op nieuwe telefoon). - De HMAC key wordt opgeslagen in NVS (namespace `hmac`, key `key`, partitie 0x9000-0xDFFF). Via `pio run -t erase` kan NVS gewist worden.
- De app gebruikt `WRITE_TYPE_NO_RESPONSE` (Write Without Response) voor minimale latency. De ESP characteristic heeft `PROPERTY_WRITE_NR` zodat writes zonder ACK worden geaccepteerd. Dit maakt directe ON/OFF sequenties mogelijk zonder write queue. Zie _Ontwerpbeslissingen_ voor de rationale. - De app gebruikt `WRITE_TYPE_NO_RESPONSE` (Write Without Response) voor minimale latency.
- De sequence counter start bij 1 (niet 0) om te voorkomen dat het eerste packet na connectie als replay wordt geweerd (`0 <= 0` is true in de `seq <= lastSeq` check). De counter wordt gepersisteerd in EncryptedSharedPreferences — bij app-herstart (zonder BLE reconnect) gaat de counter verder waar hij gebleven was. Bij BLE reconnect wordt de in-memory counter teruggezet naar 1 (de ESP reset zijn `lastSeq` ook). - De sequence counter start bij 1 (niet 0) om te voorkomen dat het eerste packet na connectie als replay wordt geweerd.
- Voor meerdere ESP modules met verschillende keys: sluit elke ESP één voor één aan op de telefoon. De app genereert per ESP een eigen key en slaat deze op. De app ondersteunt momenteel slechts één key tegelijk (opgeslagen in EncryptedSharedPreferences). Bij wisselen van ESP: wis app data of sluit de nieuwe ESP aan (overschrijft vorige key). - De app gebruikt raw Android USB API (`UsbDeviceConnection.bulkTransfer`) in plaats van een externe serial library.
- De app gebruikt raw Android USB API (`UsbDeviceConnection.bulkTransfer`) in plaats van een externe serial library. Dit vermijdt compatibiliteitsproblemen met de ESP32-S3 USB-JTAG/serial controller die geen CDC `get_status` requests ondersteunt. - `minSdk` is 26 (Android 8.0).
- CDC control interface setup: line coding (115200 8N1, parity NONE) via SET_LINE_CODING request + DTR/RTS via SET_CONTROL_LINE_STATE. Dit activeert de data-flow op de ESP32-S3 CDC-ACM interface voordat commando's worden gestuurd. - ProgressDialog is vervangen door eigen `AlertDialog` layouts (`dialog_waiting.xml`, `dialog_progress.xml`).
- Het buildnummer wordt via `BuildConfig.BUILD_NUMBER` in `build.gradle` gegenereerd als timestamp (`yyyyMMddHHmmss`). Elke build produceert een uniek buildnummer dat onderaan het instellingenscherm staat. - De HMAC key in de app is opgeslagen via `EncryptedSharedPreferences` (AES256-GCM).
- `minSdk` is 26 (Android 8.0). Android 7 en lager worden niet ondersteund. - De app gebruikt `Theme.AppCompat.DayNight` met Systeem/Licht/Donker thema-selector.
- ProgressDialog is vervangen door eigen `AlertDialog` layouts (`dialog_waiting.xml` met spinner, `dialog_progress.xml` met ProgressBar). ProgressDialog is deprecated sinds API 26. - `compileSdk`/`targetSdk` blijft op 34 (Android 14) voor maximale compatibiliteit met de doelgroep.
- De HMAC key wordt in de app opgeslagen via `EncryptedSharedPreferences` (AES256-GCM) via `PrefsHelper`. Dit versleutelt de key at-rest, in tegenstelling tot gewone SharedPreferences waarbij waarden plaintext op schijf staan.
- Bij geweigerde BLE-permissies wordt de HOLD-knop uitgeschakeld en toont de app "Permissies geweigerd — open Instellingen" met een link naar de systeem-instellingen.
- De BLE-scan wordt in `onPause` betrouwbaar gestopt via `handler.removeCallbacks(scanRunnable)`, wat voorkomt dat scans doordraaien op de achtergrond.
- Het firmware flash-proces is annuleerbaar via coroutine Job cancellation. De annuleerknop en `onDestroy` roepen `flasherJob?.cancel()` aan, wat de `CancellationException` doorgeeft aan de actieve coroutine. Checkpoints via `ensureActive()` in de flash-lus stoppen het proces direct.
- USB systeem-broadcasts (`ACTION_USB_DEVICE_ATTACHED`/`DETACHED`) worden geregistreerd met `RECEIVER_EXPORTED` voor compatibiliteit met Android 14+ (API 34). De custom `ACTION_USB_PERMISSION` broadcast gebruikt `RECEIVER_NOT_EXPORTED` (app-intern).
- De app gebruikt `Theme.AppCompat.DayNight` in plaats van `Theme.AppCompat.Light`. Het thema wordt gekozen via Instellingen → Weergave: Systeem (volgt telefoon via `AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM`), Licht (`MODE_NIGHT_NO`) of Donker (`MODE_NIGHT_YES`). Het thema wordt toegepast in elke `onCreate` vóór `super.onCreate()`. Bij wijziging in de Instellingen wordt de Activity herstart via `setDefaultNightMode`. De `onResume`-methode detecteert of het thema gewijzigd is sinds de Activity voor het laatst zichtbaar was en herstart dan.
## Licentie ## Licentie