feat(clinical): Clinical Live 에 실시간 BV 표시 + Method 전환 chip
시험용 앱 (Clinical mode 6ch) 에서 지금까지 BV 추정값이 표시되지 않아 시험
중 참고 불가능. 6ch grid 아래에 BV 카드 신설.
- 3 가지 BvMethod 를 cycle 마다 병행 계산:
* V41 : Method C SphereFit (V4.1 detector 이미 계산한 값 재사용)
* FRUSTUM : V4.1 walls → estimateBladderVolume6ch (Tanaka frustum)
* METHOD_D: MethodDRunner walls (apply_cross=true) → estimateBladderVolume6ch
(임상 Python `for_app_share` 파이프라인 1:1)
- 카드 상단: 선택된 method 의 BV 를 26sp ExtraBold 로 크게 표시
- 하단 FilterChip 3 개: method 이름 + 각 method BV 값 요약 (예: "V41: 342")
chip 탭 시 즉시 큰 숫자 갱신 + GreenZoneConstants.bvMethod 동기 업데이트
→ 다른 화면 (측정 모드) 에도 전역 반영
session log 에는 저장 안 함 (표시 전용). 저장 필요 시 별도 이슈.
This commit is contained in:
@@ -81,6 +81,17 @@ fun ClinicalLiveView(appState: AppState) {
|
||||
// V4.1 wall detection — 채널별 ant/post peak 표시용 (사내 테스트 가시화 요청)
|
||||
val v41 = remember { com.medithings.vesiscan.walldetect.V41Detector() }
|
||||
var channelWalls by remember { mutableStateOf<List<Pair<Int, Int>?>>(List(6) { null }) }
|
||||
// BV 실시간 표시 — cycle 마다 3 가지 method 병행 계산 (2026-07-15 사용자 요청).
|
||||
// V41 = SphereFit (Method C)
|
||||
// FRUSTUM = legacy analyzer walls + estimateBladderVolume6ch (Tanaka frustum)
|
||||
// METHOD_D = Method D walls + apply_cross + estimateBladderVolume6ch (임상 Python 1:1)
|
||||
var bvV41 by remember { mutableStateOf<Double?>(null) }
|
||||
var bvFrustum by remember { mutableStateOf<Double?>(null) }
|
||||
var bvMethodD by remember { mutableStateOf<Double?>(null) }
|
||||
// 사용자가 chip 으로 선택한 표시 method (GreenZoneConstants.bvMethod 와 동기).
|
||||
var selectedBvMethod by remember {
|
||||
mutableStateOf(com.medithings.vesiscan.managers.GreenZoneConstants.bvMethod)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
bleManager.piezoCollector.onMultiChannelComplete = { channels ->
|
||||
@@ -109,6 +120,24 @@ fun ClinicalLiveView(appState: AppState) {
|
||||
val a = r?.antIdx; val p = r?.postIdx
|
||||
if (a != null && p != null) Pair(a, p) else null
|
||||
}
|
||||
// BV[V41] = SphereFit 결과 (V4.1 detector 가 이미 계산).
|
||||
bvV41 = result.summary.bvDispatch?.bvMl?.toDouble()
|
||||
// BV[FRUSTUM] = V4.1 walls 를 frustum 알고리즘에 통과.
|
||||
bvFrustum = com.medithings.vesiscan.managers.estimateBladderVolume6ch(channelWalls)
|
||||
?.volumeMl?.takeIf { it.isFinite() && it > 0 }
|
||||
// BV[METHOD_D] = MethodD walls (apply_cross=true) + frustum.
|
||||
val signalsD = (0..5).map { ch ->
|
||||
channels.find { it.channel == ch }?.buffer?.let { buf ->
|
||||
DoubleArray(buf.size) { buf[it].toDouble() }
|
||||
} ?: DoubleArray(100)
|
||||
}
|
||||
val mdResults = com.medithings.vesiscan.walldetect.MethodDRunner
|
||||
.detectMultichannel(signalsD, applyCross = true)
|
||||
val mdWalls: List<Pair<Int, Int>?> = mdResults.map { r ->
|
||||
r?.let { Pair(it.ant, it.post) }
|
||||
}
|
||||
bvMethodD = com.medithings.vesiscan.managers.estimateBladderVolume6ch(mdWalls)
|
||||
?.volumeMl?.takeIf { it.isFinite() && it > 0 }
|
||||
}
|
||||
}
|
||||
bleManager.imuCollector.onComplete = { imuSamples ->
|
||||
@@ -181,6 +210,59 @@ fun ClinicalLiveView(appState: AppState) {
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// ── BV 실시간 표시 + Method 선택 chip (2026-07-15) ──
|
||||
// 6ch grid 아래에 세 method (V41/FRUSTUM/METHOD_D) 를 chip 으로 전환하며
|
||||
// 선택된 method 의 BV 를 큼직하게 표시. session log 에는 저장 안 함 (표시 전용).
|
||||
val bvByMethod = mapOf(
|
||||
com.medithings.vesiscan.managers.BvMethod.V41 to bvV41,
|
||||
com.medithings.vesiscan.managers.BvMethod.FRUSTUM to bvFrustum,
|
||||
com.medithings.vesiscan.managers.BvMethod.METHOD_D to bvMethodD,
|
||||
)
|
||||
val currentBv = bvByMethod[selectedBvMethod]
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MlCardBackground),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("BV", fontSize = 12.sp, color = MlSecondaryText,
|
||||
fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(currentBv?.let { "${"%.1f".format(it)} mL" } ?: "— mL",
|
||||
fontSize = 26.sp, fontWeight = FontWeight.ExtraBold,
|
||||
color = MlPrimary)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(selectedBvMethod.name, fontSize = 11.sp,
|
||||
color = MlSecondaryText, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
// Method 전환 chip row — 클릭 시 GreenZoneConstants.bvMethod 도 동기 갱신.
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
com.medithings.vesiscan.managers.BvMethod.values().forEach { m ->
|
||||
val selected = m == selectedBvMethod
|
||||
val bv = bvByMethod[m]
|
||||
FilterChip(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
selectedBvMethod = m
|
||||
com.medithings.vesiscan.managers.GreenZoneConstants.bvMethod = m
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
"${m.name}: ${bv?.let { "%.0f".format(it) } ?: "-"}",
|
||||
fontSize = 11.sp,
|
||||
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// ── IMU 정보 ──
|
||||
if (lastImu.isNotEmpty()) {
|
||||
val posture = remember(lastImu) { ImuAnalysis.estimatePosture(lastImu) }
|
||||
|
||||
Reference in New Issue
Block a user