feat: Detection/BV 분리 설정 + Spot 5회 trimmedMean + UI 여유
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
+66
-24
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user