fix(ble): 화면 전환 후 좀비 BLE 세션 방지 — 콜백 순서 + 정리
증상: 앱 실행 후 alignment → 도넛차트 → 개발자모드 → alignment 이동 시 BLE 가 끊긴 것처럼 응답 실종 → 15초 후 watchdog forced reconnect. 심하면 재연결도 좀비 상태로 나와 3번째 재연결에서야 정상 동작. 원인 1: BleManager.onConnectionStateChange 에서 STATE_CONNECTED 직후 onConnectionStateChanged?.invoke(true) 호출 → 이 시점에는 아직 MTU/ service discovery/CCCD write 이 완료되지 않아 txCharacteristic 은 null. 상위 (PiezoMonitoringView) 가 startBatteryPolling 등 TX 를 시도하면 sendRaw 내부 null-check 에 걸려 silent drop → peripheral 이 응답을 보내지 않음 → watchdog timeout. 원인 2: PiezoMonitoringView / PlacementGuideView 의 DisposableEffect 에서 BleManager 콜백 (onConnectionStateChanged, onUnexpectedDisconnect, onReconnectionFailed, piezoCollector.onMultiChannelComplete 등) 을 정리하지 않음. 화면 전환 시 이전 화면의 stale 콜백이 남아 새 화면 진입 후에도 발동 → 상태 충돌. 수정: - BleManager: onConnectionStateChanged(true) 호출을 CCCD write 완료 (isServiceReady=true) 시점으로 이동. CCCD 없음 / RX 없음 edge case 도 동일하게 처리. - PiezoMonitoringView / PlacementGuideView: DisposableEffect 에서 등록한 BleManager 콜백 전부 null 로 정리. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -937,7 +937,12 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
if (!gatt.requestMtu(247)) {
|
if (!gatt.requestMtu(247)) {
|
||||||
gatt.discoverServices() // MTU 요청 실패 시 바로 진행
|
gatt.discoverServices() // MTU 요청 실패 시 바로 진행
|
||||||
}
|
}
|
||||||
onConnectionStateChanged?.invoke(true)
|
// 2026-07-07 fix: onConnectionStateChanged(true) 를 여기서 호출하면
|
||||||
|
// TX/RX characteristic 이 아직 null (onServicesDiscovered 미실행) 이라
|
||||||
|
// 상위 (PiezoMonitoringView) 가 startBatteryPolling 등 TX 를 시작해도
|
||||||
|
// sendRaw 에서 silent drop → RX 실종 → watchdog timeout → forced reconnect
|
||||||
|
// 좀비 세션의 원인. 콜백은 isServiceReady=true 시점 (onDescriptorWrite)
|
||||||
|
// 으로 이동.
|
||||||
}
|
}
|
||||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||||
logd { "Disconnected from ${gatt.device.address}" }
|
logd { "Disconnected from ${gatt.device.address}" }
|
||||||
@@ -1011,6 +1016,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
handler.post {
|
handler.post {
|
||||||
if (txCharacteristic != null && rxCharacteristic != null) {
|
if (txCharacteristic != null && rxCharacteristic != null) {
|
||||||
isServiceReady.value = true
|
isServiceReady.value = true
|
||||||
|
onConnectionStateChanged?.invoke(true)
|
||||||
startBatteryPolling()
|
startBatteryPolling()
|
||||||
startWatchdog()
|
startWatchdog()
|
||||||
logd { "Service ready (no CCCD)" }
|
logd { "Service ready (no CCCD)" }
|
||||||
@@ -1021,6 +1027,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
handler.post {
|
handler.post {
|
||||||
if (txCharacteristic != null) {
|
if (txCharacteristic != null) {
|
||||||
isServiceReady.value = true
|
isServiceReady.value = true
|
||||||
|
onConnectionStateChanged?.invoke(true)
|
||||||
startBatteryPolling()
|
startBatteryPolling()
|
||||||
startWatchdog()
|
startWatchdog()
|
||||||
logd { "Service ready (no RX)" }
|
logd { "Service ready (no RX)" }
|
||||||
@@ -1036,6 +1043,10 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
if (txCharacteristic != null && rxCharacteristic != null) {
|
if (txCharacteristic != null && rxCharacteristic != null) {
|
||||||
isServiceReady.value = true
|
isServiceReady.value = true
|
||||||
|
// 2026-07-07 fix: 여기서 상위에 알림 — TX/RX 준비 완료 시점. 이전에는
|
||||||
|
// STATE_CONNECTED 즉시 호출해서 아직 characteristic null 인 상태로
|
||||||
|
// 상위가 TX 시도 → silent drop → 좀비 세션.
|
||||||
|
onConnectionStateChanged?.invoke(true)
|
||||||
// Connection priority HIGH 요청: peripheral이 수락하면 interval ~15ms로 협상되어
|
// Connection priority HIGH 요청: peripheral이 수락하면 interval ~15ms로 협상되어
|
||||||
// Central Link Layer ACK 속도 ↑ → FW SoftDevice TX queue 포화 빈도 ↓
|
// Central Link Layer ACK 속도 ↑ → FW SoftDevice TX queue 포화 빈도 ↓
|
||||||
val priorityOk = try {
|
val priorityOk = try {
|
||||||
|
|||||||
+7
-1
@@ -846,11 +846,17 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PiezoMonitoring 이탈 시 imuCollector callback cleanup — 다른 화면 (PlacementGuide 등) 이
|
// PiezoMonitoring 이탈 시 callback cleanup — 다른 화면 (PlacementGuide, ClinicalHome 등) 이
|
||||||
// 자기 콜백 등록하기 전에 우리 콜백 해제. 다시 진입하면 위 LaunchedEffect 가 재등록.
|
// 자기 콜백 등록하기 전에 우리 콜백 해제. 다시 진입하면 위 LaunchedEffect 가 재등록.
|
||||||
|
// 2026-07-07 fix: BleManager 콜백 (onConnectionStateChanged 등) 3개도 정리. 이전에는
|
||||||
|
// 화면 전환 시 stale 콜백이 남아 새 화면 진입 후에도 이전 화면 로직 (startBatteryPolling
|
||||||
|
// 등) 이 발동 → GATT operation queue 겹침 → 좀비 세션의 간접 원인.
|
||||||
androidx.compose.runtime.DisposableEffect(Unit) {
|
androidx.compose.runtime.DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
bleManager.imuCollector.onComplete = null
|
bleManager.imuCollector.onComplete = null
|
||||||
|
bleManager.onConnectionStateChanged = null
|
||||||
|
bleManager.onUnexpectedDisconnect = null
|
||||||
|
bleManager.onReconnectionFailed = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,9 +162,16 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
sweepLastDetected = 0; sweepEverSawAll = false
|
sweepLastDetected = 0; sweepEverSawAll = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 화면 벗어날 때 LED OFF
|
// 화면 벗어날 때 LED OFF + 콜백 정리.
|
||||||
|
// 2026-07-07 fix: piezoCollector.onMultiChannelComplete / imuCollector.onComplete 는
|
||||||
|
// PlacementGuide 진입 시 자기 콜백으로 덮어쓰는데, 이탈 시 정리하지 않으면 다음
|
||||||
|
// 화면 (PiezoMonitoring 등) 의 콜백과 stale reference 가 뒤엉킴.
|
||||||
androidx.compose.runtime.DisposableEffect(Unit) {
|
androidx.compose.runtime.DisposableEffect(Unit) {
|
||||||
onDispose { bleManager.sendLedMode(0) }
|
onDispose {
|
||||||
|
bleManager.sendLedMode(0)
|
||||||
|
bleManager.piezoCollector.onMultiChannelComplete = null
|
||||||
|
bleManager.imuCollector.onComplete = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detachment alert (rising edge → 팝업, 재부착 시 자동 해제)
|
// Detachment alert (rising edge → 팝업, 재부착 시 자동 해제)
|
||||||
|
|||||||
Reference in New Issue
Block a user