Add USB auto-read key transfer + UI redesign
USB auto-read: - ESP responds to 'KEY?\n' over USB serial with 'HMAC_KEY:<hex>' - Android UsbKeyReceiver (manifest BroadcastReceiver) catches USB_DEVICE_ATTACHED - Filters on Espressif vendor 0x303a, requests USB permission - Opens CDC-ACM serial (115200), sends KEY?, parses response, saves to SharedPreferences - Works even when app not in foreground - usb-serial-for-android v3.7.3 via JitPack UI redesign: - activity_main.xml: FrameLayout with settings icon top-right, relay button center, status bottom-left (14sp) - Scan & Connect button moved to SettingsActivity - SettingsActivity: hint text 'Voer de HMAC sleutel in of sluit de relaismodule aan op de telefoon' - SettingsActivity: scan button saves key + returns RESULT_OK to trigger scanning - MainActivity: removed scanButton, auto-scan on onResume when key is set - Back button removed from settings (ActionBar up navigation only) README updated with USB protocol docs, new UI description, and updated setup guide
This commit is contained in:
parent
7f68e9ad70
commit
4518aec34e
10 changed files with 270 additions and 74 deletions
95
README.md
95
README.md
|
|
@ -53,7 +53,7 @@ De relay is beveiligd tegen ongeautoriseerde toegang via twee onafhankelijke lag
|
||||||
|
|
||||||
Omdat BLE Just Works geen MITM-bescherming biedt, is er een tweede laag toegevoegd:
|
Omdat BLE Just Works geen MITM-bescherming biedt, is er een tweede laag toegevoegd:
|
||||||
|
|
||||||
- Both de ESP firmware en de Android app bevatten een **32-byte shared secret** (HMAC key)
|
- 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)`
|
- Elk commando wordt ondertekend met `HMAC-SHA256(key, command || sequence)`
|
||||||
- De ESP verifieert de handtekening voordat het relais schakelt
|
- De ESP verifieert de handtekening voordat het relais schakelt
|
||||||
- Een **sequence counter** voorkomt replay-aanvallen: elk packet moet een strikt oplopend volgnummer hebben
|
- Een **sequence counter** voorkomt replay-aanvallen: elk packet moet een strikt oplopend volgnummer hebben
|
||||||
|
|
@ -74,10 +74,20 @@ De sequence counter reset naar 0 bij elke (re)connectie op de ESP. De app start
|
||||||
- De HMAC key staat in `src/hmac_key.h` (niet in git, `.gitignore`)
|
- 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`
|
- Een CLI-tool genereert een willekeurige key: `python3 tools/generate_hmac_key.py`
|
||||||
- De ESP print de key 5 seconden na opstarten naar serial
|
- De ESP print de key 5 seconden na opstarten naar serial
|
||||||
- De gebruiker voert de key eenmalig in via het instellingenscherm in de Android app
|
- **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)
|
- 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
|
- 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
|
### Beveiligingsanalyse
|
||||||
|
|
||||||
| Aanval | Hoe geweerd |
|
| Aanval | Hoe geweerd |
|
||||||
|
|
@ -169,29 +179,38 @@ Na het uploaden treedt er een harde reset op. Als het board in download-mode bli
|
||||||
- **BLE Pairing:** De app initieert bonding (`createBond()`) na connectie; Just Works pairing verloopt zonder gebruikersinteractie
|
- **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)
|
- **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
|
- **HMAC authenticatie:** Elk commando wordt ondertekend met HMAC-SHA256 + sequence counter
|
||||||
- **Instellingenscherm:** Losse Activity voor het invoeren van de HMAC key (met oog-icoon om de key te tonen/verbergen)
|
- **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+)
|
- **Permissies:** Vraagt BLUETOOTH_SCAN, BLUETOOTH_CONNECT en ACCESS_FINE_LOCATION aan (Android 12+)
|
||||||
|
|
||||||
### Instellingenscherm
|
### Instellingenscherm
|
||||||
|
|
||||||
1. Open de app
|
1. Open de app
|
||||||
2. Tik op het **tandwiel-icoon** rechtsboven
|
2. Tik op het **tandwiel-icoon** rechtsboven
|
||||||
3. Voer de 64-teken hex key in (zelfde als de ESP op serial print)
|
3. De app toont een hint: _"Voer de HMAC sleutel in of sluit de relaismodule aan op de telefoon."_
|
||||||
4. Tik op het **oog-icoon** om de key te tonen/verbergen
|
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. 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 app "Geen HMAC sleutel - open Instellingen" en wordt er niet gescant.
|
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
|
### Bouwen
|
||||||
|
|
||||||
Vereisten:
|
Vereisten:
|
||||||
- Android SDK (API 34+)
|
- Android SDK (API 34+)
|
||||||
- Java 17+ (getest met Java 17 en 21; Java 26 werkt niet met Kotlin 1.9 daemon — valt terug op non-incremental)
|
- Java 17+ (Java 26 werkt niet met Gradle 8.5 — gebruik Java 21)
|
||||||
- Gradle 8.2.2 (via wrapper)
|
- Gradle 8.2.2 (via wrapper)
|
||||||
|
- USB host support (usb-serial-for-android library via JitPack)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd android
|
cd android
|
||||||
./gradlew assembleDebug
|
JAVA_HOME=/pad/naar/jdk21 ./gradlew assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
De APK staat na build in `android/app/build/outputs/apk/debug/app-debug.apk`.
|
De APK staat na build in `android/app/build/outputs/apk/debug/app-debug.apk`.
|
||||||
|
|
@ -222,29 +241,23 @@ adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
pio run -t upload
|
pio run -t upload
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Lees de HMAC key uit serial** (5 seconden na opstarten):
|
3. **Installeer de app op de telefoon:**
|
||||||
```bash
|
|
||||||
# Via python:
|
|
||||||
python3 -c "import serial,time; s=serial.Serial('/dev/ttyACM0',115200); [print(l) for l in iter(lambda: s.readline().decode(errors='replace').strip() or None, None) if 'HMAC Key:' in l] if False else exec('''
|
|
||||||
import serial, time
|
|
||||||
s = serial.Serial(\"/dev/ttyACM0\", 115200, timeout=2)
|
|
||||||
start = time.time()
|
|
||||||
while time.time() - start < 15:
|
|
||||||
d = s.read(1024)
|
|
||||||
if d:
|
|
||||||
for line in d.decode(errors=\"replace\").splitlines():
|
|
||||||
if line.strip(): print(line.strip())
|
|
||||||
if b\"HMAC Key:\" in d: break
|
|
||||||
s.close()
|
|
||||||
''')"
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Installeer de app op de telefoon:**
|
|
||||||
```bash
|
```bash
|
||||||
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Open de app → tandwiel-icoon → voer key in → Opslaan**
|
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.
|
6. De app scant, paart (Just Works), en verbindt. Houd de HOLD-knop ingedrukt om het relais te schakelen.
|
||||||
|
|
||||||
|
|
@ -256,30 +269,31 @@ sylvia-touch-relay/
|
||||||
├── tools/
|
├── tools/
|
||||||
│ └── generate_hmac_key.py # CLI tool: genereert src/hmac_key.h
|
│ └── generate_hmac_key.py # CLI tool: genereert src/hmac_key.h
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── main.cpp # Firmware (BLE + HMAC + sensor + relais)
|
│ ├── main.cpp # Firmware (BLE + HMAC + USB serial + sensor + relais)
|
||||||
│ └── hmac_key.h # Gegenereerd, niet in git (.gitignore)
|
│ └── hmac_key.h # Gegenereerd, niet in git (.gitignore)
|
||||||
├── android/
|
├── android/
|
||||||
│ ├── build.gradle # Root Gradle config
|
│ ├── build.gradle # Root Gradle config
|
||||||
│ ├── settings.gradle
|
│ ├── settings.gradle # JitPack repos voor usb-serial-for-android
|
||||||
│ ├── gradle.properties
|
│ ├── gradle.properties
|
||||||
│ ├── gradlew # Gradle wrapper
|
│ ├── gradlew # Gradle wrapper
|
||||||
│ ├── gradle/wrapper/
|
│ ├── gradle/wrapper/
|
||||||
│ │ └── gradle-wrapper.properties
|
│ │ └── gradle-wrapper.properties
|
||||||
│ └── app/
|
│ └── app/
|
||||||
│ ├── build.gradle # App module config
|
│ ├── build.gradle # App module config (+ usb-serial-for-android dep)
|
||||||
│ └── src/main/
|
│ └── src/main/
|
||||||
│ ├── AndroidManifest.xml
|
│ ├── AndroidManifest.xml
|
||||||
│ ├── java/com/eddy/atomrelay/
|
│ ├── java/com/eddy/atomrelay/
|
||||||
│ │ ├── MainActivity.kt # BLE client + HMAC signing
|
│ │ ├── MainActivity.kt # BLE client + HMAC signing
|
||||||
│ │ └── SettingsActivity.kt # HMAC key invoer + oog toggle
|
│ │ ├── SettingsActivity.kt # HMAC key invoer + scan knop
|
||||||
|
│ │ └── UsbKeyReceiver.kt # USB auto-read BroadcastReceiver
|
||||||
│ └── res/
|
│ └── res/
|
||||||
│ ├── drawable/
|
│ ├── drawable/
|
||||||
│ │ ├── ic_eye_open.xml # Oog open icoon
|
│ │ ├── ic_eye_open.xml # Oog open icoon
|
||||||
│ │ ├── ic_eye_closed.xml # Oog dicht icoon
|
│ │ ├── ic_eye_closed.xml # Oog dicht icoon
|
||||||
│ │ └── ic_settings.xml # Tandwiel icoon
|
│ │ └── ic_settings.xml # Tandwiel icoon
|
||||||
│ ├── layout/
|
│ ├── layout/
|
||||||
│ │ ├── activity_main.xml # Hoofd UI
|
│ │ ├── activity_main.xml # Hoofd UI (FrameLayout)
|
||||||
│ │ └── activity_settings.xml # Key invoer UI
|
│ │ └── activity_settings.xml # Key invoer + scan UI
|
||||||
│ └── values/strings.xml
|
│ └── values/strings.xml
|
||||||
├── .gitignore
|
├── .gitignore
|
||||||
└── README.md
|
└── README.md
|
||||||
|
|
@ -294,4 +308,7 @@ sylvia-touch-relay/
|
||||||
- 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 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 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).
|
- 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).
|
- 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.
|
||||||
|
|
@ -34,4 +34,5 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'com.github.mik3y:usb-serial-for-android:3.7.3'
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||||
|
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|
@ -29,5 +30,13 @@
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:label="Instellingen"
|
android:label="Instellingen"
|
||||||
android:parentActivityName=".MainActivity"/>
|
android:parentActivityName=".MainActivity"/>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".UsbKeyReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -2,6 +2,7 @@ package com.eddy.atomrelay
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothGatt
|
import android.bluetooth.BluetoothGatt
|
||||||
|
|
@ -40,8 +41,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
private val CHARACTERISTIC_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d8")
|
private val CHARACTERISTIC_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d8")
|
||||||
private val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
private val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
||||||
private const val DEVICE_NAME = "AtomS3R-Relay"
|
private const val DEVICE_NAME = "AtomS3R-Relay"
|
||||||
private const val HMAC_PKT_SIZE = 37 // 1 cmd + 4 seq + 32 hmac
|
private const val HMAC_PKT_SIZE = 37
|
||||||
private const val HMAC_LEN = 32
|
private const val HMAC_LEN = 32
|
||||||
|
private const val REQUEST_SETTINGS = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private var bluetoothGatt: BluetoothGatt? = null
|
private var bluetoothGatt: BluetoothGatt? = null
|
||||||
|
|
@ -55,7 +57,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var statusText: TextView
|
private lateinit var statusText: TextView
|
||||||
private lateinit var relayButton: Button
|
private lateinit var relayButton: Button
|
||||||
private lateinit var scanButton: Button
|
|
||||||
|
|
||||||
private val scanCallback = object : ScanCallback() {
|
private val scanCallback = object : ScanCallback() {
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
|
|
@ -111,7 +112,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
gatt.close()
|
gatt.close()
|
||||||
bluetoothGatt = null
|
bluetoothGatt = null
|
||||||
relayCharacteristic = null
|
relayCharacteristic = null
|
||||||
seq = 0
|
seq = 1
|
||||||
handler.postDelayed({ startScan() }, 2000)
|
handler.postDelayed({ startScan() }, 2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -160,18 +161,22 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
statusText = findViewById(R.id.statusText)
|
statusText = findViewById(R.id.statusText)
|
||||||
relayButton = findViewById(R.id.relayButton)
|
relayButton = findViewById(R.id.relayButton)
|
||||||
scanButton = findViewById(R.id.scanButton)
|
|
||||||
val settingsButton = findViewById<ImageButton>(R.id.settingsButton)
|
val settingsButton = findViewById<ImageButton>(R.id.settingsButton)
|
||||||
|
|
||||||
scanButton.setOnClickListener { startScan() }
|
|
||||||
settingsButton.setOnClickListener {
|
settingsButton.setOnClickListener {
|
||||||
startActivity(Intent(this, SettingsActivity::class.java))
|
startActivityForResult(Intent(this, SettingsActivity::class.java), REQUEST_SETTINGS)
|
||||||
}
|
}
|
||||||
setupRelayButton()
|
setupRelayButton()
|
||||||
|
|
||||||
registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
|
registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
|
||||||
|
}
|
||||||
|
|
||||||
handler.postDelayed({ startScan() }, 500)
|
@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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|
@ -179,6 +184,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
loadHmacKey()
|
loadHmacKey()
|
||||||
if (hmacKey == null) {
|
if (hmacKey == null) {
|
||||||
statusText.text = "Geen HMAC sleutel - open Instellingen"
|
statusText.text = "Geen HMAC sleutel - open Instellingen"
|
||||||
|
} else if (!isScanning && bluetoothGatt == null) {
|
||||||
|
handler.postDelayed({ startScan() }, 300)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,7 +268,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
isScanning = true
|
isScanning = true
|
||||||
statusText.text = "Scanning..."
|
statusText.text = "Scanning..."
|
||||||
scanButton.isEnabled = false
|
|
||||||
Log.d(TAG, "Starting BLE scan for $DEVICE_NAME")
|
Log.d(TAG, "Starting BLE scan for $DEVICE_NAME")
|
||||||
|
|
||||||
adapter.bluetoothLeScanner.startScan(scanCallback)
|
adapter.bluetoothLeScanner.startScan(scanCallback)
|
||||||
|
|
@ -275,7 +281,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
private fun stopScan() {
|
private fun stopScan() {
|
||||||
if (!isScanning) return
|
if (!isScanning) return
|
||||||
isScanning = false
|
isScanning = false
|
||||||
scanButton.isEnabled = true
|
|
||||||
val adapter = BluetoothAdapter.getDefaultAdapter()
|
val adapter = BluetoothAdapter.getDefaultAdapter()
|
||||||
adapter?.bluetoothLeScanner?.stopScan(scanCallback)
|
adapter?.bluetoothLeScanner?.stopScan(scanCallback)
|
||||||
if (statusText.text == "Scanning...") {
|
if (statusText.text == "Scanning...") {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
const val PREFS_NAME = "relay_prefs"
|
const val PREFS_NAME = "relay_prefs"
|
||||||
const val PREF_KEY = "hmac_key"
|
const val PREF_KEY = "hmac_key"
|
||||||
|
const val RESULT_SCAN = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isVisible = false
|
private var isVisible = false
|
||||||
|
|
@ -31,7 +32,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
val keyInput = findViewById<EditText>(R.id.keyInput)
|
val keyInput = findViewById<EditText>(R.id.keyInput)
|
||||||
val toggleBtn = findViewById<ImageButton>(R.id.toggleVisibility)
|
val toggleBtn = findViewById<ImageButton>(R.id.toggleVisibility)
|
||||||
val saveButton = findViewById<Button>(R.id.saveButton)
|
val saveButton = findViewById<Button>(R.id.saveButton)
|
||||||
val backButton = findViewById<Button>(R.id.backButton)
|
val scanConnectButton = findViewById<Button>(R.id.scanConnectButton)
|
||||||
val keyError = findViewById<TextView>(R.id.keyError)
|
val keyError = findViewById<TextView>(R.id.keyError)
|
||||||
|
|
||||||
keyInput.setText(prefs.getString(PREF_KEY, ""))
|
keyInput.setText(prefs.getString(PREF_KEY, ""))
|
||||||
|
|
@ -80,7 +81,14 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
backButton.setOnClickListener { finish() }
|
scanConnectButton.setOnClickListener {
|
||||||
|
val key = keyInput.text.toString().uppercase().filter { it in "0123456789ABCDEF" }
|
||||||
|
if (key.length == 64) {
|
||||||
|
prefs.edit().putString(PREF_KEY, key).apply()
|
||||||
|
}
|
||||||
|
setResult(RESULT_SCAN)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
|
|
||||||
139
android/app/src/main/java/com/eddy/atomrelay/UsbKeyReceiver.kt
Normal file
139
android/app/src/main/java/com/eddy/atomrelay/UsbKeyReceiver.kt
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
package com.eddy.atomrelay
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.hardware.usb.UsbDevice
|
||||||
|
import android.hardware.usb.UsbManager
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver
|
||||||
|
import com.hoho.android.usbserial.driver.UsbSerialPort
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
class UsbKeyReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "UsbKeyReceiver"
|
||||||
|
private const val ACTION_USB_PERMISSION = "com.eddy.atomrelay.USB_PERMISSION"
|
||||||
|
private const val ESP_VENDOR_ID = 0x303a
|
||||||
|
private const val KEY_TIMEOUT_MS = 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
||||||
|
val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE) ?: return
|
||||||
|
Log.d(TAG, "USB device attached: vid=0x${device.vendorId.toString(16)} pid=0x${device.productId.toString(16)}")
|
||||||
|
if (device.vendorId != ESP_VENDOR_ID) return
|
||||||
|
|
||||||
|
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||||
|
if (usbManager.hasPermission(device)) {
|
||||||
|
Thread { readKey(context, usbManager, device) }.start()
|
||||||
|
} else {
|
||||||
|
val permIntent = PendingIntent.getBroadcast(
|
||||||
|
context, 0,
|
||||||
|
Intent(ACTION_USB_PERMISSION)
|
||||||
|
.setClass(context, UsbKeyReceiver::class.java)
|
||||||
|
.putExtra(UsbManager.EXTRA_DEVICE, device),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
usbManager.requestPermission(device, permIntent)
|
||||||
|
Log.d(TAG, "Requesting USB permission for ${device.deviceName}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACTION_USB_PERMISSION -> {
|
||||||
|
val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE) ?: return
|
||||||
|
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
||||||
|
Log.d(TAG, "USB permission granted for ${device.deviceName}")
|
||||||
|
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||||
|
Thread { readKey(context, usbManager, device) }.start()
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "USB permission denied")
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
Toast.makeText(context, "USB-toegang geweigerd", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readKey(context: Context, usbManager: UsbManager, device: UsbDevice) {
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
var port: UsbSerialPort? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val driver = CdcAcmSerialDriver(device)
|
||||||
|
val connection = usbManager.openDevice(device)
|
||||||
|
if (connection == null) {
|
||||||
|
Log.e(TAG, "Failed to open USB device")
|
||||||
|
handler.post { Toast.makeText(context, "Kan USB-apparaat niet openen", Toast.LENGTH_SHORT).show() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val ports = driver.ports
|
||||||
|
if (ports.isEmpty()) {
|
||||||
|
Log.e(TAG, "No serial ports found")
|
||||||
|
handler.post { Toast.makeText(context, "Geen serial-poort gevonden", Toast.LENGTH_SHORT).show() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
port = ports[0]
|
||||||
|
port.open(connection)
|
||||||
|
port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
|
||||||
|
Log.d(TAG, "Serial port opened, sending KEY?")
|
||||||
|
|
||||||
|
port.purgeHwBuffers(true, true)
|
||||||
|
|
||||||
|
val cmd = "KEY?\n".toByteArray(StandardCharsets.US_ASCII)
|
||||||
|
port.write(cmd, 500)
|
||||||
|
|
||||||
|
Thread.sleep(200)
|
||||||
|
|
||||||
|
val buf = ByteArray(256)
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
val sb = StringBuilder()
|
||||||
|
|
||||||
|
while (System.currentTimeMillis() - startTime < KEY_TIMEOUT_MS) {
|
||||||
|
val len = port.read(buf, 200)
|
||||||
|
if (len > 0) {
|
||||||
|
sb.append(String(buf, 0, len, StandardCharsets.US_ASCII))
|
||||||
|
if (sb.contains("HMAC_KEY:") && sb.contains("\n")) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = sb.toString()
|
||||||
|
Log.d(TAG, "Serial response: $response")
|
||||||
|
|
||||||
|
val keyLine = response.lines().firstOrNull { it.startsWith("HMAC_KEY:") }
|
||||||
|
if (keyLine != null) {
|
||||||
|
val hexKey = keyLine.removePrefix("HMAC_KEY:").trim()
|
||||||
|
if (hexKey.length == 64 && hexKey.all { it in "0123456789ABCDEFabcdef" }) {
|
||||||
|
val upperKey = hexKey.uppercase()
|
||||||
|
val prefs = context.getSharedPreferences(SettingsActivity.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().putString(SettingsActivity.PREF_KEY, upperKey).apply()
|
||||||
|
Log.d(TAG, "HMAC key saved: $upperKey")
|
||||||
|
handler.post {
|
||||||
|
Toast.makeText(context, "HMAC sleutel gelezen van ESP!", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Invalid key format: length=${hexKey.length}")
|
||||||
|
handler.post { Toast.makeText(context, "Ongeldig sleutel formaat van ESP", Toast.LENGTH_SHORT).show() }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "No HMAC_KEY line found in response")
|
||||||
|
handler.post { Toast.makeText(context, "Geen sleutel gevonden op de ESP", Toast.LENGTH_SHORT).show() }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error reading key: ${e.message}", e)
|
||||||
|
handler.post { Toast.makeText(context, "Fout bij uitlezen: ${e.message}", Toast.LENGTH_SHORT).show() }
|
||||||
|
} finally {
|
||||||
|
try { port?.close() } catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,42 +1,33 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:padding="16dp">
|
||||||
android:gravity="center"
|
|
||||||
android:padding="32dp">
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/settingsButton"
|
android:id="@+id/settingsButton"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end|top"
|
||||||
android:src="@drawable/ic_settings"
|
android:src="@drawable/ic_settings"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="Instellingen"
|
android:contentDescription="Instellingen" />
|
||||||
android:layout_marginBottom="16dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/statusText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Disconnected"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:layout_marginBottom="48dp"/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/relayButton"
|
android:id="@+id/relayButton"
|
||||||
android:layout_width="200dp"
|
android:layout_width="200dp"
|
||||||
android:layout_height="200dp"
|
android:layout_height="200dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
android:text="HOLD"
|
android:text="HOLD"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:enabled="false"/>
|
android:enabled="false" />
|
||||||
|
|
||||||
<Button
|
<TextView
|
||||||
android:id="@+id/scanButton"
|
android:id="@+id/statusText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Scan & Connect"
|
android:layout_gravity="bottom|start"
|
||||||
android:layout_marginTop="48dp"/>
|
android:text="Disconnected"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</FrameLayout>
|
||||||
|
|
@ -5,6 +5,15 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="24dp">
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/keyHint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Voer de HMAC sleutel in of sluit de relaismodule aan op de telefoon."
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="italic"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
@ -66,11 +75,10 @@
|
||||||
android:layout_marginTop="24dp" />
|
android:layout_marginTop="24dp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/backButton"
|
android:id="@+id/scanConnectButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Terug"
|
android:text="Scan & Verbind"
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:layout_marginTop="8dp" />
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
@ -10,6 +10,7 @@ dependencyResolutionManagement {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootProject.name = "AtomRelay"
|
rootProject.name = "AtomRelay"
|
||||||
|
|
|
||||||
17
src/main.cpp
17
src/main.cpp
|
|
@ -158,6 +158,23 @@ void setup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
if (Serial.available()) {
|
||||||
|
static String serialBuf;
|
||||||
|
char c = Serial.read();
|
||||||
|
serialBuf += c;
|
||||||
|
if (c == '\n') {
|
||||||
|
serialBuf.trim();
|
||||||
|
if (serialBuf == "KEY?") {
|
||||||
|
Serial.print("HMAC_KEY:");
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
Serial.printf("%02X", HMAC_KEY[i]);
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
serialBuf = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int sensorState = digitalRead(SENSOR_PIN);
|
int sensorState = digitalRead(SENSOR_PIN);
|
||||||
|
|
||||||
bool touchActive = (sensorState == LOW);
|
bool touchActive = (sensorState == LOW);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue