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 7b59d36bd7
All checks were successful
CI / build (pull_request) Successful in 1m3s
fix: cast CONFIG_LOG_DEFAULT_LEVEL to esp_log_level_t
2026-08-04 19:04:37 +02:00
.forgejo/workflows Remove version check from CI (needs Gradle, not available) 2026-08-03 16:55:32 +02:00
android Merge pull request 'fix: critical issues from code review' (#20) from fix/code-review-critical-issues into main 2026-08-04 18:44:55 +02:00
docs docs: add implementation plan for issue #16 2026-08-04 19:01:57 +02:00
scripts Merge pull request 'fix: remove outdated ble_client.py, add test_ota_notify.py to tools' (#22) from fix/cleanup-outdated-scripts into main 2026-08-04 18:48:54 +02:00
src fix: cast CONFIG_LOG_DEFAULT_LEVEL to esp_log_level_t 2026-08-04 19:04:37 +02:00
tools fix(script): filter ID response in query_id() 2026-08-04 19:02:51 +02:00
.gitignore chore: cleanup — .gitignore, track design docs 2026-08-01 00:52:03 +02:00
AGENTS.md docs: HS-S43A actief LOW, !t1Active logica gedocumenteerd 2026-08-04 01:21:36 +02:00
icon.png Replace HMAC-SHA256 with AES-256-GCM + ECDH key exchange for BLE relay commands (issue #2) 2026-08-03 04:34:26 +02:00
icon.svg Replace HMAC-SHA256 with AES-256-GCM + ECDH key exchange for BLE relay commands (issue #2) 2026-08-03 04:34:26 +02:00
LICENSE Convert Thread+Handler to lifecycle-aware coroutines, add GPL-3.0 license 2026-07-26 20:43:37 +02:00
platformio.ini Revert "Add auto-detected upload_port for both ESP32s" 2026-08-03 23:34:31 +02:00
README.md fix: critical issues from code review 2026-08-04 18:27:38 +02:00
version.txt Fix BLE scan + touch logic + UUID + version read 2026-08-03 16:38:26 +02:00

Dit project is vibecoded met DeepSeek v4 Pro en OpenCode 1.18.5

Project SylDa

BLE-gestuurde relais-switch voor de M5Stack Atom S3 Lite (ESP32-S3) met een bijbehorende Android-app. De relais kunnen worden bediend via capacitieve touch-sensoren (HS-S43A) ó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.

Hardware

Component Details
Relais 1 Op G1
Relais 2 Op G2
Touch sensor 1 HS-S43A Digital Capacitive Switch Module op G8 (actief LOW)
Touch sensor 2 HS-S43A Digital Capacitive Switch Module op G6 (actief LOW)
Knop Op G41 (active low)
Status LED Op G35 (WS2812 RGB, rood = geen verbinding, geel = verbinding <60s, uit = stabiel)

Pinout

G1 = RELAY1
G2 = RELAY2
G6 = TOUCH2 (INPUT)
G8 = TOUCH1 (INPUT)
G41 = BUTTON (INPUT, active low)
G35 = LED (WS2812 RGB, OUTPUT)

Aansluiting

Touch sensor GND → GND
Touch sensor OUT → G8 / G6
Touch sensor VCC → 3.3V

Relais + → G1 / G2
Relais - → GND

HS-S43A touch sensor: Digitale output (actief LOW — pin naar GND bij aanraking). Gebruik INPUT_PULLUP modus. De sensor trekt de pin actief laag bij detectie.

LED-signalering

De LED fungeert als debug-indicator en schakelt uit na 60 seconden stabiele verbinding:

State LED Prioriteit
OTA in progress Groen knipperend (100ms) 1 (hoogst)
OTA completed, wacht op confirm Geel vast 1
Geen verbinding Rood 2
Verbinding actief (< 60s) Geel 3
Verbinding stabiel (>= 60s) Uit 4 (laagst)
Relais-flash overlay Groen 150ms overlay

De relais-flash toont kort groen bij een verandering in relais-status, waarna de LED terugkeert naar de verbindingskleur.

Beveiliging

De relay is beveiligd tegen ongeautoriseerde toegang via HMAC-SHA256 authenticatie op applicatieniveau.

HMAC-SHA256 Authenticatie (application-level)

  • 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)
  • De module 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      0x00=relay1 OFF, 0x01=ON, 0x02=relay2 ON, 0x03=OFF
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 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 SharedPreferences 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

  • 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 module via de Arduino Preferences library (namespace hmac, key key, partitie op 0x9000-0xDFFF)
  • De app slaat de key op in SharedPreferences (overleeft app-herstarts)
  • 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 module al een key heeft → module antwoordt HMAC_KEY:<hex> → 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

USB Serial Protocol

App → module:  VERSION?\n
module → App:  BUILD_NR:1\n                  (firmware versie check)

App → module:  KEY?\n
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)

Provisioning flow

  Android app                           ESP32 (USB CDC)
  ───────────                           ───────────────
  User taps "Firmware bijwerken"
  Check version: VERSION?\n ───────►
                            ◄─────────── BUILD_NR:<version>\n
  (flash if needed, module reboots)
  Key exchange: KEY?\n ──────────►
                            ◄─────────── HMAC_KEY:<hex>\n
                                         DEVICE_ID:<hex>\n
                                         (of NO_KEY\n als leeg)
  (if NO_KEY) SET_KEY + SET_ID ─► Save to NVS

Beveiligingsanalyse

Aanval Hoe geweerd
Replay Sequence counter — zelfde packet werkt niet 2x (seq <= lastSeq wordt geweerd)
Ongeautoriseerd device HMAC vereist geheime sleutel (32-byte random via USB provisioning)
BLE spoofing (client) Bekend lek (issue #10): ESP32 client scant op naam "SylDa_Server" i.p.v. UUID — geen server-verificatie. De Android app scant wél op service UUID én verifieert device UUID na connect.
Commando vervalsing HMAC dekt command byte + sequence — wijzigen maakt handtekening ongeldig
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.
Reverse engineering van key Sleutel staat niet in de APK — wordt pas bij USB provisioning gegenereerd en in SharedPreferences opgeslagen. Extractie vereist fysieke toegang tot de telefoon.
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 — module is daarna leeg en kan opnieuw geprovisioneerd worden.

BLE-link encryptie (bonding/Just Works) vereist complexe pairing flows die op Android sliding bugs veroorzaken (write queues, GATT_INSUFFICIENT_AUTHENTICATION handling, state leaks). HMAC biedt authenticatie en replay-bescherming op applicatieniveau — voldoende voor een relais. Het enige wat link-encryptie extra biedt is confidentiality (afluisteren), wat voor een aan/uit commando irrelevant is. De HMAC key wordt fysiek via USB uitgewisseld, nooit via BLE.

Firmware

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). Er zijn twee PlatformIO environments:

Environment Bronbestand Rol
s3lite-server src/main.cpp Ontvangt BLE-verbindingen, bedient relais
s3lite-client src/client.cpp Verbindt als BLE-central met server, stuurt relais-commando's

Firmwareversies worden beheerd via version.txt (single source of truth). PlatformIO genereert src/version.h automatisch via een pre-build script.

Logica

Elk relais gaat aan als één van beide bronnen actief is:

Relais Bron Voorwaarde Resultaat
Relais 1 Touch sensor 1 G8 = HIGH (aanraken) Relais 1 aan
Relais 1 BLE command App stuurt HMAC-ondertekend 0x01 Relais 1 aan
Relais 2 Touch sensor 2 G6 = HIGH (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 sensoren en BLE werken onafhankelijk. 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
Relay Characteristic beb5483e-36e1-4688-b7f5-ea07361f26d8
UUID Characteristic beb5483e-36e1-4688-b7f5-ea07361f26d9

De relay characteristic ondersteunt WRITE_NR (Write Without Response). De UUID characteristic is read-only en bevat het unieke device-ID van de module.

Key provisioning

De firmware wordt zonder HMAC key gecompileerd. Bij de eerste opstart print de module:

No HMAC key provisioned — connect USB and run SET_KEY

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

Key wissen (factory reset): Om de module terug te zetten naar ongeprovisioneerd:

pio run -t erase

Dit wist alle flash inclusief NVS. Na opnieuw flashen start de module weer zonder key.

Serial monitor

Baudrate: 115200 (USB CDC). De firmware stuurt heartbeat-statussen elke 5 seconden, logt statuswijzigingen, en print de HMAC key (of de afwezigheid ervan) bij opstarten.

Met geprovisioneerde key:

=== SylDa BLE Relay ===
HMAC key loaded from NVS
LED red (no connection), starting BLE...
BLE: advertising started as 'SylDa_Server' (start=1, isAdv=1)
Setup done.
[heartbeat] R1=0 R2=0 t1=1 t2=1 btn=1 ble1=0 ble2=0
BLE: device connected
BLE: relay1 ON (seq=1)
BLE: relay1 OFF (seq=2)

Zonder key (nieuwe/gewiste module):

=== SylDa BLE Relay ===
No HMAC key provisioned — connect USB and run SET_KEY
LED red (no connection), starting BLE...
BLE: advertising started as 'SylDa_Server' (start=1, isAdv=1)
No HMAC key — use USB SET_KEY to provision
Setup done.

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

Download-mode: Houd de reset-knop ~2 seconden ingedrukt tot de groene LED aangaat, laat dan los. Het board is nu in download-mode.

# Flash server firmware (behoudt NVS/key)
pio run -e s3lite-server --target upload

# Flash client firmware (behoudt NVS/key)
pio run -e s3lite-client --target upload

# Volledig wissen (incl. NVS/key) + opnieuw flashen
pio run -t erase && pio run -e s3lite-server --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.

Firmware flashen via de app

De firmware kan rechtstreeks via de Android-app worden geflashed, geen computer nodig:

  1. Sluit de module via USB-OTG aan op de telefoon
  2. Open de app → Instellingen → Firmware bijwerken
  3. De app checkt de firmware versie. Als de versie actueel is, wordt het flashen overgeslagen
  4. Is de firmware verouderd, dan flasht de app bootloader, partitions, en firmware
  5. Na het flashen herstart de module en volgt automatisch de key exchange

De firmware binaries worden via tools/copy_firmware.sh naar de app assets gekopieerd (assets/firmware/server/ en assets/firmware/client/).

Android App

Features

  • 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
  • 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
  • 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 512 aan na service discovery (voor 37-byte HMAC packets)
  • HMAC authenticatie: Elk commando wordt ondertekend met HMAC-SHA256 + sequence counter
  • Key exchange geïntegreerd: De "Firmware bijwerken" knop checkt versie, flasht indien nodig, en wisselt automatisch sleutels uit
  • Thema-selector: Instellingen → Weergave kiest Systeem, Licht of Donker
  • Permissies: Vraagt BLUETOOTH_SCAN en BLUETOOTH_CONNECT aan. Bij weigeren wordt de knop uitgeschakeld en verwezen naar Instellingen

Instellingenscherm

  1. Open de app → tik op instellingen (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. Weergave: Kies Systeem (volgt telefoon), Licht of Donker
  4. Relais labels: Stel eigen namen in voor de twee relais knoppen

De key wordt opgeslagen in SharedPreferences en overleeft app-herstarts. Bij geen key toont de hoofdapp "Geen sleutel — open Instellingen" en wordt er niet gescant.

Hoofdscherm UI

  • instellingen tekst + tandwiel-icoon rechtsboven → opent instellingenscherm
  • Twee knoppen centrum → hold-to-activate voor relais 1 en 2
  • Status tekst linksonder (14sp) → toont verbindingsstatus

Bouwen

Vereisten:

  • Android SDK (API 34+)
  • Java 21 (Java 26 werkt niet met Gradle)
  • Gradle 8.14 (via wrapper)
  • AGP 8.5.2, Kotlin 2.0.21 (K2 compiler)
  • USB host support (raw Android USB API, geen externe serial library)
  • PlatformIO (voor firmware compilatie)
# Firmware compileren en naar app assets kopiëren
pio run
bash tools/copy_firmware.sh

# App bouwen en installeren
cd android
JAVA_HOME=/home/linuxbrew/.linuxbrew/opt/openjdk@21/libexec ./gradlew installDebug

De APK staat na build in android/app/build/outputs/apk/debug/app-debug.apk.

Installeren

adb -s <ip>:5555 install -r android/app/build/outputs/apk/debug/app-debug.apk

Eerste keer setup

  1. App op de telefoon installeren:

    cd android
    JAVA_HOME=/home/linuxbrew/.linuxbrew/opt/openjdk@21/libexec ./gradlew installDebug
    
  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
  3. Gebruiken: De app scant automatisch, verbindt, en toont "Verbonden". Houd een knop ingedrukt om het relais te schakelen.

Nieuwe module, bestaande telefoon

  • Sluit de nieuwe module via USB aan → Firmware bijwerken → app flasht firmware en genereert nieuwe key

Bestaande module, nieuwe 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

Projectstructuur

Project_SylDa/
├── platformio.ini              # PlatformIO configuratie (ESP32-S3, server + client environments)
├── version.txt                 # Single source of truth voor firmwareversies
├── src/
│   ├── main.cpp                # Server firmware (BLE + HMAC + NVS + USB serial + touch + relais)
│   ├── client.cpp              # Client firmware (BLE central + touch + relais commando's)
│   └── version.h               # Auto-gegenereerd uit version.txt — niet handmatig bewerken
├── tools/
│   ├── copy_firmware.sh        # Kopieert firmware binaries naar app assets
│   └── pio_version_pre.py      # PlatformIO pre-build script (genereert version.h)
├── android/
│   ├── build.gradle             # Root Gradle config
│   ├── settings.gradle          # Gradle settings
│   ├── gradle.properties
│   ├── gradlew                  # Gradle wrapper
│   └── app/
│       ├── build.gradle         # App module (nl.eddydevink.sylda, minSdk 26)
│       └── src/main/
│           ├── AndroidManifest.xml
│           ├── assets/firmware/
│           │   ├── server/
│           │   │   ├── bootloader.bin
│           │   │   ├── partitions.bin
│           │   │   ├── firmware.bin
│           │   │   └── boot_app0.bin
│           │   └── client/
│           │       ├── bootloader.bin
│           │       ├── partitions.bin
│           │       ├── firmware.bin
│           │       └── boot_app0.bin
│           ├── java/nl/eddydevink/sylda/
│           │   ├── MainActivity.kt
│           │   ├── SettingsActivity.kt
│           │   ├── BleRelayManager.kt
│           │   ├── BleStatus.kt
│           │   ├── EspFlasher.kt
│           │   ├── OtaManager.kt
│           │   ├── SylDaVersions.kt
│           │   ├── UsbSerialTransport.kt
│           │   ├── UsbUtils.kt
│           │   └── PrefsHelper.kt
│           ├── res/
│           │   ├── drawable/
│           │   │   └── ic_settings.xml
│           │   ├── layout/
│           │   │   ├── activity_main.xml
│           │   │   ├── activity_settings.xml
│           │   │   ├── dialog_waiting.xml
│           │   │   └── dialog_progress.xml
│           │   ├── values/strings.xml
│           │   └── xml/
│           │       └── device_filter.xml
├── .gitignore
├── LICENSE
└── README.md

Ontwerpbeslissingen

WRITE_TYPE_NO_RESPONSE (geen BLE write ACK)

De app gebruikt WRITE_TYPE_NO_RESPONSE (Write Without Response) in plaats van WRITE_TYPE_DEFAULT (Write With Response). Eerdere versies gebruikten ACK's, maar de BLE write acknowledgement duurde lang genoeg dat de gebruiker de knop kon loslaten vóór de ACK arriveerde. Als ACTION_UP plaatsvond tijdens een pending write, werd het loslaten genegeerd en bleven het relais en de app hangen in de ON-status. WRITE_TYPE_NO_RESPONSE verwijdert die latency en maakt directe ON→OFF sequenties mogelijk. De UI reflecteert de intentie van de gebruiker ("ingedrukt" / "losgelaten") in plaats van bevestigde relais-state — een aanvaardbare afweging voor een hold-to-activate interactie.

compileSdk/targetSdk 34 (niet 35 / Android 15)

De doelgroep bestaat uit lichamelijk gehandicapte gebruikers die zeer terughoudend zijn met het updaten van hun telefoon. Door te blijven op API 34 (Android 14) wordt de compatibiliteit met zoveel mogelijk apparaten gemaximaliseerd. Heroverweeg wanneer de gebruikersbasis voldoende is gemigreerd.

Thema-selector en DayNight

De app gebruikt Theme.AppCompat.DayNight in plaats van het eerder geforceerde Theme.AppCompat.Light. De instellingen bieden 3 opties: Systeem (volgt telefoon), Licht en Donker. Gekozen is voor de standaard Android-best-practice: geen eigen font-slider of contrast-kleurenkiezer, omdat Android's systeem-lettergrootte-instelling (sp-eenheden schalen automatisch) en hoog-contrast modus (Android 13+) al voldoen voor de doelgroep van visueel beperkte gebruikers. Een eigen implementatie zou het wiel opnieuw uitvinden en onnodige complexiteit toevoegen.

Technische notities

  • De Atom S3 Lite heeft GPIO G1, G2, G5-G8, G38, G39 beschikbaar op de headers. G41 is de interne knop (active low). G46 is de interne LED.
  • 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 NimBLE + mbedtls.
  • ARDUINO_USB_CDC_ON_BOOT=1 is ingeschakeld voor virtuele serial via USB.
  • De HMAC key wordt opgeslagen in NVS (namespace hmac, key key, partitie 0x9000-0xDFFF). Via pio run -t erase kan NVS gewist worden.
  • Firmwareversies worden beheerd via version.txt — src/version.h wordt automatisch gegenereerd, nooit handmatig bewerken.
  • 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.
  • De app gebruikt raw Android USB API (UsbDeviceConnection.bulkTransfer) in plaats van een externe serial library.
  • minSdk is 26 (Android 8.0).
  • ProgressDialog is vervangen door eigen AlertDialog layouts (dialog_waiting.xml, dialog_progress.xml).
  • De HMAC key in de app is opgeslagen via SharedPreferences.
  • De app gebruikt Theme.AppCompat.DayNight met Systeem/Licht/Donker thema-selector.
  • compileSdk/targetSdk blijft op 34 (Android 14) voor maximale compatibiliteit met de doelgroep.

Licentie

Dit project is gelicentieerd onder de GNU General Public License v3.0 of later (GPL-3.0-or-later).

Deze licentie is vereist omdat de app gebruik maakt van esp32-flash-lib (GPL-3.0), een library voor het flashen van ESP32 firmware. De GPL-3.0 copyleft-clausule bepaalt dat een werk dat aan een GPL-3.0 werk linkt zelf ook onder GPL-3.0 (of een compatibele licentie) moet worden gedistribueerd.

Zie het LICENSE bestand voor de volledige licentietekst.