diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 528a773..c8c12f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,6 +67,15 @@ android { // 개발 진행 — 모든 신규 기능/실험/리팩토링은 dev에서만. } } + testOptions { + unitTests { + // android.util.Log 등 Android 스텁을 예외 대신 기본값으로 돌려준다. + // 알고리즘 대조 시험은 순수 계산인데 로그 한 줄 때문에 죽는 것을 막는다. + isReturnDefaultValues = true + } + } + + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt b/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt index 529abd4..36600eb 100644 --- a/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt +++ b/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt @@ -38,7 +38,20 @@ object PiezoHW { R300, // R3: raw 0/9/18/27° → Snell 0/-5.6/-11.2/-16.6° } - var activePreset: DevicePreset = DevicePreset.V0 + /** + * 현재 기기 프리셋. 바뀌면 dps 를 **즉시** 검출기 쪽에도 동기화한다. + * + * [WdConfig.DPS_DEFAULT] 는 검출 함수들의 기본 인자로 쓰여서, 프리셋이 바뀐 뒤 + * 아무도 [distancePerSample] 을 읽지 않으면 옛 값이 남는다. 실리콘(r*)으로 + * 바꿔 연결했는데 검출은 abs 의 1.936 으로 도는 상황이 정확히 그것이다. + */ + @Volatile private var _activePreset: DevicePreset = DevicePreset.V0 + var activePreset: DevicePreset + get() = _activePreset + set(value) { + _activePreset = value + WdConfig.DPS_DEFAULT = _dpsOverride ?: presetDps + } fun autoDetectPreset(deviceName: String) { if (deviceName.startsWith("VBT") && deviceName.length >= 4) { 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 4c9601e..650ecac 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 @@ -198,7 +198,30 @@ fun AnchorAlignView(appState: AppState) { } Column(Modifier.padding(horizontal = 16.dp)) { - StatusChip(isConnected, deviceName) + Row(verticalAlignment = Alignment.CenterVertically) { + StatusChip(isConnected, deviceName) + Spacer(Modifier.weight(1f)) + // 실리콘 기기를 번갈아 쓸 때 이 화면에서도 끊을 수 있어야 한다. + // 측정 중에는 막는다 — 중간에 끊으면 그 위치 데이터가 반쪽이 된다. + TextButton( + enabled = !measuring, + onClick = { + if (isConnected) { + appState.anchorCm = null + bleManager.disconnect() + } else { + appState.connectReturnScreen = AppScreen.ANCHOR_ALIGN + appState.currentScreen = AppScreen.DEVICE_SCAN + } + }, + ) { + Text( + if (isConnected) "연결 해제" else "기기 연결", + fontWeight = FontWeight.SemiBold, + color = if (isConnected) MlCritical else MlPrimary, + ) + } + } Spacer(Modifier.height(12.dp)) Text( diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt index ad9f285..46e4ac4 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt @@ -260,10 +260,28 @@ fun HospitalModeView(appState: AppState) { TextButton( enabled = !running, onClick = { - appState.connectReturnScreen = AppScreen.HOSPITAL_MODE - appState.currentScreen = AppScreen.DEVICE_SCAN + if (isConnected) { + // 실리콘 R100/R200/R300 을 번갈아 쓰려면 한 기기를 끊고 + // 다음 기기를 잡아야 한다. disconnect() 가 자동 재연결까지 + // 취소하므로 방금 끊은 기기로 되돌아가지 않는다. + // + // 기기가 바뀌면 **정렬도 무효**다. R100/R200/R300 은 빔 각도가 + // 서로 달라(0/-7/-14/-21 vs 0/-9/-18/-27) 같은 자리에서도 + // nch·cap_frac 이 달라진다 — 최적 부착 위치 자체가 다르다. + appState.anchorCm = null + bleManager.disconnect() + } else { + appState.connectReturnScreen = AppScreen.HOSPITAL_MODE + appState.currentScreen = AppScreen.DEVICE_SCAN + } }, - ) { Text(if (isConnected) "기기 변경" else "기기 연결", fontWeight = FontWeight.SemiBold) } + ) { + Text( + if (isConnected) "연결 해제" else "기기 연결", + fontWeight = FontWeight.SemiBold, + color = if (isConnected) MlCritical else MlPrimary, + ) + } } Spacer(Modifier.height(12.dp)) diff --git a/app/src/test/java/com/medithings/vesiscan/managers/PresetDpsTest.kt b/app/src/test/java/com/medithings/vesiscan/managers/PresetDpsTest.kt new file mode 100644 index 0000000..c73a811 --- /dev/null +++ b/app/src/test/java/com/medithings/vesiscan/managers/PresetDpsTest.kt @@ -0,0 +1,65 @@ +package com.medithings.vesiscan.managers + +import com.medithings.vesiscan.walldetect.core.WdConfig +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * 기기 프리셋 → dps · delay 대응. Python `config_hw` 표와 1:1. + * + * 같은 sample index 가 다른 깊이로 매핑되면 벽 좌표부터 어긋나므로, 이 표가 틀리면 + * 그 아래 전부가 레퍼런스와 달라진다. 그리고 프리셋을 바꾼 **직후** 검출기가 보는 + * [WdConfig.DPS_DEFAULT] 까지 따라가야 한다 — 실리콘 기기로 갈아 끼웠는데 검출이 + * abs 값으로 도는 일을 막는다. + */ +class PresetDpsTest { + + @Test + fun presetDrivesDpsAndDelay() { + val saved = PiezoHW.activePreset + try { + PiezoHW.clearDpsOverride() + // abs (구 임상기) + for (p in listOf(PiezoHW.DevicePreset.V0, PiezoHW.DevicePreset.V1, + PiezoHW.DevicePreset.V2)) { + PiezoHW.activePreset = p + assertEquals("$p dps", 1.936, PiezoHW.distancePerSample, 1e-9) + assertEquals("$p delay", 6.85, PiezoHW.delayOffsetMm, 1e-9) + assertEquals("$p WdConfig 동기화", 1.936, WdConfig.DPS_DEFAULT, 1e-9) + } + // silicon (최종 기기) + for (p in listOf(PiezoHW.DevicePreset.R100, PiezoHW.DevicePreset.R200, + PiezoHW.DevicePreset.R300)) { + PiezoHW.activePreset = p + assertEquals("$p dps", 1.897, PiezoHW.distancePerSample, 1e-9) + assertEquals("$p delay", 7.651, PiezoHW.delayOffsetMm, 1e-9) + assertEquals("$p WdConfig 동기화", 1.897, WdConfig.DPS_DEFAULT, 1e-9) + } + } finally { + PiezoHW.clearDpsOverride() + PiezoHW.activePreset = saved + } + } + + /** 기기 이름 → 프리셋. 구형 `VBT{YY}{MM}0{a}{bb}` · 실리콘 `VBT{YY}{MM}R{a}{bb}`. */ + @Test + fun deviceNameMapsToPreset() { + val saved = PiezoHW.activePreset + try { + val cases = listOf( + "VBT26080001" to PiezoHW.DevicePreset.V0, + "VBT26080201" to PiezoHW.DevicePreset.V1, + "VBT26080301" to PiezoHW.DevicePreset.V2, + "VBT2608R101" to PiezoHW.DevicePreset.R100, + "VBT2608R201" to PiezoHW.DevicePreset.R200, + "VBT2608R301" to PiezoHW.DevicePreset.R300, + ) + for ((name, want) in cases) { + PiezoHW.autoDetectPreset(name) + assertEquals(name, want, PiezoHW.activePreset) + } + } finally { + PiezoHW.activePreset = saved + } + } +}