diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..edc46f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md index 552b090..0961272 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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). +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 @@ -39,19 +39,9 @@ Relais - → GND ## Beveiliging -De relay is beveiligd tegen ongeautoriseerde toegang via twee onafhankelijke lagen: +De relay is beveiligd tegen ongeautoriseerde toegang via HMAC-SHA256 authenticatie op applicatieniveau. -### 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: +### 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 - Elk commando wordt ondertekend met `HMAC-SHA256(key, command || sequence)` @@ -78,7 +68,6 @@ De sequence counter reset naar 0 bij elke (re)connectie op de ESP. De app start - Als de ESP al een key heeft → ESP antwoordt `HMAC_KEY:` → 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:\n` → ESP slaat op in NVS en antwoordt `OK` → app slaat op lokaal op - Beide paden tonen dezelfde toast: **"Beveiligingssleutel gelezen"** -- Handmatige invoer: via het instellingenscherm in de Android app (64-teken hex string) - Zo kan dezelfde app gebruikt worden voor meerdere ESP modules — each met een eigen key #### USB Serial Protocol @@ -92,7 +81,7 @@ App → ESP: SET_KEY:C3F17B...AF\n (nieuwe key schrijven) ESP → App: OK\n (bevestiging) ``` -De ESP luistert in `loop()` voor USB serial data. Bij `KEY?\n` antwoordt hij met de hex key (`HMAC_KEY:`) of `NO_KEY`. Bij `SET_KEY:<64-hex>\n` slaat hij de key op in NVS en antwoordt `OK`. 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, en handelt het hele provisioning-proces automatisch af. +De ESP luistert in `loop()` voor USB serial data. Bij `KEY?\n` antwoordt hij met de hex key (`HMAC_KEY:`) of `NO_KEY`. Bij `SET_KEY:<64-hex>\n` slaat hij de key op in NVS en antwoordt `OK`. #### Provisioning flow (auto) @@ -113,20 +102,23 @@ De ESP luistert in `loop()` voor USB serial data. Bij `KEY?\n` antwoordt hij met App saves to prefs → toast ``` -De `UsbKeyReceiver` doet dit op een achtergrondthread met max 5 retries (1s pauze) om andere USB-apps te overleven die mogelijk het apparaat tijdelijk claimen. - ### 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) | +| **Replay** | Sequence counter — zelfde packet werkt niet 2x (`seq <= lastSeq` wordt geweerd) | +| **Ongeautoriseerd device** | HMAC vereist geheime sleutel (32-byte random via USB provisioning) | +| **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 é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. | | **NVS wiping** | `pio run -t erase` wist alle flash waaronder NVS — ESP is daarna leeg en kan opnieuw geprovisioneerd worden. | +### Waarom geen BLE-link encryptie? + +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 BLE library, HMAC door mbedtls (ingebouwd in ESP-IDF). @@ -141,7 +133,7 @@ Het relais gaat aan als **één van beide** bronnen actief is: | 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. +De touch sensor en BLE werken onafhankelijk. Als de sensor losgelaten wordt en de BLE-knop niet ingedrukt is, gaat het relais uit. ### BLE GATT @@ -150,7 +142,7 @@ De touch sensor en BLE werken onafzankelijk. Als de sensor losgelaten wordt en d | 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. +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. ### Key provisioning @@ -160,7 +152,7 @@ De firmware wordt zonder HMAC key gecompileerd. Bij de eerste opstart print de E No HMAC key provisioned — connect USB and run SET_KEY ``` -Sluit de ESP via USB aan op de telefoon met de app geinstalleerd. De app genereert automatisch een willekeurige key en stuurt deze naar de ESP. Na provisioning print de ESP bij opstarten: +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: ``` HMAC key loaded from NVS @@ -220,6 +212,19 @@ pio run -t erase && pio run -t 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 ook rechtstreeks via de Android-app worden geflashed, zonder computer: + +1. Zet de ESP in download-modus (houd reset ~2s in tot groene LED) +2. Sluit de ESP via USB-OTG aan op de telefoon +3. Open de app → Instellingen → Firmware Flashen +4. De app flasht bootloader, partitions, en firmware via USB +5. Dialog verdwijnt automatisch wanneer de USB wordt losgekoppeld +6. Sluit de ESP opnieuw aan → Sleutel Uitwisselen + +De firmware binaries worden via `tools/copy_firmware.sh` naar de app assets gekopieerd. + ## Android App ### Features @@ -227,21 +232,22 @@ Na het uploaden treedt er een harde reset op. Als het board in download-mode bli - **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 +- **Write Without Response:** Gebruikt `WRITE_TYPE_NO_RESPONSE` voor minimale latency — ON en OFF kunnen direct achter elkaar worden verstuurd zonder ACK-wachttijd - **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-provisioning:** Bij aansluiten van de ESP via USB-OTG 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"_ (BroadcastReceiver, werkt ook als app niet actief is) -- **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 +- **USB auto-provisioning:** Bij aansluiten van de ESP via USB-OTG 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"_ +- **Firmware flashen via app:** De app kan de ESP32 firmware rechtstreeks via USB flashen (download-modus), geen computer nodig +- **Instellingenscherm:** Losse Activity voor sleutel uitwisseling, firmware flashen, en buildnummer weergave - **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. Tik op het **oog-icoon** om de key te tonen/verbergen. Tik **Opslaan**. -5. **Optie B — automatisch (aanbevolen):** Sluit de ESP via USB-OTG aan op de telefoon. Android vraagt USB-toestemming (1x). De app leest de key uit (als de ESP er al een heeft) of genereert een nieuwe key en provisioneert de ESP. Toast: _"Beveiligingssleutel gelezen"_. -6. Tik **Scan & Verbind** om op te slaan en direct BLE scannen te starten. +3. De app toont twee acties: **Sleutel Uitwisselen** en **Firmware Flashen** +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. Toast: _"Beveiligingssleutel gelezen"_ +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. Dialog verdwijnt automatisch bij loskoppelen USB. +6. Onderaan staat het **Buildnummer** (timestamp van de build) 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. @@ -255,13 +261,19 @@ De key wordt opgeslagen in SharedPreferences en overleeft app-herstarts. Bij gee 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 (raw Android USB API, geen externe library) +- Java 17+ (Java 26 werkt niet met Gradle — gebruik Java 21) +- Gradle 8.14 (via wrapper) +- USB host support (raw Android USB API, geen externe serial library) +- PlatformIO (voor firmware compilatie) ```bash +# Firmware compileren en naar app assets kopiëren +pio run +bash tools/copy_firmware.sh + +# App bouwen en installeren cd android -JAVA_HOME=/pad/naar/jdk21 ./gradlew assembleDebug +JAVA_HOME=/pad/naar/jdk21 ./gradlew installDebug ``` De APK staat na build in `android/app/build/outputs/apk/debug/app-debug.apk`. @@ -281,42 +293,49 @@ adb install -r android/app/build/outputs/apk/debug/app-debug.apk ## Eerste keer setup -1. **Flash de ESP:** +1. **Firmware compileren:** + ```bash + pio run + bash tools/copy_firmware.sh + ``` + +2. **App op de telefoon installeren:** + ```bash + cd android + JAVA_HOME=/pad/naar/jdk21 ./gradlew installDebug + ``` + +3. **Firmware flashen (kies één):** + + **Optie A — Via de app (aanbevolen, geen computer nodig na eerste installatie):** + - 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:** ```bash pio run -t upload ``` -2. **Installeer de app op de telefoon:** - ```bash - adb install -r android/app/build/outputs/apk/debug/app-debug.apk - ``` - -3. **Provision de HMAC key (kies één):** - - **Optie A — USB auto-provisioning (aanbevolen):** - - Sluit de ESP via USB-OTG aan op de telefoon - - Android vraagt USB-toestemming (1x) — tik "Toestaan" - - De app detecteert dat de ESP geen key heeft, genereert er één, en stuurt deze naar de ESP - - Toast: _"Beveiligingssleutel gelezen"_ - - Ontkoppel de ESP en zet hem op stroom - - **Optie B — handmatig:** - - Open de app → tandwiel-icoon → voer een 64-teken hex key in → Opslaan - - Let op: de ESP moet dan dezelfde key hebben (via USB provisioning of handmatig via serial `SET_KEY:\n`) - - **Optie C — bestand 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 +4. **Sleutel uitwisselen:** + - 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"_ - **Nieuwe ESP, bestaande telefoon:** - - Wis de app data (instellingen → apps → Atom Relay → storage → clear) of de-installeer en herinstalleer - - Sluit de nieuwe ESP via USB aan → app genereert nieuwe key - - Let op: de oude key in de app wordt overschreven +5. **Gebruiken:** De app scant automatisch, verbindt, en toont "Connected". Houd de HOLD-knop ingedrukt om het relais te schakelen. -4. **Scan & Verbind:** Tik in instellingen op "Scan & Verbind" (of de app scant automatisch bij terugkeren naar het hoofdscherm) +### Nieuwe ESP, bestaande telefoon -5. De app scant, paart (Just Works), en verbindt. Houd de HOLD-knop ingedrukt om het relais te schakelen. +- 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 @@ -324,22 +343,32 @@ adb install -r android/app/build/outputs/apk/debug/app-debug.apk sylvia-touch-relay/ ├── platformio.ini # PlatformIO configuratie (ESP32-S3) ├── src/ -│ └── main.cpp # Firmware (BLE + HMAC + NVS key storage + USB serial + sensor + relais) +│ └── main.cpp # Firmware (BLE + HMAC + NVS + USB serial + sensor + relais) +├── tools/ +│ └── copy_firmware.sh # Kopieert firmware binaries naar app assets ├── android/ -│ ├── build.gradle # Root Gradle config -│ ├── settings.gradle # Gradle settings +│ ├── build.gradle # Root Gradle config +│ ├── settings.gradle # Gradle settings │ ├── gradle.properties -│ ├── gradlew # Gradle wrapper +│ ├── gradlew # Gradle wrapper │ ├── gradle/wrapper/ │ │ └── gradle-wrapper.properties │ └── app/ -│ ├── build.gradle # App module config +│ ├── build.gradle # App module config (esp32-flash-lib, minSdk 26) │ └── src/main/ │ ├── AndroidManifest.xml +│ ├── assets/firmware/ # GeCompileerde firmware binaries +│ │ ├── bootloader.bin +│ │ ├── partitions.bin +│ │ ├── firmware.bin +│ │ └── boot_app0.bin │ ├── java/com/eddy/atomrelay/ -│ │ ├── MainActivity.kt # BLE client + HMAC signing -│ │ ├── SettingsActivity.kt # HMAC key invoer + scan knop -│ │ └── UsbKeyReceiver.kt # USB key provisioning (raw USB API) +│ │ ├── MainActivity.kt # BLE client + HMAC signing +│ │ ├── SettingsActivity.kt # Key exchange + firmware flash UI +│ │ ├── EspFlasher.kt # ESP32 firmware flasher via USB +│ │ ├── UsbSerialTransport.kt # USB serial transport voor flasher +│ │ ├── UsbUtils.kt # Gedeelde USB utility functions +│ │ └── BuildConfig # Buildnummer (timestamp) │ └── res/ │ ├── drawable/ │ │ ├── ic_eye_open.xml # Oog open icoon @@ -347,7 +376,7 @@ sylvia-touch-relay/ │ │ └── ic_settings.xml # Tandwiel icoon │ ├── layout/ │ │ ├── activity_main.xml # Hoofd UI (FrameLayout) -│ │ └── activity_settings.xml # Key invoer + scan UI +│ │ └── activity_settings.xml # Key exchange + flash UI │ └── values/strings.xml ├── .gitignore └── README.md @@ -360,11 +389,10 @@ sylvia-touch-relay/ - 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 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 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 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. - 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: 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 SharedPreferences). Bij wisselen van ESP: wis app data of sluit de nieuwe ESP aan (overschrijft vorige key). -- De `UsbKeyReceiver` is een manifest-geregistreerde `BroadcastReceiver` voor `ACTION_USB_DEVICE_ATTACHED`. Hij filtert op Espressif (vendor 0x303a), vraagt USB-permission via `PendingIntent` (FLAG_MUTABLE voor Android 12+), opent een CDC-ACM serial connectie met raw USB API (geen externe library), configureert CDC control interface (line coding + DTR/RTS), en handelt het volledige provisioning-proces af. Werkt ook als de app niet actief is. -- De `UsbKeyReceiver` 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. +- 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 SharedPreferences). 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. Dit vermijdt compatibiliteitsproblemen met de ESP32-S3 USB-JTAG/serial controller die geen CDC `get_status` requests ondersteunt. - CDC control interface setup: line coding (115200 8N1) 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. -- De `UsbKeyReceiver` doet serial I/O op een achtergrondthread (max 5s timeout per poging, 5 retries met 1s pauze) om ANR's te voorkomen en andere USB-apps te overleven die mogelijk het apparaat tijdelijk claimen. -- Bij elke retry wordt het ESP-apparaat opnieuw opgezocht via `UsbManager.deviceList` om apparaat-pad-wijzigingen op te vangen. \ No newline at end of file +- 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. +- `minSdk` is 26 (Android 8.0). Android 7 en lager worden niet ondersteund. \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 02ad158..bc5e419 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -9,10 +9,15 @@ android { defaultConfig { applicationId "com.eddy.atomrelay" - minSdk 24 + minSdk 26 targetSdk 34 versionCode 1 versionName "1.0" + buildConfigField "String", "BUILD_NUMBER", "\"${new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(new Date())}\"" + } + + buildFeatures { + buildConfig true } buildTypes { diff --git a/android/app/src/main/assets/firmware/boot_app0.bin b/android/app/src/main/assets/firmware/boot_app0.bin new file mode 100644 index 0000000..13562ca Binary files /dev/null and b/android/app/src/main/assets/firmware/boot_app0.bin differ diff --git a/android/app/src/main/assets/firmware/bootloader.bin b/android/app/src/main/assets/firmware/bootloader.bin new file mode 100644 index 0000000..204061a Binary files /dev/null and b/android/app/src/main/assets/firmware/bootloader.bin differ diff --git a/android/app/src/main/assets/firmware/firmware.bin b/android/app/src/main/assets/firmware/firmware.bin new file mode 100644 index 0000000..2e5e5ad Binary files /dev/null and b/android/app/src/main/assets/firmware/firmware.bin differ diff --git a/android/app/src/main/assets/firmware/partitions.bin b/android/app/src/main/assets/firmware/partitions.bin new file mode 100644 index 0000000..951265f Binary files /dev/null and b/android/app/src/main/assets/firmware/partitions.bin differ diff --git a/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt index 6e88176..b48c39f 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt @@ -4,7 +4,6 @@ import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothGattCharacteristic @@ -12,10 +11,8 @@ import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothProfile import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.pm.PackageManager import android.os.Bundle import android.os.Handler @@ -55,7 +52,6 @@ class MainActivity : AppCompatActivity() { private var seq = 1 private var hmacKey: ByteArray? = null - private var pendingDevice: BluetoothDevice? = null private lateinit var statusText: TextView private lateinit var relayButton: Button @@ -72,43 +68,18 @@ class MainActivity : AppCompatActivity() { } } - private val bondReceiver = object : BroadcastReceiver() { - @SuppressLint("MissingPermission") - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action - if (action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) { - val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) - val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE) - Log.d(TAG, "Bond state changed: ${device?.address} -> $bondState") - if (bondState == BluetoothDevice.BOND_BONDED && device == pendingDevice) { - Log.d(TAG, "Bonded, discovering services...") - runOnUiThread { statusText.text = "Bonded, connecting..." } - bluetoothGatt?.discoverServices() - } - } - } - } - private val gattCallback = object : BluetoothGattCallback() { + @SuppressLint("MissingPermission") override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { Log.d(TAG, "GATT connected") bluetoothGatt = gatt - val device = gatt.device - pendingDevice = device - if (device.bondState == BluetoothDevice.BOND_BONDED) { - Log.d(TAG, "Already bonded, discovering services...") - runOnUiThread { statusText.text = "Connected, discovering..." } - gatt.discoverServices() - } else { - Log.d(TAG, "Not bonded, creating bond...") - runOnUiThread { statusText.text = "Pairing..." } - device.createBond() - } + runOnUiThread { statusText.text = "Verbonden, services zoeken..." } + gatt.discoverServices() } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { Log.d(TAG, "GATT disconnected") runOnUiThread { - statusText.text = "Reconnecting..." + statusText.text = "Verbinden..." relayButton.isEnabled = false } gatt.close() @@ -147,14 +118,6 @@ class MainActivity : AppCompatActivity() { runOnUiThread { statusText.text = "Service not found" } } } - - override fun onCharacteristicWrite( - gatt: BluetoothGatt, - characteristic: BluetoothGattCharacteristic, - status: Int - ) { - Log.d(TAG, "Write status: $status") - } } override fun onCreate(savedInstanceState: Bundle?) { @@ -169,15 +132,19 @@ class MainActivity : AppCompatActivity() { startActivityForResult(Intent(this, SettingsActivity::class.java), REQUEST_SETTINGS) } setupRelayButton() - - registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED), Context.RECEIVER_NOT_EXPORTED) } @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_SETTINGS && resultCode == Activity.RESULT_OK) { - handler.postDelayed({ startScan() }, 300) + stopScan() + bluetoothGatt?.close() + bluetoothGatt = null + relayCharacteristic = null + seq = 1 + loadHmacKey() + handler.postDelayed({ startScan() }, 500) } } @@ -300,7 +267,7 @@ class MainActivity : AppCompatActivity() { } @SuppressLint("MissingPermission") - private fun connectToDevice(device: BluetoothDevice) { + private fun connectToDevice(device: android.bluetooth.BluetoothDevice) { runOnUiThread { statusText.text = "Connecting..." } Log.d(TAG, "Connecting to ${device.address}") bluetoothGatt = device.connectGatt(this, false, gattCallback) @@ -348,6 +315,5 @@ class MainActivity : AppCompatActivity() { handler.removeCallbacksAndMessages(null) stopScan() bluetoothGatt?.close() - try { unregisterReceiver(bondReceiver) } catch (e: Exception) {} } } \ No newline at end of file diff --git a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt index 4b1e74b..996b56e 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt @@ -1,5 +1,6 @@ package com.eddy.atomrelay +import android.app.AlertDialog import android.app.PendingIntent import android.app.ProgressDialog import android.content.BroadcastReceiver @@ -17,6 +18,7 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.widget.Button +import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import java.nio.charset.StandardCharsets @@ -39,7 +41,8 @@ class SettingsActivity : AppCompatActivity() { private enum class UsbAction { NONE, EXCHANGE_KEY, FLASH } private var currentAction = UsbAction.NONE - private var dialog: ProgressDialog? = null + private var progressDialog: ProgressDialog? = null + private var dialog: AlertDialog? = null private val handler = Handler(Looper.getMainLooper()) private val usbAttachedReceiver = object : BroadcastReceiver() { @@ -80,9 +83,13 @@ class SettingsActivity : AppCompatActivity() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == UsbManager.ACTION_USB_DEVICE_DETACHED) { val device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) - if (device?.vendorId == ESP_VENDOR_ID && dialog != null && currentAction == UsbAction.NONE) { - dialog?.dismiss() - dialog = null + if (device?.vendorId == ESP_VENDOR_ID) { + if (dialog != null && currentAction == UsbAction.NONE) { + dialog?.dismiss() + dialog = null + } else if (currentAction != UsbAction.NONE) { + cleanupAction() + } } } } @@ -103,6 +110,8 @@ class SettingsActivity : AppCompatActivity() { startFlashing() } + findViewById(R.id.buildNumber).text = "Buildnummer: ${BuildConfig.BUILD_NUMBER}" + registerReceiver(usbAttachedReceiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED), Context.RECEIVER_NOT_EXPORTED) registerReceiver(usbPermissionReceiver, IntentFilter(ACTION_USB_PERMISSION), Context.RECEIVER_NOT_EXPORTED) registerReceiver(usbDetachedReceiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED), Context.RECEIVER_NOT_EXPORTED) @@ -198,27 +207,33 @@ class SettingsActivity : AppCompatActivity() { flasher.flashDevice(usbManager, device, object : EspFlasher.FlashCallback { override fun onProgress(percent: Int, message: String) { runOnUiThread { - dialog?.progress = percent - dialog?.setMessage(message) + progressDialog?.progress = percent + progressDialog?.setMessage(message) } } override fun onComplete() { runOnUiThread { - dialog?.setMessage("Firmware geflashed!\n\nOntkoppel de ESP en sluit opnieuw aan om de sleutel in te stellen.") - dialog?.setCancelable(true) - dialog?.setButton(ProgressDialog.BUTTON_NEUTRAL, "Sluiten") { _, _ -> - dialog?.dismiss() - dialog = null - cleanupAction() - } + currentAction = UsbAction.NONE + progressDialog?.dismiss() + progressDialog = null + dialog = AlertDialog.Builder(this@SettingsActivity) + .setTitle("Firmware Flashen") + .setMessage("Firmware geflashed!\n\nOntkoppel de ESP en sluit opnieuw aan om de sleutel in te stellen.") + .setPositiveButton("Sluiten") { d, _ -> + d.dismiss() + dialog = null + } + .setCancelable(true) + .setOnCancelListener { dialog = null } + .show() } } override fun onError(message: String) { runOnUiThread { - dialog?.dismiss() - dialog = null + progressDialog?.dismiss() + progressDialog = null Toast.makeText(this@SettingsActivity, "Flashen mislukt: $message", Toast.LENGTH_LONG).show() cleanupAction() } @@ -253,8 +268,8 @@ class SettingsActivity : AppCompatActivity() { } private fun showProgressDialog(title: String, message: String, style: Int) { - dialog?.dismiss() - dialog = ProgressDialog(this).apply { + progressDialog?.dismiss() + progressDialog = ProgressDialog(this).apply { setTitle(title) setMessage(message) setProgressStyle(style) @@ -271,6 +286,8 @@ class SettingsActivity : AppCompatActivity() { currentAction = UsbAction.NONE dialog?.dismiss() dialog = null + progressDialog?.dismiss() + progressDialog = null } private fun requestUsbPermission(usbManager: UsbManager, device: UsbDevice) { diff --git a/android/app/src/main/res/layout/activity_settings.xml b/android/app/src/main/res/layout/activity_settings.xml index 3946ad3..e87ccca 100644 --- a/android/app/src/main/res/layout/activity_settings.xml +++ b/android/app/src/main/res/layout/activity_settings.xml @@ -38,4 +38,17 @@ android:layout_height="wrap_content" android:text="Firmware Flashen" /> + + + + diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 2bbac7d..d4c12f2 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 5237e9c..4af519e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,7 +3,7 @@ #include #include #include -#include + #include #include @@ -150,20 +150,15 @@ void setup() { BLEDevice::init("AtomS3R-Relay"); - BLESecurity *pSecurity = new BLESecurity(); - pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND); - pSecurity->setCapability(ESP_IO_CAP_NONE); - pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK); - BLEServer *pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); BLEService *pService = pServer->createService(SERVICE_UUID); pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, - BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY + BLECharacteristic::PROPERTY_WRITE_NR | BLECharacteristic::PROPERTY_NOTIFY ); - pCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENCRYPTED); + pCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE); pCharacteristic->addDescriptor(new BLE2902()); pCharacteristic->setCallbacks(new MyCharacteristicCallbacks()); pCharacteristic->setValue("0"); diff --git a/tools/copy_firmware.sh b/tools/copy_firmware.sh new file mode 100755 index 0000000..e8242da --- /dev/null +++ b/tools/copy_firmware.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +BINARY_DIR="$(cd "$(dirname "$0")/.." && pwd)/.pio/build/m5stack-atoms3r" +ASSETS_DIR="$(cd "$(dirname "$0")/.." && pwd)/android/app/src/main/assets/firmware" + +mkdir -p "$ASSETS_DIR" + +cp "$BINARY_DIR/bootloader.bin" "$ASSETS_DIR/" +cp "$BINARY_DIR/partitions.bin" "$ASSETS_DIR/" +cp "$BINARY_DIR/firmware.bin" "$ASSETS_DIR/" + +BOOT_APP0=$(find ~/.platformio/packages/framework-arduinoespressif32 -name "boot_app0.bin" 2>/dev/null | head -1) +if [ -n "$BOOT_APP0" ]; then + cp "$BOOT_APP0" "$ASSETS_DIR/" +else + echo "ERROR: boot_app0.bin not found" + exit 1 +fi + +echo "Firmware binaries copied to $ASSETS_DIR:" +ls -la "$ASSETS_DIR" \ No newline at end of file