diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/AnchorAlignView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/AnchorAlignView.kt index b84fb26..da8a9e7 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/AnchorAlignView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/AnchorAlignView.kt @@ -326,7 +326,9 @@ fun AnchorAlignView(appState: AppState) { return@LaunchedEffect } // IMU 는 piezo 뒤에 온다. 없으면 빈 목록 — 측정을 실패로 돌리지 않는다. - cyclesImu.add(imuSide.await() ?: emptyList()) + val imu = imuSide.await() ?: emptyList() + cyclesImu.add(imu) + android.util.Log.d("AnchorAlign", "cycle $i/${AnchorConfig.ALIGN_CYCLES} imu=${imu.size}") val sorted = got.sortedBy { it.channel } liveChannels = sorted // 화면 파형 — 저장·판정에 쓰는 것과 같은 데이터 detachWatcher.push(sorted.map { it.buffer })?.let { d -> detachSnapshot = d } diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ImuSidecar.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ImuSidecar.kt index a7484a8..6ee6ce5 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ImuSidecar.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ImuSidecar.kt @@ -18,9 +18,16 @@ * IMU 가 안 와도 측정은 성립한다 — 펌웨어·설정에 따라 `rim:` 이 없을 수 있고, 그때 * 측정을 실패로 돌리면 기존에 되던 일이 안 되게 된다. [await] 는 null 을 돌려주고 * 호출부는 IMU 없이 저장한다. + * + * ## 2026-09-28 진단 로그 · 방어적 restore + * A34 실기에서 0cm 정렬 20회가 `rim:` 을 전부 받았는데(BLE 로그 · parseRim 로그 확인) + * `align_0cm_imu.csv` 가 안 생겼다. 콜백 사슬 어디서 끊기는지 보려고 단계마다 로그를 + * 남긴다(태그 `ImuSidecar`). 아울러 [restore] 는 **지금 걸린 콜백이 자기 것일 때만** + * 되돌린다 — 옛 인스턴스의 restore 가 새 인스턴스의 install 을 덮는 순서가 가능하다. */ package com.medithings.vesiscan.ui.views.clinical +import android.util.Log import com.medithings.vesiscan.ble.BleManager import com.medithings.vesiscan.ble.ImuSample import kotlinx.coroutines.channels.Channel @@ -34,29 +41,59 @@ class ImuSidecar(private val ble: BleManager) { private val inbox = Channel>(Channel.CONFLATED) private var previous: ((List) -> Unit)? = null private var installed = false + private val id = Integer.toHexString(System.identityHashCode(this)) + + /** 우리가 건 콜백. restore 때 "아직 우리 것인가"를 참조로 비교한다. */ + private val mine: (List) -> Unit = { samples -> + val r = inbox.trySend(samples) + Log.d(TAG, "[$id] onComplete <- ${samples.size} samples · trySend ok=${r.isSuccess} closed=${r.isClosed}") + } /** 콜백을 가로챈다. 원래 콜백은 [restore] 에서 되돌린다. */ fun install() { if (installed) return previous = ble.imuCollector.onComplete - ble.imuCollector.onComplete = { samples -> inbox.trySend(samples) } + ble.imuCollector.onComplete = mine installed = true + Log.d( + TAG, + "[$id] install · previous=${previous?.javaClass?.name ?: "null"}" + + " · ble=${Integer.toHexString(System.identityHashCode(ble))}" + + " · collector=${Integer.toHexString(System.identityHashCode(ble.imuCollector))}", + ) } /** 직전 회차에서 늦게 도착한 것을 버린다. piezo 쪽 비우기와 같은 자리에서 부른다. */ fun drain() { - while (inbox.tryReceive().isSuccess) { /* 비우기 */ } + var n = 0 + while (inbox.tryReceive().isSuccess) n++ + if (n > 0) Log.d(TAG, "[$id] drain · dropped=$n") } /** piezo 를 받은 뒤 부른다. 시간 안에 안 오면 null — 호출부는 IMU 없이 진행한다. */ - suspend fun await(graceMs: Long = IMU_GRACE_MS): List? = - withTimeoutOrNull(graceMs) { inbox.receive() } + suspend fun await(graceMs: Long = IMU_GRACE_MS): List? { + val hookIsMine = ble.imuCollector.onComplete === mine + val got = withTimeoutOrNull(graceMs) { inbox.receive() } + Log.d(TAG, "[$id] await -> ${got?.size?.toString() ?: "TIMEOUT"} · hookIsMine=$hookIsMine") + return got + } fun restore() { if (!installed) return - ble.imuCollector.onComplete = previous + val current = ble.imuCollector.onComplete + if (current === mine) { + ble.imuCollector.onComplete = previous + Log.d(TAG, "[$id] restore · ok") + } else { + // 우리 뒤에 누군가 다시 걸었다 — 그 사람 것을 되돌리면 안 된다. + Log.w(TAG, "[$id] restore · SKIPPED — hook is not mine (${current?.javaClass?.name ?: "null"})") + } previous = null installed = false inbox.close() } + + private companion object { + const val TAG = "ImuSidecar" + } }