From 459dc5f20172deeb6772f8a1a3da0b7a24b23ab5 Mon Sep 17 00:00:00 2001 From: eddy Date: Wed, 29 Jul 2026 19:40:30 +0200 Subject: [PATCH] docs: add BLE refactor design spec (BleRelayManager extraction) --- .../specs/2026-07-29-ble-refactor-design.md | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-29-ble-refactor-design.md diff --git a/docs/superpowers/specs/2026-07-29-ble-refactor-design.md b/docs/superpowers/specs/2026-07-29-ble-refactor-design.md new file mode 100644 index 0000000..b046529 --- /dev/null +++ b/docs/superpowers/specs/2026-07-29-ble-refactor-design.md @@ -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 + 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