fix(hospital): ImuSidecar.restore 가 남의 콜백을 되돌리지 않게 한다 · 단계별 로그
## 계기 — 그리고 정정 2026-09-28 A34 에서 0cm 정렬 뒤 `align_0cm_imu.csv` 가 없다고 판단해 원인을 쫓았다. BLE 로그·parseRim 로그로 IMU 가 20/20 도착한 것까지 확인한 뒤 "사이드카 콜백이 덮였다"는 가설로 진단 로그를 넣어 재설치했다. 그런데 파일은 **처음부터 있었다** — 0cm·1cm 모두 09:49/09:51 에 20 cycle × 15 sample 로 써졌다. `adb shell` 이 보는 `/sdcard`(FUSE · MediaProvider 뷰)가 앱이 방금 쓴 파일을 한동안 안 보여 준 것이고, `find -type f` 는 한 파일만, `find -mmin` 은 아무것도 돌려주지 않았다. 진단은 불필요했다. 코드에 결함은 없었다. ## 그래도 남기는 것 - `restore()` 방어: 지금 걸린 콜백이 **자기 것일 때만** 되돌린다. LaunchedEffect 가 재시작하면 옛 인스턴스의 restore 가 새 인스턴스의 install 뒤에 실행될 수 있고 (취소된 코루틴의 finally 는 나중에 돈다 · 새 코루틴은 Main.immediate 로 즉시 시작), 그러면 새 훅이 옛 previous 로 덮여 그 자리 20회가 전부 IMU 없이 저장된다. 이번엔 그 순서가 아니었지만 순서 자체는 가능하다. - 로그(태그 ImuSidecar · AnchorAlign): install 시 이전 콜백 종류 · 콜백 도착 · await 결과 · 회차별 imu 수. "IMU 가 왔나" 는 파형 해석에서 제일 먼저 묻는 질문인데 답이 BLE 로그를 뒤져야 나왔다. 이제 logcat 한 줄로 답이 나온다. ## 교훈 (도구) 앱이 공용 Downloads 에 방금 쓴 파일은 `adb shell ls/find` 에 늦게 나타난다. 파일 유무를 근거로 결론 내리기 전에 `run-as`(앱 내부 경로) 로 보거나, 몇 분 뒤 다시 보거나, 앱이 남긴 로그·업로드 표식으로 교차 확인할 것. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -326,7 +326,9 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
// IMU 는 piezo 뒤에 온다. 없으면 빈 목록 — 측정을 실패로 돌리지 않는다.
|
// 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 }
|
val sorted = got.sortedBy { it.channel }
|
||||||
liveChannels = sorted // 화면 파형 — 저장·판정에 쓰는 것과 같은 데이터
|
liveChannels = sorted // 화면 파형 — 저장·판정에 쓰는 것과 같은 데이터
|
||||||
detachWatcher.push(sorted.map { it.buffer })?.let { d -> detachSnapshot = d }
|
detachWatcher.push(sorted.map { it.buffer })?.let { d -> detachSnapshot = d }
|
||||||
|
|||||||
@@ -18,9 +18,16 @@
|
|||||||
* IMU 가 안 와도 측정은 성립한다 — 펌웨어·설정에 따라 `rim:` 이 없을 수 있고, 그때
|
* IMU 가 안 와도 측정은 성립한다 — 펌웨어·설정에 따라 `rim:` 이 없을 수 있고, 그때
|
||||||
* 측정을 실패로 돌리면 기존에 되던 일이 안 되게 된다. [await] 는 null 을 돌려주고
|
* 측정을 실패로 돌리면 기존에 되던 일이 안 되게 된다. [await] 는 null 을 돌려주고
|
||||||
* 호출부는 IMU 없이 저장한다.
|
* 호출부는 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
|
package com.medithings.vesiscan.ui.views.clinical
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.medithings.vesiscan.ble.BleManager
|
import com.medithings.vesiscan.ble.BleManager
|
||||||
import com.medithings.vesiscan.ble.ImuSample
|
import com.medithings.vesiscan.ble.ImuSample
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@@ -34,29 +41,59 @@ class ImuSidecar(private val ble: BleManager) {
|
|||||||
private val inbox = Channel<List<ImuSample>>(Channel.CONFLATED)
|
private val inbox = Channel<List<ImuSample>>(Channel.CONFLATED)
|
||||||
private var previous: ((List<ImuSample>) -> Unit)? = null
|
private var previous: ((List<ImuSample>) -> Unit)? = null
|
||||||
private var installed = false
|
private var installed = false
|
||||||
|
private val id = Integer.toHexString(System.identityHashCode(this))
|
||||||
|
|
||||||
|
/** 우리가 건 콜백. restore 때 "아직 우리 것인가"를 참조로 비교한다. */
|
||||||
|
private val mine: (List<ImuSample>) -> Unit = { samples ->
|
||||||
|
val r = inbox.trySend(samples)
|
||||||
|
Log.d(TAG, "[$id] onComplete <- ${samples.size} samples · trySend ok=${r.isSuccess} closed=${r.isClosed}")
|
||||||
|
}
|
||||||
|
|
||||||
/** 콜백을 가로챈다. 원래 콜백은 [restore] 에서 되돌린다. */
|
/** 콜백을 가로챈다. 원래 콜백은 [restore] 에서 되돌린다. */
|
||||||
fun install() {
|
fun install() {
|
||||||
if (installed) return
|
if (installed) return
|
||||||
previous = ble.imuCollector.onComplete
|
previous = ble.imuCollector.onComplete
|
||||||
ble.imuCollector.onComplete = { samples -> inbox.trySend(samples) }
|
ble.imuCollector.onComplete = mine
|
||||||
installed = true
|
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 쪽 비우기와 같은 자리에서 부른다. */
|
/** 직전 회차에서 늦게 도착한 것을 버린다. piezo 쪽 비우기와 같은 자리에서 부른다. */
|
||||||
fun drain() {
|
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 없이 진행한다. */
|
/** piezo 를 받은 뒤 부른다. 시간 안에 안 오면 null — 호출부는 IMU 없이 진행한다. */
|
||||||
suspend fun await(graceMs: Long = IMU_GRACE_MS): List<ImuSample>? =
|
suspend fun await(graceMs: Long = IMU_GRACE_MS): List<ImuSample>? {
|
||||||
withTimeoutOrNull(graceMs) { inbox.receive() }
|
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() {
|
fun restore() {
|
||||||
if (!installed) return
|
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
|
previous = null
|
||||||
installed = false
|
installed = false
|
||||||
inbox.close()
|
inbox.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "ImuSidecar"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user