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 표시용 (사내 테스트 가시화 요청)
|
// V4.1 wall detection — 채널별 ant/post peak 표시용 (사내 테스트 가시화 요청)
|
||||||
val v41 = remember { com.medithings.vesiscan.walldetect.V41Detector() }
|
val v41 = remember { com.medithings.vesiscan.walldetect.V41Detector() }
|
||||||
var channelWalls by remember { mutableStateOf<List<Pair<Int, Int>?>>(List(6) { null }) }
|
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) {
|
DisposableEffect(Unit) {
|
||||||
bleManager.piezoCollector.onMultiChannelComplete = { channels ->
|
bleManager.piezoCollector.onMultiChannelComplete = { channels ->
|
||||||
@@ -109,6 +120,24 @@ fun ClinicalLiveView(appState: AppState) {
|
|||||||
val a = r?.antIdx; val p = r?.postIdx
|
val a = r?.antIdx; val p = r?.postIdx
|
||||||
if (a != null && p != null) Pair(a, p) else null
|
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 ->
|
bleManager.imuCollector.onComplete = { imuSamples ->
|
||||||
@@ -181,6 +210,59 @@ fun ClinicalLiveView(appState: AppState) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
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 정보 ──
|
// ── IMU 정보 ──
|
||||||
if (lastImu.isNotEmpty()) {
|
if (lastImu.isNotEmpty()) {
|
||||||
val posture = remember(lastImu) { ImuAnalysis.estimatePosture(lastImu) }
|
val posture = remember(lastImu) { ImuAnalysis.estimatePosture(lastImu) }
|
||||||
|
|||||||
Reference in New Issue
Block a user