feat: 측정 가로모드 대응 + Ant Offset + Scan Log + 다중 sanity check
## 측정 화면 (PiezoMonitoringView)
- BoxWithConstraints + donutFitSize: 가로/작은 화면에서도 donut+Bladdy+buttons
모두 들어오도록 height-aware sizing (cap 120~360dp)
- nonDevSpacing: available height 기반 단계 (18/22/28/36) — 큰 화면 답답함 해소
- center 4 중 1개라도 missing → 즉시 realign prompt (이전: AutoMeasure 5회 누적)
- 6채널 중 3개 이상 missing 시 위치 이탈 알림 (모드 무관)
- 다이얼로그 문구: "Position Lost or Insufficient Gel"
- V41 결과 후처리 — walls를 PiezoEchoAnalyzer 기준으로 강제 (UI 차트와 일관)
· ant = analyzer.result.ant
· post = analyzer.result.lowEnd + 3
- CH3 secondary echo 잡힘 / CH4·CH5 lateral ant too-deep 문제 해결
- Bladdy 3-tap → Developer Mode toggle (측정 화면에서도)
## Placement 가이드 (PlacementGuideView)
- 가로 모드 대응: bodyW = min(w, h*0.7f) — 태블릿 가로에서 portrait 비율 유지
- Sensor 이미지를 Canvas drawImage 로 이동 — bladder oval 좌표계와 정확히 정렬
- Start Alignment 버튼: enabled = true 명시
## Settings overlay
- "Ant Offset" 숫자 입력 필드 (0~20mm, dev mode 전용)
· BV 계산 시 ant 를 입력값만큼 probe 쪽으로 당김 (chord 증가 → BV 증가)
· mm→samples 변환은 PiezoHW.distancePerSample
· V41 SphereFit BV 는 offset≠0 일 때 우회 → 6ch estimate
- "Version" row 추가 (BuildConfig.VERSION_NAME)
- Save 버튼: 실제 UserStorage 영구 저장 + state 재전파
· AppState.savePiezoSettingsNow() 추가
- 전체 overlay 에 verticalScroll + Card maxHeight 90% — dev 컨텐츠 길어도 Save 도달
## DPS 동기화 (긴급 수정)
설정의 DPS 변경이 V41Detector(Geometry/AnatomicalGate)에 반영 안 되던 문제:
- WdConfig.DPS_DEFAULT: const val → @Volatile @JvmField var
- PiezoHW.distancePerSample setter 가 WdConfig.DPS_DEFAULT 자동 동기화
- 시작 시점 1.968.also { WdConfig.DPS_DEFAULT = it } 로 두 값 일치
## 버전
- versionName: VBTAND0101 → 2026005-101
- versionCode: 2 → 3
## 신규 모듈
- services/ScanLogStore.kt — dev mode 진단 로그 (마지막 Single Scan summary)
· WALLS CSV (ch,ant,post,urine), DATA CSV (raw ADC), antOffsetMm
- ui/components/ScanLogDialog.kt — 클립보드 복사 다이얼로그
· xbench (CharlesKWONsLaw) MbbLogDialog 패턴 차용
· SelectionContainer + monospace + Copy/Clear/Close
- 측정 화면 dev panel 에 "Scan Log (N)" 버튼 추가
## 데이터 모델
- PiezoSettings.antOffsetMm: Int = 0 필드 추가
## 단위 테스트
- V41DetectorCH4Test.kt — 2026-05-06 floating-signal 캡쳐 6채널 raw 를
V41Detector.detect 에 통과시켜 결과 출력 (CH4 가짜 wall 진단용)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package com.example.medilightv2android.walldetect
|
||||
|
||||
import com.example.medilightv2android.walldetect.dto.ProbeProfileDto
|
||||
import com.example.medilightv2android.walldetect.dto.SweepInput
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Diagnostic run for the 2026-05-06 floating-signal capture (charleskwon mail
|
||||
* dated 5/6). The 100-sample stream for each of the 6 channels is fed straight
|
||||
* through `V41Detector.detect(...)` so the team can see exactly which channels
|
||||
* the production algorithm keeps, which it gates out, and on what score / tier.
|
||||
*
|
||||
* No assertions — output is meant to be read off the JUnit console. Run with
|
||||
* ./gradlew :app:testDebugUnitTest --tests "*V41DetectorCH4Test*"
|
||||
* or use the green-arrow gutter in Android Studio.
|
||||
*/
|
||||
class V41DetectorCH4Test {
|
||||
|
||||
@Test
|
||||
fun runFloatingSignalCaptureThroughV41() {
|
||||
val sweep = SweepInput(
|
||||
requestId = "ch4-floating-2026-05-06",
|
||||
timestampMs = 0L,
|
||||
deviceId = "ckl-mail",
|
||||
sweepSeq = 0,
|
||||
probe = ProbeProfileDto(
|
||||
name = "V1",
|
||||
rMm = 50.2,
|
||||
anteriorAnchorMm = 32.0,
|
||||
fsHz = 537500L,
|
||||
cMmPerUs = 1.54,
|
||||
// V1 housing → Snell-refracted angles (informational; runtime uses WdProbe)
|
||||
anglesDeg = listOf(6.89, 0.0, -6.89, -13.66, -6.87, -6.87),
|
||||
),
|
||||
adc = listOf(CH0, CH1, CH2, CH3, CH4, CH5),
|
||||
)
|
||||
|
||||
val result = V41Detector().detect(sweep)
|
||||
|
||||
println()
|
||||
println("════════════════════════════════════════════════════════════════════════")
|
||||
println(" V4.1 result — floating-signal capture (CH0..CH5)")
|
||||
println("════════════════════════════════════════════════════════════════════════")
|
||||
println(" algorithm : ${result.algorithm} v=${result.algorithmVersion}")
|
||||
println(" processingMs : ${"%.2f".format(result.processingMs)}")
|
||||
println(" gatedCount : ${result.summary.gatedCount}/6")
|
||||
println(" matchCount : ${result.summary.matchCount}/6")
|
||||
println(" tier(sweep) : ${result.summary.tier} scoreMean=${result.summary.scoreMean}")
|
||||
println(" chordMmMean : ${result.summary.chordMmMean} std=${result.summary.chordMmStd}")
|
||||
println(" v41Sphere : ${result.summary.v41Sphere?.let { "R=${it.radiusMm}mm BV=${it.bvMl}mL n=${it.nPoints}" } ?: "—(gated<4)"}")
|
||||
println(" bvDispatch : ${result.summary.bvDispatch?.let { "method=${it.methodChosen} bv=${it.bvMl}mL" } ?: "—"}")
|
||||
println()
|
||||
println(" CH | ant post | gated | score tier | sContrast tier | antMm postMm chord")
|
||||
println(" ---+-----------+-------+--------------+---------------------+----------------------")
|
||||
result.perChannel.forEach { cr ->
|
||||
val d = cr.v41Diag
|
||||
println(
|
||||
" %1d | %4s %4s | %5s | %.3f %5s | %.3f %-9s | %6s %6s %6s".format(
|
||||
cr.ch,
|
||||
cr.antIdx?.toString() ?: "—",
|
||||
cr.postIdx?.toString() ?: "—",
|
||||
d?.gated?.toString() ?: "—",
|
||||
d?.score ?: 0f,
|
||||
d?.tier ?: "—",
|
||||
d?.sContrast ?: 0f,
|
||||
d?.sContrastTier ?: "—",
|
||||
cr.antMm?.let { "%.1f".format(it) } ?: "—",
|
||||
cr.postMm?.let { "%.1f".format(it) } ?: "—",
|
||||
cr.chordMm?.let { "%.1f".format(it) } ?: "—",
|
||||
)
|
||||
)
|
||||
}
|
||||
println()
|
||||
println(" Per-channel lumen + peak count")
|
||||
println(" CH | lumen[start,end] | peaks(OS-CFAR pass)")
|
||||
println(" ---+------------------+--------------------")
|
||||
result.perChannel.forEach { cr ->
|
||||
val d = cr.v41Diag
|
||||
val lumen = if (cr.lumenStart != null && cr.lumenEnd != null)
|
||||
"[${cr.lumenStart}, ${cr.lumenEnd}] (n=${cr.lumenEnd - cr.lumenStart})"
|
||||
else "—"
|
||||
val peaks = d?.peaksAll?.size ?: 0
|
||||
println(" %1d | %-16s | %d peak(s): %s".format(cr.ch, lumen, peaks, d?.peaksAll?.joinToString(",") ?: ""))
|
||||
}
|
||||
println("════════════════════════════════════════════════════════════════════════")
|
||||
}
|
||||
|
||||
companion object {
|
||||
// 6 × 100 ADC samples — copied verbatim from the 2026-05-06 mbb log.
|
||||
private val CH0 = listOf(
|
||||
1840, 1686, 1626, 1471, 1653, 1621, 1558, 1563, 1510, 1743,
|
||||
1498, 1405, 1092, 1382, 961, 892, 845, 848, 816, 839,
|
||||
839, 829, 825, 850, 836, 831, 834, 846, 808, 826,
|
||||
836, 831, 834, 835, 841, 831, 840, 816, 824, 827,
|
||||
814, 837, 825, 823, 828, 841, 830, 823, 867, 1109,
|
||||
1232, 1070, 1096, 1035, 1004, 951, 958, 1079, 1146, 1214,
|
||||
984, 1008, 1052, 1100, 975, 1010, 1226, 922, 1305, 2221,
|
||||
2003, 1867, 1439, 1199, 1119, 1322, 1081, 1053, 1135, 1136,
|
||||
984, 1024, 1030, 1070, 895, 920, 990, 974, 878, 937,
|
||||
841, 835, 846, 863, 863, 972, 932, 883, 877, 878,
|
||||
)
|
||||
private val CH1 = listOf(
|
||||
1954, 1570, 1595, 1592, 1449, 1422, 1476, 1551, 1598, 1498,
|
||||
1478, 1457, 1642, 1369, 1161, 890, 880, 905, 899, 888,
|
||||
887, 893, 897, 897, 908, 882, 901, 876, 869, 894,
|
||||
909, 894, 899, 894, 910, 886, 905, 894, 892, 882,
|
||||
910, 877, 890, 886, 899, 915, 890, 888, 885, 874,
|
||||
1111, 1355, 1041, 1048, 1199, 1155, 1144, 1064, 1293, 1089,
|
||||
1215, 1052, 1047, 961, 987, 1192, 1256, 1187, 1596, 1831,
|
||||
1648, 1426, 981, 1056, 1057, 1100, 1041, 1247, 1119, 1223,
|
||||
1124, 972, 976, 943, 936, 892, 880, 905, 936, 906,
|
||||
919, 890, 943, 1126, 1222, 1049, 971, 1002, 1000, 911,
|
||||
)
|
||||
private val CH2 = listOf(
|
||||
1810, 1750, 1698, 1626, 1664, 1188, 1538, 1456, 1231, 1261,
|
||||
1514, 1616, 1112, 1459, 1039, 867, 858, 835, 872, 834,
|
||||
829, 847, 850, 818, 841, 822, 844, 834, 829, 862,
|
||||
870, 831, 864, 882, 844, 846, 896, 867, 862, 890,
|
||||
830, 876, 868, 833, 867, 874, 1115, 974, 1036, 1234,
|
||||
1271, 1094, 1363, 1140, 1061, 1189, 910, 1106, 1213, 964,
|
||||
1070, 1033, 1155, 1091, 1097, 965, 1203, 949, 1413, 1373,
|
||||
1198, 1211, 1043, 946, 891, 1309, 1298, 1364, 1071, 1009,
|
||||
990, 902, 905, 893, 901, 868, 869, 865, 862, 872,
|
||||
873, 923, 911, 1369, 1089, 1132, 960, 918, 1006, 985,
|
||||
)
|
||||
private val CH3 = listOf(
|
||||
1812, 1796, 1701, 1585, 1414, 1619, 1607, 1394, 1536, 1502,
|
||||
1157, 1469, 1327, 1413, 1340, 1480, 1440, 1111, 986, 894,
|
||||
916, 855, 851, 848, 856, 823, 848, 828, 844, 851,
|
||||
870, 854, 853, 859, 898, 888, 946, 1093, 1185, 1081,
|
||||
1210, 1089, 1217, 1145, 1007, 1112, 1278, 1155, 1099, 1205,
|
||||
1037, 991, 1129, 1041, 936, 1113, 1060, 1091, 996, 918,
|
||||
1073, 1154, 892, 938, 945, 1144, 1051, 1000, 1250, 1247,
|
||||
999, 1144, 1176, 1184, 1940, 1890, 1593, 1155, 1150, 1012,
|
||||
878, 1029, 893, 857, 852, 873, 857, 869, 842, 867,
|
||||
847, 858, 858, 1119, 972, 1007, 873, 1003, 864, 879,
|
||||
)
|
||||
private val CH4 = listOf(
|
||||
1604, 1585, 1608, 1549, 1708, 1451, 1548, 1618, 1712, 1616,
|
||||
1230, 1422, 1687, 1440, 1244, 844, 895, 850, 824, 838,
|
||||
871, 839, 839, 826, 818, 848, 815, 825, 849, 850,
|
||||
846, 825, 834, 879, 831, 838, 818, 834, 854, 827,
|
||||
848, 838, 839, 861, 830, 826, 844, 844, 856, 933,
|
||||
1052, 1253, 1035, 1304, 1143, 1003, 1252, 1217, 1196, 979,
|
||||
1184, 937, 1242, 1047, 1056, 961, 1150, 907, 1465, 2028,
|
||||
1973, 1388, 1143, 985, 1230, 1048, 997, 1228, 1344, 1049,
|
||||
913, 893, 1005, 914, 977, 880, 869, 1014, 1015, 910,
|
||||
876, 905, 933, 881, 967, 950, 933, 915, 921, 915,
|
||||
)
|
||||
private val CH5 = listOf(
|
||||
1796, 1725, 1694, 1605, 1543, 1588, 1589, 1562, 1527, 1769,
|
||||
1785, 1760, 1614, 1474, 1326, 899, 872, 863, 870, 967,
|
||||
887, 866, 828, 842, 852, 847, 864, 831, 845, 837,
|
||||
853, 850, 867, 837, 833, 826, 820, 837, 829, 861,
|
||||
843, 829, 843, 837, 826, 832, 845, 951, 1277, 1234,
|
||||
1429, 1257, 1253, 1286, 1180, 1189, 1249, 1162, 1110, 972,
|
||||
963, 1126, 1310, 1129, 1137, 917, 1171, 1287, 1199, 1610,
|
||||
1782, 1355, 1257, 1341, 1135, 1088, 1012, 995, 955, 1041,
|
||||
1182, 945, 909, 980, 956, 941, 928, 866, 880, 908,
|
||||
846, 868, 900, 959, 1032, 921, 963, 898, 872, 855,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user