docs: add BLE refactor design spec (BleRelayManager extraction)

This commit is contained in:
eddy 2026-07-29 19:40:30 +02:00
commit 459dc5f201

View file

@ -0,0 +1,172 @@
# BLE Refactor: BleRelayManager extractie
**Datum:** 2026-07-29
**Status:** goedgekeurd
## Doel
Extractie van alle BLE-logica uit `MainActivity.kt` naar een aparte `BleRelayManager` klasse. MainActivity wordt verantwoordelijk voor UI en lifecycle only.
## Scope
- **In:** BLE scan, GATT connectie, UUID verificatie, HMAC relay commands, key/seq management, auto-retry logica
- **Uit:** UI binding (views, touch listeners, string resources), lifecycle (onCreate/onResume/etc), permissions result handling, theme switching
- **Niet aangeraakt:** `PrefsHelper`, `SettingsActivity`, `EspFlasher`, `UsbSerialTransport`, firmware
## Nieuwe bestanden
### `BleStatus.kt` — enum + callback interface
```kotlin
enum class BleStatus {
Scanning,
Connecting,
Connected,
Disconnected,
NoKey,
WrongDevice,
ServiceNotFound,
ServiceError,
CommError,
DeviceNotFound,
BluetoothDisabled,
PermissionsDenied
}
interface BleCallbacks {
fun onStatusChanged(status: BleStatus)
}
```
### `BleRelayManager.kt` — publieke API
```kotlin
class BleRelayManager(
private val context: Context,
private val callbacks: BleCallbacks
) {
fun startScan()
fun stopScan()
fun sendRelayCommand(cmd: Byte)
fun disconnect()
fun updateKeys() // herlaadt keys na settings key exchange
fun resetTriedAddresses() // wist triedAddresses (bij onResume)
fun hasPermissions(): Boolean
fun getRequiredPermissions(): Array<String>
fun destroy() // cleanup handler, stop scan, close gatt
}
```
### Interne componenten (private)
1. **State:** `bluetoothGatt`, `relayCharacteristic`, `hmacKey`, `deviceUuid`, `seq`, `triedAddresses`, `isScanning`, `scanTimedOut`, `scanTimeoutRetries`
2. **Handler:** `Handler(Looper.getMainLooper())` voor scan scheduling en timeouts
3. **ScanCallback:** scan op `SERVICE_UUID`, skip `triedAddresses`, bij match → `stopScan()` + `connectToDevice()`
4. **GattCallback:** connectie → check key → discoverServices → requestMtu → read UUID char → verify → enable notifications. Bij disconnect: auto-retry na delay.
5. **HMAC:** `hexStringToBytes()`, `hmacSha256()`, `buildPacket()` — ongewijzigd verplaatst
6. **Key loading:** `loadState()` leest via `PrefsHelper.getPrefs(context)`
**Companion constants** (ongewijzigd): `SERVICE_UUID`, `CHARACTERISTIC_UUID`, `UUID_CHAR_UUID`, `CCCD_UUID`, `DEVICE_NAME`, `HMAC_PKT_SIZE`, `HMAC_LEN`, `SCAN_TIMEOUT_MS`, `RECONNECT_DELAY_MS`, `MAX_SCAN_RETRIES`
```mermaid
sequenceDiagram
participant MA as MainActivity
participant BRM as BleRelayManager
participant ESP as ESP32-S3
MA->>BRM: startScan()
BRM->>BRM: loadState() via PrefsHelper
BRM-->>MA: onStatusChanged(Scanning)
BRM->>ESP: BLE scan (filter SERVICE_UUID)
ESP-->>BRM: scan result
BRM->>BRM: skip triedAddresses?
BRM->>ESP: connectGatt()
BRM-->>MA: onStatusChanged(Connecting)
ESP-->>BRM: STATE_CONNECTED
BRM->>BRM: check hmacKey != null
BRM->>ESP: discoverServices → requestMtu(256)
BRM->>ESP: readCharacteristic(UUID_CHAR)
ESP-->>BRM: UUID value
BRM->>BRM: verify against stored deviceUuid
BRM->>ESP: setCharacteristicNotification + writeDescriptor(CCCD)
BRM-->>MA: onStatusChanged(Connected)
MA->>BRM: sendRelayCommand(0x01)
BRM->>BRM: buildPacket(cmd, hmacKey, seq)
BRM->>ESP: writeCharacteristic (NO_RESPONSE)
BRM->>Prefs: save seq
ESP-->>BRM: STATE_DISCONNECTED
BRM-->>MA: onStatusChanged(Disconnected)
BRM->>BRM: handler.postDelayed(startScan, 2s)
```
## MainActivity — wat blijft
| Component | Blijft in MainActivity |
|---|---|
| `onCreate` | View binding, settings button, `BleRelayManager` instantiatie |
| `onResume` | Theme check, `manager.resetTriedAddresses()`, `manager.startScan()` of toon NoKey |
| `onPause` | `manager.stopScan()` |
| `onDestroy` | `manager.destroy()` |
| `onActivityResult` | Bij RESULT_SCAN: `manager.disconnect()`, `manager.updateKeys()`, `manager.resetTriedAddresses()`, `manager.startScan()` |
| `onRequestPermissionsResult` | Bij granted: `manager.startScan()`; bij denied: UI update |
| `setupRelayButton()` | Touch listener → `manager.sendRelayCommand(...)` |
| `setupRelay2Button()` | Touch listener → `manager.sendRelayCommand(...)` |
| Status mapping | `onStatusChanged` → `statusText.text = getString(R.string.status_*)` + knop enabled/disabled |
### Status → UI mapping
```kotlin
callbacks = object : BleCallbacks {
override fun onStatusChanged(status: BleStatus) {
when (status) {
BleStatus.Connected -> {
statusText.text = getString(R.string.status_connected)
setupRelayButton()
setupRelay2Button()
relay1Button.isEnabled = true
relay2Button.isEnabled = true
}
BleStatus.Disconnected -> {
statusText.text = getString(R.string.status_connecting)
relay1Button.isEnabled = false
relay2Button.isEnabled = false
}
BleStatus.DeviceNotFound -> {
// retry knop op relay1Button (huidig gedrag behouden)
relay1Button.setOnTouchListener(null)
relay2Button.setOnTouchListener(null)
relay1Button.setOnClickListener(null)
relay2Button.setOnClickListener(null)
relay1Button.text = getString(R.string.button_retry_scan)
relay2Button.text = getString(R.string.button_hold)
relay2Button.isEnabled = false
relay1Button.isEnabled = true
relay1Button.setOnClickListener {
setupRelayButton()
setupRelay2Button()
manager.startScan()
}
}
else -> statusText.text = getString(R.string.status_scanning) // fallback
}
}
}
```
## Risico's & mitigatie
| Risico | Mitigatie |
|---|---|
| Race condition bij snelle key-update + scan | `updateKeys()` stopt scan vóór herladen |
| Context leak via Handler | `destroy()` roept `handler.removeCallbacksAndMessages(null)` |
| GattCallback refs naar oude Activity | Manager instantieert per Activity, geen singleton |
| Touch listeners verwijzen naar verouderde manager | `setupRelay*Button()` opnieuw aanroepen bij statuswissel (huidig patroon) |
## Verificatie
1. Build: `./gradlew assembleDebug` moet slagen
2. Lint: `./gradlew lint` geen nieuwe warnings
3. Functioneel: APK deployen via ADB, BLE scan → connect → UUID verify → relay knoppen ON/OFF
4. Randgevallen: app herstarten (keys behouden), settings key exchange → rescan, timeout + retry, disconnect + auto-reconnect, permissie deny/accept