# 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