fix: BLE reconnect를 fresh scan→connect 방식으로 변경

기존: connectGatt(autoConnect=true) — Android에서 매우 불안정, 수분 걸림
변경: BLE 스캔으로 기기를 다시 찾은 후 connectGatt(autoConnect=false)로 빠르게 연결

재연결 흐름:
1. watchdog 15초 타임아웃 → forceDisconnectAndReconnect()
2. scheduleAutoReconnect() → 3s/5s/10s 백오프
3. attemptAutoReconnect():
   - BLE 스캔 시작 (기기 이름 or MAC 주소 매칭)
   - 발견 시 즉시 connectGatt(autoConnect=false) → 빠른 연결
   - 8초 스캔 타임아웃 → 다음 attempt
   - 최대 5회 시도 후 실패 처리
4. 모든 단계 BleDebugLogger에 기록

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 14:02:08 +09:00
parent d4e2627efe
commit 9d32d13452
@@ -418,8 +418,11 @@ class BleManager private constructor(private val context: Context) {
handler.postDelayed(timer, delay)
}
private var reconnectScanCallback: ScanCallback? = null
private fun attemptAutoReconnect() {
val address = lastConnectedAddress ?: return
val targetAddress = lastConnectedAddress ?: return
val targetName = debugLogger.deviceName
if (bluetoothAdapter?.isEnabled != true) {
onReconnectionFailed?.invoke()
isReconnecting.value = false
@@ -427,15 +430,69 @@ class BleManager private constructor(private val context: Context) {
}
reconnectAttempt++
logd { "Attempting auto-reconnect #$reconnectAttempt to $address" }
logd { "Auto-reconnect #$reconnectAttempt: scanning for $targetName ($targetAddress)" }
debugLogger.info("RECONNECT_SCAN #$reconnectAttempt scanning for $targetName")
bluetoothGatt?.let { gatt ->
try { gatt.close() } catch (_: Exception) {}
bluetoothGatt = null
}
val device = bluetoothAdapter.getRemoteDevice(address)
bluetoothGatt = device.connectGatt(context, true, gattCallback, BluetoothDevice.TRANSPORT_LE)
// Fresh scan to find the device again
val scanner = bluetoothAdapter.bluetoothLeScanner
if (scanner == null) {
scheduleAutoReconnect()
return
}
// Stop any previous reconnect scan
reconnectScanCallback?.let {
try { scanner.stopScan(it) } catch (_: Exception) {}
}
val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val name = try { result.device.name ?: "" } catch (_: Exception) { "" }
val address = result.device.address
if (address == targetAddress || (targetName.isNotEmpty() && name == targetName)) {
logd { "Reconnect scan found: $name ($address)" }
debugLogger.info("RECONNECT_FOUND $name ($address) RSSI=${result.rssi}")
try { scanner.stopScan(this) } catch (_: Exception) {}
reconnectScanCallback = null
// Connect directly (autoConnect=false for fast connection)
bluetoothGatt = result.device.connectGatt(
context, false, gattCallback, BluetoothDevice.TRANSPORT_LE
)
}
}
}
reconnectScanCallback = callback
try {
scanner.startScan(callback)
} catch (e: Exception) {
debugLogger.error("RECONNECT_SCAN_FAIL ${e.message}")
scheduleAutoReconnect()
return
}
// Stop scan after 8 seconds → retry next attempt
handler.postDelayed({
try { scanner.stopScan(callback) } catch (_: Exception) {}
reconnectScanCallback = null
if (!isConnected.value && reconnectAttempt < maxReconnectAttempts) {
debugLogger.warn("RECONNECT_SCAN_TIMEOUT #$reconnectAttempt — device not found")
scheduleAutoReconnect()
} else if (!isConnected.value) {
debugLogger.reconnectFailed()
isReconnecting.value = false
onReconnectionFailed?.invoke()
}
}, 8000)
}
private fun cancelReconnectTimer() {