fix: BLE watchdog — 좀비 연결 감지 + 강제 reconnect
Android BLE가 disconnect 콜백을 안 보내는 경우(좀비 연결) 대응: - 마지막 RX로부터 15초간 응답 없으면 강제 disconnect → auto reconnect - processReceivedData()에서 lastRxTimestamp 갱신 - 5초마다 watchdog 체크 - WATCHDOG_TIMEOUT 이벤트 BleDebugLogger에 기록 - 수동 disconnect 시 watchdog 해제 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<BleDevice>()
|
||||
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)" }
|
||||
|
||||
Reference in New Issue
Block a user