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 DetectionMethod { METHOD_A, METHOD_B, METHOD_C }
|
||||||
enum class PlacementGuideMode { SIMPLE, BOUNDARY, SWEEP }
|
enum class PlacementGuideMode { SIMPLE, BOUNDARY, SWEEP }
|
||||||
|
enum class BvMethod { FRUSTUM, V41 }
|
||||||
|
|
||||||
object GreenZoneConstants {
|
object GreenZoneConstants {
|
||||||
|
|
||||||
var detectionMethod: DetectionMethod = DetectionMethod.METHOD_C
|
var detectionMethod: DetectionMethod = DetectionMethod.METHOD_C
|
||||||
|
var bvMethod: BvMethod = BvMethod.V41
|
||||||
var placementGuideMode: PlacementGuideMode = PlacementGuideMode.SIMPLE
|
var placementGuideMode: PlacementGuideMode = PlacementGuideMode.SIMPLE
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
+66
-24
@@ -239,9 +239,14 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
||||||
|
|
||||||
if (method == com.example.medilightv2android.managers.DetectionMethod.METHOD_C && methodCBvMl != null) {
|
val bvMethodSetting = com.example.medilightv2android.managers.GreenZoneConstants.bvMethod
|
||||||
rawVolumeMl = methodCBvMl
|
val useV41Bv = method == com.example.medilightv2android.managers.DetectionMethod.METHOD_C
|
||||||
Log.d("PiezoMonitor", "BV[C]: ${"%.1f".format(methodCBvMl)}ml (V4.1, walls=${totalWallCount}/6)")
|
&& 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,
|
bleManager.debugLogger.measurementResult(methodCBvMl, 1.0, totalWallCount,
|
||||||
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 })
|
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 })
|
||||||
logService.logMeasurement(methodCBvMl, channelLogs, rawADC)
|
logService.logMeasurement(methodCBvMl, channelLogs, rawADC)
|
||||||
@@ -744,7 +749,10 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
.padding(horizontal = hPad)
|
.padding(horizontal = hPad)
|
||||||
.padding(top = if (isCompact) 8.dp else 16.dp, bottom = if (isCompact) 16.dp else 32.dp),
|
.padding(top = if (isCompact) 8.dp else 16.dp, bottom = if (isCompact) 16.dp else 32.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
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
|
// Status badge
|
||||||
@@ -760,7 +768,11 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Donut chart with Bladdy
|
// 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, 부드러운 연속값)
|
// maxVolume 기준 비율 (0.0~1.0, 부드러운 연속값)
|
||||||
val maxVolumeForDonut = appState.piezoSettings.maxVolume.toDouble()
|
val maxVolumeForDonut = appState.piezoSettings.maxVolume.toDouble()
|
||||||
val donutProgress = if (maxVolumeForDonut > 0 && maxVolumeMl > 0)
|
val donutProgress = if (maxVolumeForDonut > 0 && maxVolumeMl > 0)
|
||||||
@@ -795,16 +807,17 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
color = Color(0xFFFF9800))
|
color = Color(0xFFFF9800))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val volFontSize = if (appState.isDevMode) 18.sp else 22.sp
|
||||||
Text(
|
Text(
|
||||||
if (maxVolumeMl > 0) "${"%.0f".format(maxVolumeMl)} ml" else "${appState.estimatedVolume} ml",
|
if (maxVolumeMl > 0) "${"%.0f".format(maxVolumeMl)} ml" else "${appState.estimatedVolume} ml",
|
||||||
fontSize = 18.sp,
|
fontSize = volFontSize,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = urgency.accentColor,
|
color = urgency.accentColor,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(MlCardBackground)
|
.background(MlCardBackground)
|
||||||
.shadow(4.dp, RoundedCornerShape(12.dp), spotColor = Color.Black.copy(alpha = 0.08f))
|
.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
|
// 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
|
// Void / Catheterize
|
||||||
Button(
|
Button(
|
||||||
onClick = { showCatheterizeSheet = true },
|
onClick = { showCatheterizeSheet = true },
|
||||||
modifier = Modifier.weight(1.3f).height(56.dp),
|
modifier = Modifier.weight(1.3f).height(btnHeight),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MlPrimary.copy(alpha = 0.1f))
|
colors = ButtonDefaults.buttonColors(containerColor = MlPrimary.copy(alpha = 0.1f))
|
||||||
) {
|
) {
|
||||||
@@ -841,7 +855,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (isAutoMeasuring) stopAutoMeasure() else startAutoMeasure()
|
if (isAutoMeasuring) stopAutoMeasure() else startAutoMeasure()
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f).height(56.dp),
|
modifier = Modifier.weight(1f).height(btnHeight),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = if (isAutoMeasuring) Color.Red else Color(0xFFFF9800)
|
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) }
|
var spotCooldown by remember { mutableStateOf(false) }
|
||||||
val spotScope = rememberCoroutineScope()
|
val spotScope = rememberCoroutineScope()
|
||||||
Button(
|
Button(
|
||||||
@@ -864,26 +878,23 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
spotCooldown = true
|
spotCooldown = true
|
||||||
spotVolumes.clear()
|
spotVolumes.clear()
|
||||||
spotScope.launch {
|
spotScope.launch {
|
||||||
// 최대 6회 시도, 유효값 3개 수집 목표
|
val maxAttempts = 8
|
||||||
val maxAttempts = 6
|
val targetCount = 5
|
||||||
val targetCount = 3
|
|
||||||
var attempts = 0
|
var attempts = 0
|
||||||
while (spotVolumes.size < targetCount && attempts < maxAttempts) {
|
while (spotVolumes.size < targetCount && attempts < maxAttempts) {
|
||||||
attempts++
|
attempts++
|
||||||
measure()
|
measure()
|
||||||
while (isMeasuring) { kotlinx.coroutines.delay(100) }
|
while (isMeasuring) { kotlinx.coroutines.delay(100) }
|
||||||
kotlinx.coroutines.delay(300) // 간격 여유 (BLE 겹침 방지)
|
kotlinx.coroutines.delay(1500)
|
||||||
}
|
}
|
||||||
Log.d("PiezoMonitor", "SPOT attempts=$attempts results=${spotVolumes.size}")
|
Log.d("PiezoMonitor", "SPOT attempts=$attempts results=${spotVolumes.size}")
|
||||||
// 콜백에서 spotVolumes에 수집됨 (화면 갱신 없이)
|
if (spotVolumes.size >= 3) {
|
||||||
if (spotVolumes.size >= 2) {
|
val result = trimmedMean(spotVolumes.toList())
|
||||||
spotVolumes.sort()
|
lastVolumeMl = result
|
||||||
val median = spotVolumes[spotVolumes.size / 2]
|
maxVolumeMl = maxOf(maxVolumeMl, result)
|
||||||
lastVolumeMl = median
|
Log.d("PiezoMonitor", "SPOT trimmedMean=${"%.1f".format(result)}ml from ${spotVolumes.map { "%.0f".format(it) }}")
|
||||||
maxVolumeMl = maxOf(maxVolumeMl, median)
|
|
||||||
Log.d("PiezoMonitor", "SPOT median=${"%.1f".format(median)}ml from ${spotVolumes.map { "%.0f".format(it) }}")
|
|
||||||
val maxVol = appState.piezoSettings.maxVolume.toDouble()
|
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)
|
val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(1, BladderLevel.MAX_LEVEL)
|
||||||
appState.bladderLevel = appState.bladderLevel.copy(currentLevel = level)
|
appState.bladderLevel = appState.bladderLevel.copy(currentLevel = level)
|
||||||
}
|
}
|
||||||
@@ -894,7 +905,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = !isMeasuring && !isAutoMeasuring && !spotCooldown && !isSpotInProgress,
|
enabled = !isMeasuring && !isAutoMeasuring && !spotCooldown && !isSpotInProgress,
|
||||||
modifier = Modifier.weight(1f).height(56.dp),
|
modifier = Modifier.weight(1f).height(btnHeight),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = urgency.gradientColors.first()
|
containerColor = urgency.gradientColors.first()
|
||||||
@@ -1080,6 +1091,37 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
|
|
||||||
HorizontalDivider()
|
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
|
// SG Filter
|
||||||
var sgEnabled by remember { mutableStateOf(analyzer.useSgFilter) }
|
var sgEnabled by remember { mutableStateOf(analyzer.useSgFilter) }
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
|||||||
+3
-3
@@ -1305,15 +1305,15 @@ private fun computePlacementGuide(
|
|||||||
val absOff = kotlin.math.abs(lateralOffsetMm)
|
val absOff = kotlin.math.abs(lateralOffsetMm)
|
||||||
if (absOff <= 8.0) {
|
if (absOff <= 8.0) {
|
||||||
return PlacementGuideResult(
|
return PlacementGuideResult(
|
||||||
"LR centred (${"%.0f".format(absOff)}mm). Final check...",
|
"Final check...",
|
||||||
"checkmark.circle", 60,
|
"checkmark.circle", 60,
|
||||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN)
|
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN)
|
||||||
}
|
}
|
||||||
return if (lateralOffsetMm > 0) {
|
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)
|
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||||
} else {
|
} else {
|
||||||
PlacementGuideResult("Move left ${"%.0f".format(absOff)}mm ←", "arrow.left", 45,
|
PlacementGuideResult("Move left ←", "arrow.left", 45,
|
||||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user