diff --git a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt index fdb793a..54cc755 100644 --- a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt +++ b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt @@ -937,7 +937,12 @@ class BleManager private constructor(private val context: Context) { if (!gatt.requestMtu(247)) { 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 -> { logd { "Disconnected from ${gatt.device.address}" } @@ -1011,6 +1016,7 @@ class BleManager private constructor(private val context: Context) { handler.post { if (txCharacteristic != null && rxCharacteristic != null) { isServiceReady.value = true + onConnectionStateChanged?.invoke(true) startBatteryPolling() startWatchdog() logd { "Service ready (no CCCD)" } @@ -1021,6 +1027,7 @@ class BleManager private constructor(private val context: Context) { handler.post { if (txCharacteristic != null) { isServiceReady.value = true + onConnectionStateChanged?.invoke(true) startBatteryPolling() startWatchdog() logd { "Service ready (no RX)" } @@ -1036,6 +1043,10 @@ class BleManager private constructor(private val context: Context) { if (status == BluetoothGatt.GATT_SUCCESS) { if (txCharacteristic != null && rxCharacteristic != null) { 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로 협상되어 // Central Link Layer ACK 속도 ↑ → FW SoftDevice TX queue 포화 빈도 ↓ val priorityOk = try { diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PiezoMonitoringView.kt index 41dbdc7..eb6aa01 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PiezoMonitoringView.kt @@ -846,11 +846,17 @@ fun PiezoMonitoringView(appState: AppState) { } } - // PiezoMonitoring 이탈 시 imuCollector callback cleanup — 다른 화면 (PlacementGuide 등) 이 + // PiezoMonitoring 이탈 시 callback cleanup — 다른 화면 (PlacementGuide, ClinicalHome 등) 이 // 자기 콜백 등록하기 전에 우리 콜백 해제. 다시 진입하면 위 LaunchedEffect 가 재등록. + // 2026-07-07 fix: BleManager 콜백 (onConnectionStateChanged 등) 3개도 정리. 이전에는 + // 화면 전환 시 stale 콜백이 남아 새 화면 진입 후에도 이전 화면 로직 (startBatteryPolling + // 등) 이 발동 → GATT operation queue 겹침 → 좀비 세션의 간접 원인. androidx.compose.runtime.DisposableEffect(Unit) { onDispose { bleManager.imuCollector.onComplete = null + bleManager.onConnectionStateChanged = null + bleManager.onUnexpectedDisconnect = null + bleManager.onReconnectionFailed = null } } diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PlacementGuideView.kt index 0268ca5..72d4b5f 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PlacementGuideView.kt @@ -162,9 +162,16 @@ fun PlacementGuideView(appState: AppState) { 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) { - onDispose { bleManager.sendLedMode(0) } + onDispose { + bleManager.sendLedMode(0) + bleManager.piezoCollector.onMultiChannelComplete = null + bleManager.imuCollector.onComplete = null + } } // Detachment alert (rising edge → 팝업, 재부착 시 자동 해제)