fix(ble): 좀비 세션 재연결 실측 대응 — 콜백 duplicate 제거 + watchdog 25초

실측 로그 (2026-07-07 10:43~10:44 세션) 재분석 결과 두 이슈 발견.

1) Duplicate startBatteryPolling
이전 fix (f4f5407) 로 onConnectionStateChanged(true) 가 CCCD write 완료
시점 (BleManager onDescriptorWrite 안 line 1059 startBatteryPolling
직전) 으로 이동. 그런데 PiezoMonitoringView 콜백도 startBatteryPolling
을 호출해 첫 msn battery TX 가 2번 발생 (10:43:46.008 + 10:43:46.040).
Idempotent 라 실제 부작용은 낮지만 노이즈. PiezoMonitoringView 콜백에서
startBatteryPolling 제거 — BleManager 가 이미 담당.

2) Watchdog 25초 (from 15초)
세션 1: 첫 연결 후 CCCD/MTU/CONN_PRIORITY/startBatteryPolling 모두 성공,
그런데 15초간 RX 0개 → forced reconnect. 세션 2: 재연결 후 14초 만에
첫 rim RX 도착 (10:44:00.475) → 스스로 회복. 즉 peripheral / OS BLE
스택의 좀비 회복 시간이 실측 14초. 기존 15초 timeout 은 회복 직전에
reconnect 발동 → 무한 재연결 루프. 25초로 연장하면 대부분 회복 케이스
살릴 수 있음.

세션 1 의 첫 handshake 응답 실종 (mid → rid 응답 없음) 은 앱 코드로
근본 해결 불가 (peripheral 또는 OS BLE 스택 문제). Watchdog 연장은
실질적 완화책.
This commit is contained in:
2026-07-07 10:50:34 +09:00
parent a10d4b9517
commit 04607241ac
2 changed files with 14 additions and 4 deletions
@@ -75,7 +75,11 @@ class BleManager private constructor(private val context: Context) {
private var reconnectTimer: Runnable? = null
@Volatile private var lastRxTimestamp: Long = 0
private var watchdogTimer: Runnable? = null
private val watchdogTimeoutMs: Long = 15000
// 2026-07-07: 15 → 25초. 실측 로그 (2026-07-07 10:43~10:44) 에서 재연결 후 첫 RX 가
// 14초 지연 후 도착하는 케이스 확인. Peripheral / OS BLE 스택의 좀비 회복 시간을
// 허용하기 위해 timeout 여유 확보. 15초로는 회복 직전에 forced reconnect 발생 →
// 무한 재연결 루프. 25초면 대부분 회복 케이스 커버 가능.
private val watchdogTimeoutMs: Long = 25000
// 2026-07-07 fix: disconnect 시 취소 위해 Runnable 참조 유지.
// fwFallback: onDescriptorWrite 성공 후 4초 mfv? fallback.
// cccdRetry: onDescriptorWrite 실패 후 500ms retry.
@@ -738,14 +738,20 @@ fun PiezoMonitoringView(appState: AppState) {
// 실패해도 UI 알림 없음 — BleManager가 다음 주기에 또 재시도하거나 idle.
}
bleManager.onConnectionStateChanged = { connected ->
// 2026-07-07 fix: startBatteryPolling 을 여기서 호출하지 않음.
// 이전 fix 로 onConnectionStateChanged(true) 가 CCCD 완료 시점 (=BleManager
// 자체가 line 1059 에서 startBatteryPolling 실행하는 시점) 으로 이동했음.
// 콜백에서 또 호출하면 첫 msn battery TX 가 2번 나가는 duplicate 발생.
// BleManager 가 항상 담당하므로 여기서는 아무것도 안 함. (또는 UI 관련 side effect
// 만 처리하도록 향후 확장.)
if (connected) {
bleManager.startBatteryPolling()
// 필요 시 UI 관련 반응만. Battery polling / TX 는 BleManager 소관.
}
}
// 진입 시 이미 연결되어 있으면 한 번 polling 재시작 (안전망).
// 이전 화면(Clinical Unpair → 새 연결 등)에서 batteryLevel=0으로 reset된 채로
// 도넛차트 진입 시 onDescriptorWrite/onConnectionStateChanged 둘 다 이미 끝나 있어
// 콜백이 트리거 안 됨. 진입 시점에 강제 1회 polling으로 보강.
// 콜백이 트리거 안 됨. 진입 시점에 강제 1회 polling으로 보강 — 이건 duplicate 아님
// (BleManager 는 이미 polling 중이지만 stopBatteryPolling→restart 로 idempotent).
if (bleManager.isConnected.value) {
bleManager.startBatteryPolling()
}