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:
eddy 2026-07-26 08:39:28 +02:00
commit 4518aec34e
10 changed files with 270 additions and 74 deletions

View file

@ -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
@ -295,3 +309,6 @@ sylvia-touch-relay/
- 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.

View file

@ -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'
} }

View file

@ -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>

View file

@ -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...") {

View file

@ -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 {

View 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) {}
}
}
}

View file

@ -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 &amp; Connect" android:layout_gravity="bottom|start"
android:layout_marginTop="48dp"/> android:text="Disconnected"
android:textSize="14sp" />
</LinearLayout> </FrameLayout>

View file

@ -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 &amp; Verbind"
android:background="@android:color/transparent"
android:layout_marginTop="8dp" /> android:layout_marginTop="8dp" />
</LinearLayout> </LinearLayout>

View file

@ -10,6 +10,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' }
} }
} }
rootProject.name = "AtomRelay" rootProject.name = "AtomRelay"

View file

@ -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);