diff --git a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt index 507486e..b4b7115 100644 --- a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt +++ b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt @@ -63,6 +63,9 @@ class BleManager private constructor(private val context: Context) { private val maxReconnectAttempts = 5 private val reconnectDelays = longArrayOf(3000, 5000, 10000, 10000, 10000) private var reconnectTimer: Runnable? = null + private var lastRxTimestamp: Long = 0 + private var watchdogTimer: Runnable? = null + private val watchdogTimeoutMs: Long = 15000 val discoveredDevices = mutableStateListOf() val isScanning = mutableStateOf(false) @@ -220,6 +223,7 @@ class BleManager private constructor(private val context: Context) { fun disconnect() { isUserDisconnect = true cancelAutoReconnect() + stopWatchdog() connectionTimer?.let { handler.removeCallbacks(it) } connectionTimer = null bluetoothGatt?.disconnect() @@ -348,6 +352,50 @@ class BleManager private constructor(private val context: Context) { batteryTimer = null } + // ── Watchdog (zombie connection detection) ── + + private fun startWatchdog() { + stopWatchdog() + lastRxTimestamp = System.currentTimeMillis() + val check = object : Runnable { + override fun run() { + if (!isConnected.value) return + val silenceMs = System.currentTimeMillis() - lastRxTimestamp + if (silenceMs > watchdogTimeoutMs) { + logw { "Watchdog: no RX for ${silenceMs}ms — forcing disconnect + reconnect" } + debugLogger.error("WATCHDOG_TIMEOUT silence=${silenceMs}ms — forcing reconnect") + forceDisconnectAndReconnect() + return + } + handler.postDelayed(this, 5000) + } + } + watchdogTimer = check + handler.postDelayed(check, 5000) + } + + private fun stopWatchdog() { + watchdogTimer?.let { handler.removeCallbacks(it) } + watchdogTimer = null + } + + private fun forceDisconnectAndReconnect() { + stopBatteryPolling() + stopWatchdog() + isConnected.value = false + isServiceReady.value = false + + bluetoothGatt?.let { gatt -> + try { gatt.disconnect(); gatt.close() } catch (_: Exception) {} + } + bluetoothGatt = null + txCharacteristic = null + rxCharacteristic = null + + onUnexpectedDisconnect?.invoke() + scheduleAutoReconnect() + } + // ── Auto Reconnect ── private fun scheduleAutoReconnect() { @@ -544,6 +592,7 @@ class BleManager private constructor(private val context: Context) { if (txCharacteristic != null && rxCharacteristic != null) { isServiceReady.value = true startBatteryPolling() + startWatchdog() logd { "Service ready! (CCCD written)" } } } else { @@ -579,6 +628,7 @@ class BleManager private constructor(private val context: Context) { } private fun processReceivedData(data: ByteArray) { + lastRxTimestamp = System.currentTimeMillis() val prefix = String(data, 0, minOf(4, data.size), Charsets.US_ASCII) debugLogger.rx(prefix.trimEnd(':', '!'), data.size) logd { "Received: $prefix (${data.size} bytes)" }