Project_SylDa/docs/superpowers/specs/2026-07-29-ble-refactor-design.md
eddy 70f004c45b docs: add release build spec and implementation plan
Also un-ignores docs/ directory so project documentation (specs, plans)
is tracked in git. Individual research files remain gitignored.
2026-07-30 19:54:13 +02:00

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)

  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

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

  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