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 maxReconnectAttempts = 5
|
||||||
private val reconnectDelays = longArrayOf(3000, 5000, 10000, 10000, 10000)
|
private val reconnectDelays = longArrayOf(3000, 5000, 10000, 10000, 10000)
|
||||||
private var reconnectTimer: Runnable? = null
|
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 discoveredDevices = mutableStateListOf<BleDevice>()
|
||||||
val isScanning = mutableStateOf(false)
|
val isScanning = mutableStateOf(false)
|
||||||
@@ -220,6 +223,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
isUserDisconnect = true
|
isUserDisconnect = true
|
||||||
cancelAutoReconnect()
|
cancelAutoReconnect()
|
||||||
|
stopWatchdog()
|
||||||
connectionTimer?.let { handler.removeCallbacks(it) }
|
connectionTimer?.let { handler.removeCallbacks(it) }
|
||||||
connectionTimer = null
|
connectionTimer = null
|
||||||
bluetoothGatt?.disconnect()
|
bluetoothGatt?.disconnect()
|
||||||
@@ -348,6 +352,50 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
batteryTimer = null
|
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 ──
|
// ── Auto Reconnect ──
|
||||||
|
|
||||||
private fun scheduleAutoReconnect() {
|
private fun scheduleAutoReconnect() {
|
||||||
@@ -544,6 +592,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
if (txCharacteristic != null && rxCharacteristic != null) {
|
if (txCharacteristic != null && rxCharacteristic != null) {
|
||||||
isServiceReady.value = true
|
isServiceReady.value = true
|
||||||
startBatteryPolling()
|
startBatteryPolling()
|
||||||
|
startWatchdog()
|
||||||
logd { "Service ready! (CCCD written)" }
|
logd { "Service ready! (CCCD written)" }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -579,6 +628,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun processReceivedData(data: ByteArray) {
|
private fun processReceivedData(data: ByteArray) {
|
||||||
|
lastRxTimestamp = System.currentTimeMillis()
|
||||||
val prefix = String(data, 0, minOf(4, data.size), Charsets.US_ASCII)
|
val prefix = String(data, 0, minOf(4, data.size), Charsets.US_ASCII)
|
||||||
debugLogger.rx(prefix.trimEnd(':', '!'), data.size)
|
debugLogger.rx(prefix.trimEnd(':', '!'), data.size)
|
||||||
logd { "Received: $prefix (${data.size} bytes)" }
|
logd { "Received: $prefix (${data.size} bytes)" }
|
||||||
|
|||||||
Reference in New Issue
Block a user