docs: add BLE refactor design spec (BleRelayManager extraction)
This commit is contained in:
parent
d6e37322f3
commit
459dc5f201
1 changed files with 172 additions and 0 deletions
172
docs/superpowers/specs/2026-07-29-ble-refactor-design.md
Normal file
172
docs/superpowers/specs/2026-07-29-ble-refactor-design.md
Normal 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
|
||||
Loading…
Reference in a new issue