- Kotlin 58%
- C++ 31.3%
- Python 5.2%
- Shell 5.2%
- C 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- Replaced usb-serial-for-android library with raw Android USB API (UsbDeviceConnection.bulkTransfer) — the library's testConnection() sent a get_status CDC request that the ESP32-S3 USB-JTAG/serial controller does not support, causing read failures - Added CDC control interface setup (line coding + DTR/RTS) to enable data flow before sending KEY? command - Added retry logic: 5 attempts, 1s delay between retries, to handle other apps grabbing the USB device first (JTAG/serial debug unit triggers multiple system dialogs before our app gets permission) - Re-lookup ESP device on each retry to handle device path changes - Removed usb-serial-for-android dependency and JitPack repository |
||
| android | ||
| src | ||
| tools | ||
| .gitignore | ||
| platformio.ini | ||
| README.md | ||
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?\nmetHMAC_KEY:<hex>. 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:
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:
echo "y" | python3 tools/generate_hmac_key.py
Belangrijk:
src/hmac_key.hstaat in.gitignoreen 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.
# 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
- Open de app
- Tik op het tandwiel-icoon rechtsboven
- De app toont een hint: "Voer de HMAC sleutel in of sluit de relaismodule aan op de telefoon."
- 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.
- 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!"
- 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)
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
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
Of via draadloze foutopsporing:
adb connect <ip>:<poort>
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
Eerste keer setup
-
Genereer HMAC key:
python3 tools/generate_hmac_key.pyNoteer de geprinte hex key.
-
Flash de ESP:
pio run -t upload -
Installeer de app op de telefoon:
adb install -r android/app/build/outputs/apk/debug/app-debug.apk -
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
-
Scan & Verbind: Tik in instellingen op "Scan & Verbind" (of de app scant automatisch bij terugkeren naar het hoofdscherm)
-
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=1is ingeschakeld voor virtuele serial via USB.- De HMAC key wordt via
#include "hmac_key.h"in de firmware geincludeerd. Dit bestand is gegenereerd doortools/generate_hmac_key.pyen staat in.gitignore. - De app gebruikt
WRITE_TYPE_NO_RESPONSEvoor 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 <= 0is true in deseq <= lastSeqcheck). - 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
UsbKeyReceiveris een manifest-geregistreerdeBroadcastReceivervoorACTION_USB_DEVICE_ATTACHED. Hij filtert op Espressif (vendor 0x303a), vraagt USB-permission viaPendingIntent, opent een CDC-ACM serial connectie (115200), stuurtKEY?\n, en slaat de respons op. Werkt ook als de app niet actief is. - De USB serial library (
usb-serial-for-androidv3.7.3 via JitPack) ondersteunt CDC-ACM (ESP32-S3), FTDI, CP210x, CH340, PL2303, en meer. - De
UsbKeyReceiverdoet serial I/O op een achtergrondthread (max 3s timeout) om ANR's te voorkomen.