Also un-ignores docs/ directory so project documentation (specs, plans) is tracked in git. Individual research files remain gitignored.
6.6 KiB
6.6 KiB
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
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
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)
- State:
bluetoothGatt,relayCharacteristic,hmacKey,deviceUuid,seq,triedAddresses,isScanning,scanTimedOut,scanTimeoutRetries - Handler:
Handler(Looper.getMainLooper())voor scan scheduling en timeouts - ScanCallback: scan op
SERVICE_UUID, skiptriedAddresses, bij match →stopScan()+connectToDevice() - GattCallback: connectie → check key → discoverServices → requestMtu → read UUID char → verify → enable notifications. Bij disconnect: auto-retry na delay.
- HMAC:
hexStringToBytes(),hmacSha256(),buildPacket()— ongewijzigd verplaatst - Key loading:
loadState()leest viaPrefsHelper.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
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
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
- Build:
./gradlew assembleDebugmoet slagen - Lint:
./gradlew lintgeen nieuwe warnings - Functioneel: APK deployen via ADB, BLE scan → connect → UUID verify → relay knoppen ON/OFF
- Randgevallen: app herstarten (keys behouden), settings key exchange → rescan, timeout + retry, disconnect + auto-reconnect, permissie deny/accept