# Sylvia Touch Relay 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. De BLE-verbinding is beveiligd met **twee lagen**: BLE Just Works pairing (link-layer encryptie) en **HMAC-SHA256 authenticatie** (application-level handtekening met replay-protectie). ## Hardware | Component | Details | |---|---| | Board | M5Stack Atom VoiceS3R (SKU: C126-ECHO) | | SoC | ESP32-S3-PICO-1-N8R8 (8MB Flash, 8MB PSRAM) | | Touch sensor | Open-drain sensor op **G38** (3.3V, interne pullup) | | Relais / LED | Op **G1** (3.3V GPIO output) | | Status LED | Op **G46** (knippert 3x bij opstarten) | ### Pinout (Atom VoiceS3R onderste header) ``` G38 G39 3.3V GND 5V G5 G6 G7 G8 ``` - **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 ``` Sensor GND → GND Sensor OUT → G38 Sensor VCC → 3.3V Relais + → G1 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. ## Beveiliging De relay is beveiligd tegen ongeautoriseerde toegang via twee onafhankelijke lagen: ### 1. BLE Just Works Pairing (link-layer encryptie) - De ESP32 vereist **bonding** voordat de characteristic geschreven kan worden - IO capability: `ESP_IO_CAP_NONE` (Just Works — geen passcode nodig) - Authentication mode: `ESP_LE_AUTH_REQ_SC_BOND` (LE Secure Connections met bonding) - De characteristic heeft `ESP_GATT_PERM_WRITE_ENCRYPTED` — writes worden alleen geaccepteerd over een versleutelde link - Na de eerste keer paren onthoudt de telefoon de bonding; bij herverbinding wordt de link automatisch versleuteld ### 2. HMAC-SHA256 Authenticatie (application-level) Omdat BLE Just Works geen MITM-bescherming biedt, is er een tweede laag toegevoegd: - Zowel de ESP firmware als de Android app bevatten een **32-byte shared secret** (HMAC key) - Elk commando wordt ondertekend met `HMAC-SHA256(key, command || sequence)` - De ESP verifieert de handtekening voordat het relais schakelt - Een **sequence counter** voorkomt replay-aanvallen: elk packet moet een strikt oplopend volgnummer hebben #### Packet formaat (37 bytes) ``` Offset Grootte Veld Beschrijving 0 1 byte command 0x01 = relais aan, 0x00 = relais uit 1 4 bytes sequence Big-endian uint32, strikt oplopend (start bij 1) 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`. #### Key management - De HMAC key staat in `src/hmac_key.h` (niet in git, `.gitignore`) - Een CLI-tool genereert een willekeurige key: `python3 tools/generate_hmac_key.py` - De ESP print de key 5 seconden na opstarten naar serial - **USB auto-read:** Sluit de ESP via USB-OTG aan op de telefoon → de app leest de key automatisch via USB serial (CDC-ACM). De ESP reageert op het commando `KEY?\n` met `HMAC_KEY:`. Geen encryptie nodig — fysieke connectie is de beveiliging. - Handmatige invoer: via het instellingenscherm in de Android app (64-teken hex string) - De app slaat de key op in SharedPreferences (overleeft app-herstarts) - Zo kan dezelfde app gebruikt worden voor meerdere ESP modules — each met een eigen key #### USB Serial Key Transfer Protocol ``` App → ESP: KEY?\n ESP → App: HMAC_KEY:C3F17B612B93E82C56B055FA9B04E84664ACE1EC4E889F2763F271B65A5B54AF\n ``` De ESP luistert in `loop()` voor USB serial data. Bij `KEY?\n` antwoordt hij met de hex key. De Android `UsbKeyReceiver` (manifest-geregistreerde `BroadcastReceiver`) vangt `USB_DEVICE_ATTACHED` op, filtert op Espressif (vendor 0x303a), vraagt USB-permission, opent een CDC-ACM serial connectie, stuurt `KEY?\n`, en slaat de respons op in SharedPreferences. ### Beveiligingsanalyse | Aanval | Hoe geweerd | |---|---| | **Sniffen** | BLE link encrypted (AES-CCM via Just Works) | | **Replay** | Sequence counter — zelfde packet werkt niet 2x | | **Ongeautoriseerd device** | HMAC vereist geheime sleutel | | **MITM** | Just Works is kwetsbaar voor MITM; HMAC maakt actieve MITM wel moeilijk (aanvaller moet de key kennen) | | **Reverse engineering van key** | Sleutel staat in APK — extracteerbaar met reverse engineering. Voor hogere veiligheid: gebruiker ProGuard/R8 obfuscation. | ## 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). ### Logica Het relais gaat aan als **één van beide** bronnen actief is: | Bron | Voorwaarde | Resultaat | |---|---|---| | Touch sensor | G38 = LOW (aanraken) | Relais aan | | BLE command | App stuurt HMAC-ondertekend `0x01` | Relais aan | | Geen van beide | G38 = HIGH + geen BLE command | Relais uit | De touch sensor en BLE werken onafzankelijk. Als de sensor losgelaten wordt en de BLE-knop niet ingedrukt is, gaat het relais uit. ### BLE GATT | Eigenschap | UUID | |---|---| | Service | `4fafc201-1fb5-459e-8fcc-c5c9c331914b` | | Characteristic | `beb5483e-36e1-4688-b7f5-ea07361f26d8` | De characteristic ondersteunt `WRITE` (encrypted) en `NOTIFY`. Writes vereisen een versleutelde link (bonding) plus HMAC-authenticatie. ### HMAC Key genereren Voordat de firmware gecompileerd kan worden, moet er een HMAC key gegenereerd worden: ```bash python3 tools/generate_hmac_key.py ``` Dit maakt `src/hmac_key.h` aan met een willekeurige 32-byte key en print de hex-representatie. Een bestaande key overschrijven: ```bash echo "y" | python3 tools/generate_hmac_key.py ``` > **Belangrijk:** `src/hmac_key.h` staat in `.gitignore` en wordt niet gecommit. De key moet op de ESP en in de app identiek zijn. ### Serial monitor Baudrate: **115200** (USB CDC). De firmware stuurt heartbeat-statussen elke 5 seconden, logt statuswijzigingen, en print de HMAC key 5 seconden na opstarten: ``` === Atom VoiceS3R BLE Relay === LED blink done, starting BLE... BLE: advertising started as 'AtomS3R-Relay' Printing HMAC key in 5 seconds... HMAC Key: C3F17B612B93E82C56B055FA9B04E84664ACE1EC4E889F2763F271B65A5B54AF Enter this key in the Android app > Settings. Setup done. [heartbeat] relay=OFF touch=no ble=disconnected BLE: device connected Relay ON (touch=YES, ble=connected) BLE: relay ON (seq=1) BLE: relay OFF (seq=2) ``` ### Uploaden > **Download-mode:** Houd de reset-knop ~2 seconden ingedrukt tot de groene LED aangaat, laat dan los. Het board is nu in download-mode. ```bash # Zorg dat src/hmac_key.h bestaat (zo niet: python3 tools/generate_hmac_key.py) pio run --target upload ``` Na het uploaden treedt er een harde reset op. Als het board in download-mode blijft, USB eruit/erin of kort op reset tikken om in run-mode te komen. ## Android App ### Features - **Auto-connect:** De app scant automatisch bij opstarten naar het BLE-apparaat "AtomS3R-Relay" - **Auto-reconnect:** Bij wegvallen van de verbinding wordt na 2 seconden opnieuw gescant - **Hold-to-activate:** Houd de knop ingedrukt voor relais aan, loslaten voor relais uit (zelfde gedrag als de sensor) - **BLE Pairing:** De app initieert bonding (`createBond()`) na connectie; Just Works pairing verloopt zonder gebruikersinteractie - **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 - **USB auto-read:** Bij aansluiten van de ESP via USB-OTG wordt de HMAC key automatisch gelezen en opgeslagen (BroadcastReceiver) - **Instellingenscherm:** Losse Activity voor het invoeren van de HMAC key (met oog-icoon om de key te tonen/verbergen), "Scan & Verbind" knop, en hint-tekst - **Permissies:** Vraagt BLUETOOTH_SCAN, BLUETOOTH_CONNECT en ACCESS_FINE_LOCATION aan (Android 12+) ### Instellingenscherm 1. Open de app 2. Tik op het **tandwiel-icoon** rechtsboven 3. De app toont een hint: _"Voer de HMAC sleutel in of sluit de relaismodule aan op de telefoon."_ 4. **Optie A — handmatig:** Voer de 64-teken hex key in (zelfde als de ESP op serial print). Tik op het **oog-icoon** om de key te tonen/verbergen. Tik **Opslaan**. 5. **Optie B — automatisch:** Sluit de ESP via USB-OTG aan op de telefoon. Android vraagt USB-toestemming (1x). De key wordt automatisch gelezen en opgeslagen. Toast: "HMAC sleutel gelezen van ESP!" 6. Tik **Scan & Verbind** om op te slaan en direct BLE scannen te starten. De key wordt opgeslagen in SharedPreferences en overleeft app-herstarts. Bij geen key toont de hoofdapp "Geen HMAC sleutel - open Instellingen" en wordt er niet gescant. ### Hoofdscherm UI - **Tandwiel-icoon** rechtsboven → opent instellingenscherm - **HOLD-knop** centrum → houd ingedrukt voor relais aan - **Status tekst** linksonder (14sp) → toont verbindingsstatus ### Bouwen Vereisten: - Android SDK (API 34+) - Java 17+ (Java 26 werkt niet met Gradle 8.5 — gebruik Java 21) - Gradle 8.2.2 (via wrapper) - USB host support (usb-serial-for-android library via JitPack) ```bash cd android JAVA_HOME=/pad/naar/jdk21 ./gradlew assembleDebug ``` De APK staat na build in `android/app/build/outputs/apk/debug/app-debug.apk`. ### Installeren ```bash adb install -r android/app/build/outputs/apk/debug/app-debug.apk ``` Of via draadloze foutopsporing: ```bash adb connect : adb install -r android/app/build/outputs/apk/debug/app-debug.apk ``` ## Eerste keer setup 1. **Genereer HMAC key:** ```bash python3 tools/generate_hmac_key.py ``` Noteer de geprinte hex key. 2. **Flash de ESP:** ```bash pio run -t upload ``` 3. **Installeer de app op de telefoon:** ```bash adb install -r android/app/build/outputs/apk/debug/app-debug.apk ``` 4. **Voer de HMAC key in (kies één):** **Optie A — USB auto-read (aanbevolen):** - Sluit de ESP via USB-OTG aan op de telefoon - Android vraagt USB-toestemming (1x) — tik "Toestaan" - De key wordt automatisch gelezen en opgeslagen (Toast bevestigt) - Ontkoppel de ESP en zet hem op stroom **Optie B — handmatig:** - Open de app → tandwiel-icoon → voer de hex key in → Opslaan 5. **Scan & Verbind:** Tik in instellingen op "Scan & Verbind" (of de app scant automatisch bij terugkeren naar het hoofdscherm) 6. De app scant, paart (Just Works), en verbindt. Houd de HOLD-knop ingedrukt om het relais te schakelen. ## Projectstructuur ``` sylvia-touch-relay/ ├── platformio.ini # PlatformIO configuratie (ESP32-S3) ├── tools/ │ └── generate_hmac_key.py # CLI tool: genereert src/hmac_key.h ├── src/ │ ├── main.cpp # Firmware (BLE + HMAC + USB serial + sensor + relais) │ └── hmac_key.h # Gegenereerd, niet in git (.gitignore) ├── android/ │ ├── build.gradle # Root Gradle config │ ├── settings.gradle # JitPack repos voor usb-serial-for-android │ ├── gradle.properties │ ├── gradlew # Gradle wrapper │ ├── gradle/wrapper/ │ │ └── gradle-wrapper.properties │ └── app/ │ ├── build.gradle # App module config (+ usb-serial-for-android dep) │ └── src/main/ │ ├── AndroidManifest.xml │ ├── java/com/eddy/atomrelay/ │ │ ├── MainActivity.kt # BLE client + HMAC signing │ │ ├── SettingsActivity.kt # HMAC key invoer + scan knop │ │ └── UsbKeyReceiver.kt # USB auto-read BroadcastReceiver │ └── res/ │ ├── drawable/ │ │ ├── ic_eye_open.xml # Oog open icoon │ │ ├── ic_eye_closed.xml # Oog dicht icoon │ │ └── ic_settings.xml # Tandwiel icoon │ ├── layout/ │ │ ├── activity_main.xml # Hoofd UI (FrameLayout) │ │ └── activity_settings.xml # Key invoer + scan UI │ └── values/strings.xml ├── .gitignore └── README.md ``` ## 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. - G1 en G2 zitten op de HY2.0-4P Grove poort. G5-G8, G38, G39 zitten op de onderste expantie-header. - De firmware gebruikt geen M5Unified library — alleen Arduino core + ESP32 BLE + mbedtls. Dit maakt de serial output betrouwbaarder via USB CDC. - `ARDUINO_USB_CDC_ON_BOOT=1` is ingeschakeld voor virtuele serial via USB. - De HMAC key wordt via `#include "hmac_key.h"` in de firmware geincludeerd. Dit bestand is gegenereerd door `tools/generate_hmac_key.py` en staat in `.gitignore`. - De app gebruikt `WRITE_TYPE_NO_RESPONSE` voor minimale latency — de encryptie en HMAC verificatie gebeuren op de link layer resp. application layer en zijn onafhankelijk van het write type. - 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). - Für meerdere ESP modules met verschillende keys: genereer per module een eigen key, flash de firmware, en voer de juiste key in de app in. De app ondersteunt momenteel slechtséén key (opgeslagen in SharedPreferences). - De `UsbKeyReceiver` is een manifest-geregistreerde `BroadcastReceiver` voor `ACTION_USB_DEVICE_ATTACHED`. Hij filtert op Espressif (vendor 0x303a), vraagt USB-permission via `PendingIntent`, opent een CDC-ACM serial connectie (115200), stuurt `KEY?\n`, en slaat de respons op. Werkt ook als de app niet actief is. - De USB serial library (`usb-serial-for-android` v3.7.3 via JitPack) ondersteunt CDC-ACM (ESP32-S3), FTDI, CP210x, CH340, PL2303, en meer. - De `UsbKeyReceiver` doet serial I/O op een achtergrondthread (max 3s timeout) om ANR's te voorkomen.