No description
  • Kotlin 58%
  • C++ 31.3%
  • Python 5.2%
  • Shell 5.2%
  • C 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
eddy c707c21d7f Rewrite USB key reader: raw USB API + retry logic
- 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
2026-07-26 09:02:41 +02:00
android Rewrite USB key reader: raw USB API + retry logic 2026-07-26 09:02:41 +02:00
src Add USB auto-read key transfer + UI redesign 2026-07-26 08:39:28 +02:00
tools Add BLE security: Just Works pairing + HMAC-SHA256 authentication 2026-07-26 08:18:52 +02:00
.gitignore Add BLE security: Just Works pairing + HMAC-SHA256 authentication 2026-07-26 08:18:52 +02:00
platformio.ini Initial commit: BLE relay controller for Atom VoiceS3R 2026-07-26 01:17:31 +02:00
README.md Add USB auto-read key transfer + UI redesign 2026-07-26 08:39:28 +02:00

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:

  • 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:<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.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.

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

  1. Genereer HMAC key:

    python3 tools/generate_hmac_key.py
    

    Noteer de geprinte hex key.

  2. Flash de ESP:

    pio run -t upload
    
  3. Installeer de app op de telefoon:

    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.