From e5b252b314f134accac8e278ad5c20b88816a537 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Thu, 7 May 2026 17:28:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Detection/BV=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20+=20Spot=205=ED=9A=8C=20trimmedMean=20+=20?= =?UTF-8?q?UI=20=EC=97=AC=EC=9C=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detection(A/B/C)과 BV(Frustum/V41) 독립 설정 (개발자 모드) 기본값: Detection=C, BV=V41 - Spot: 3회 median → 5회 1.5초간격 trimmed mean (Auto와 동일) 최대 8회 재시도, 최소 유효 3개 - 일반 모드 도넛 320dp, 간격 28dp, 버튼 62dp (화면 여유) - Placement mm 표시 제거 — 방향만 안내 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../managers/GreenZoneConstants.kt | 2 + .../views/monitoring/PiezoMonitoringView.kt | 90 ++++++++++++++----- .../ui/views/monitoring/PlacementGuideView.kt | 6 +- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/managers/GreenZoneConstants.kt b/app/src/main/java/com/example/medilightv2android/managers/GreenZoneConstants.kt index 0668bb5..fd3819d 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/GreenZoneConstants.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/GreenZoneConstants.kt @@ -14,10 +14,12 @@ package com.example.medilightv2android.managers */ enum class DetectionMethod { METHOD_A, METHOD_B, METHOD_C } enum class PlacementGuideMode { SIMPLE, BOUNDARY, SWEEP } +enum class BvMethod { FRUSTUM, V41 } object GreenZoneConstants { var detectionMethod: DetectionMethod = DetectionMethod.METHOD_C + var bvMethod: BvMethod = BvMethod.V41 var placementGuideMode: PlacementGuideMode = PlacementGuideMode.SIMPLE // ═══════════════════════════════════════════════════════════ diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index ed2e994..5c2dbc8 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -239,9 +239,14 @@ fun PiezoMonitoringView(appState: AppState) { } val rawADC = channels.sortedBy { it.channel }.map { it.buffer } - if (method == com.example.medilightv2android.managers.DetectionMethod.METHOD_C && methodCBvMl != null) { - rawVolumeMl = methodCBvMl - Log.d("PiezoMonitor", "BV[C]: ${"%.1f".format(methodCBvMl)}ml (V4.1, walls=${totalWallCount}/6)") + val bvMethodSetting = com.example.medilightv2android.managers.GreenZoneConstants.bvMethod + val useV41Bv = method == com.example.medilightv2android.managers.DetectionMethod.METHOD_C + && bvMethodSetting == com.example.medilightv2android.managers.BvMethod.V41 + && methodCBvMl != null + + if (useV41Bv) { + rawVolumeMl = methodCBvMl!! + Log.d("PiezoMonitor", "BV[C-V41]: ${"%.1f".format(methodCBvMl)}ml (SphereFit, walls=${totalWallCount}/6)") bleManager.debugLogger.measurementResult(methodCBvMl, 1.0, totalWallCount, (0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 }) logService.logMeasurement(methodCBvMl, channelLogs, rawADC) @@ -744,7 +749,10 @@ fun PiezoMonitoringView(appState: AppState) { .padding(horizontal = hPad) .padding(top = if (isCompact) 8.dp else 16.dp, bottom = if (isCompact) 16.dp else 32.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(if (isCompact) 12.dp else 20.dp) + verticalArrangement = if (appState.isDevMode) + Arrangement.spacedBy(if (isCompact) 12.dp else 20.dp) + else + Arrangement.spacedBy(if (isCompact) 20.dp else 28.dp) ) { // Status badge @@ -760,7 +768,11 @@ fun PiezoMonitoringView(appState: AppState) { ) // Donut chart with Bladdy - val donutSize = if (isCompact) 220.dp else 280.dp + val donutSize = if (appState.isDevMode) { + if (isCompact) 220.dp else 280.dp + } else { + if (isCompact) 260.dp else 320.dp // 일반 모드: 더 크게 + } // maxVolume 기준 비율 (0.0~1.0, 부드러운 연속값) val maxVolumeForDonut = appState.piezoSettings.maxVolume.toDouble() val donutProgress = if (maxVolumeForDonut > 0 && maxVolumeMl > 0) @@ -795,16 +807,17 @@ fun PiezoMonitoringView(appState: AppState) { color = Color(0xFFFF9800)) } } else { + val volFontSize = if (appState.isDevMode) 18.sp else 22.sp Text( if (maxVolumeMl > 0) "${"%.0f".format(maxVolumeMl)} ml" else "${appState.estimatedVolume} ml", - fontSize = 18.sp, + fontSize = volFontSize, fontWeight = FontWeight.Bold, color = urgency.accentColor, modifier = Modifier .clip(RoundedCornerShape(12.dp)) .background(MlCardBackground) .shadow(4.dp, RoundedCornerShape(12.dp), spotColor = Color.Black.copy(alpha = 0.08f)) - .padding(horizontal = 14.dp, vertical = 6.dp) + .padding(horizontal = 16.dp, vertical = 8.dp) ) } } @@ -822,11 +835,12 @@ fun PiezoMonitoringView(appState: AppState) { } // Control buttons: Void/Catheterize / Auto / Spot - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + val btnHeight = if (appState.isDevMode) 56.dp else 62.dp + Row(horizontalArrangement = Arrangement.spacedBy(if (appState.isDevMode) 8.dp else 10.dp)) { // Void / Catheterize Button( onClick = { showCatheterizeSheet = true }, - modifier = Modifier.weight(1.3f).height(56.dp), + modifier = Modifier.weight(1.3f).height(btnHeight), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors(containerColor = MlPrimary.copy(alpha = 0.1f)) ) { @@ -841,7 +855,7 @@ fun PiezoMonitoringView(appState: AppState) { onClick = { if (isAutoMeasuring) stopAutoMeasure() else startAutoMeasure() }, - modifier = Modifier.weight(1f).height(56.dp), + modifier = Modifier.weight(1f).height(btnHeight), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors( containerColor = if (isAutoMeasuring) Color.Red else Color(0xFFFF9800) @@ -854,7 +868,7 @@ fun PiezoMonitoringView(appState: AppState) { } } - // Spot (3회 측정 → median, 중간 값 화면에 안 보임) + // Spot (5회 1.5초 간격 → trimmed mean, 오류시 재시도) var spotCooldown by remember { mutableStateOf(false) } val spotScope = rememberCoroutineScope() Button( @@ -864,26 +878,23 @@ fun PiezoMonitoringView(appState: AppState) { spotCooldown = true spotVolumes.clear() spotScope.launch { - // 최대 6회 시도, 유효값 3개 수집 목표 - val maxAttempts = 6 - val targetCount = 3 + val maxAttempts = 8 + val targetCount = 5 var attempts = 0 while (spotVolumes.size < targetCount && attempts < maxAttempts) { attempts++ measure() while (isMeasuring) { kotlinx.coroutines.delay(100) } - kotlinx.coroutines.delay(300) // 간격 여유 (BLE 겹침 방지) + kotlinx.coroutines.delay(1500) } Log.d("PiezoMonitor", "SPOT attempts=$attempts results=${spotVolumes.size}") - // 콜백에서 spotVolumes에 수집됨 (화면 갱신 없이) - if (spotVolumes.size >= 2) { - spotVolumes.sort() - val median = spotVolumes[spotVolumes.size / 2] - lastVolumeMl = median - maxVolumeMl = maxOf(maxVolumeMl, median) - Log.d("PiezoMonitor", "SPOT median=${"%.1f".format(median)}ml from ${spotVolumes.map { "%.0f".format(it) }}") + if (spotVolumes.size >= 3) { + val result = trimmedMean(spotVolumes.toList()) + lastVolumeMl = result + maxVolumeMl = maxOf(maxVolumeMl, result) + Log.d("PiezoMonitor", "SPOT trimmedMean=${"%.1f".format(result)}ml from ${spotVolumes.map { "%.0f".format(it) }}") val maxVol = appState.piezoSettings.maxVolume.toDouble() - val pct = if (maxVol > 0) (median / maxVol).coerceAtMost(1.0) else 0.0 + val pct = if (maxVol > 0) (result / maxVol).coerceAtMost(1.0) else 0.0 val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(1, BladderLevel.MAX_LEVEL) appState.bladderLevel = appState.bladderLevel.copy(currentLevel = level) } @@ -894,7 +905,7 @@ fun PiezoMonitoringView(appState: AppState) { } }, enabled = !isMeasuring && !isAutoMeasuring && !spotCooldown && !isSpotInProgress, - modifier = Modifier.weight(1f).height(56.dp), + modifier = Modifier.weight(1f).height(btnHeight), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors( containerColor = urgency.gradientColors.first() @@ -1080,6 +1091,37 @@ fun PiezoMonitoringView(appState: AppState) { HorizontalDivider() + // BV Method (Frustum / V41) + var selectedBvMethod by remember { mutableStateOf(com.example.medilightv2android.managers.GreenZoneConstants.bvMethod) } + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text("BV", fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Spacer(modifier = Modifier.weight(1f)) + val bvMethods = listOf( + com.example.medilightv2android.managers.BvMethod.FRUSTUM to "Frustum", + com.example.medilightv2android.managers.BvMethod.V41 to "V41" + ) + val bvColors = listOf(MlPrimary, Color(0xFFFF5722)) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + bvMethods.forEachIndexed { idx, (m, label) -> + val sel = selectedBvMethod == m + Button( + onClick = { selectedBvMethod = m; com.example.medilightv2android.managers.GreenZoneConstants.bvMethod = m }, + modifier = Modifier.height(32.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (sel) bvColors[idx] else Color.Gray.copy(alpha = 0.15f) + ), + contentPadding = PaddingValues(horizontal = 14.dp, vertical = 0.dp) + ) { + Text(label, fontSize = 13.sp, fontWeight = FontWeight.Bold, + color = if (sel) Color.White else MlSecondaryText) + } + } + } + } + + HorizontalDivider() + // SG Filter var sgEnabled by remember { mutableStateOf(analyzer.useSgFilter) } Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt index 54e0c50..488108d 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt @@ -1305,15 +1305,15 @@ private fun computePlacementGuide( val absOff = kotlin.math.abs(lateralOffsetMm) if (absOff <= 8.0) { return PlacementGuideResult( - "LR centred (${"%.0f".format(absOff)}mm). Final check...", + "Final check...", "checkmark.circle", 60, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN) } return if (lateralOffsetMm > 0) { - PlacementGuideResult("Move right ${"%.0f".format(absOff)}mm →", "arrow.right", 45, + PlacementGuideResult("Move right →", "arrow.right", 45, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } else { - PlacementGuideResult("Move left ${"%.0f".format(absOff)}mm ←", "arrow.left", 45, + PlacementGuideResult("Move left ←", "arrow.left", 45, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } }