fix: Auto 측정 완료대기+최소1초 간격, 10개 trimmed mean, 토스트 억제

Auto 측정:
- 완료 대기 방식: isMeasuring polling (100ms) → 다음 측정
- 최소 1000ms 간격 강제 (기기 한계 0.6초 + 여유)
- 윈도우 5→10개, 최대/최소 제외 8개 평균 (~10초마다 업데이트)
- valid≥4만 윈도우에 추가, 나머지 skip (로그만)
- Auto 중 "접촉불량" 토스트 숨김 (Spot에서만 표시)

Placement:
- 그래프 ant/post 항상 표시 (감지 여부 무관)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 18:12:49 +09:00
parent 6614e915d6
commit 297d67b4d7
2 changed files with 73 additions and 28 deletions
@@ -78,6 +78,7 @@ fun PiezoMonitoringView(appState: AppState) {
val maxMeasureRetries = 2
var lastVolumeMl by remember { mutableStateOf(0.0) }
var maxVolumeMl by remember { mutableStateOf(0.0) }
var measureDoneSignal = remember { kotlinx.coroutines.CompletableDeferred<Unit>() }
val lastMultiChannelResult = remember { mutableStateListOf<PiezoChannelData>() }
val analyzer = remember { PiezoEchoAnalyzer.shared }
@@ -90,8 +91,18 @@ fun PiezoMonitoringView(appState: AppState) {
val isConnected by bleManager.isConnected
val catheterCount by appState.catheterCount
// Auto 측정 윈도우 (10개 모아서 최대/최소 각 1개 제외 → 8개 평균)
val autoVolumeWindow = remember { mutableListOf<Double>() }
val autoWindowSize = 10
fun trimmedMean(values: List<Double>): Double {
if (values.size <= 2) return values.average()
val sorted = values.sorted()
return sorted.subList(1, sorted.size - 1).average()
}
fun measure() {
isMeasuring = true
measureDoneSignal = kotlinx.coroutines.CompletableDeferred()
// Simulation / demo mode — just increment level
if (isDemoMode) {
@@ -110,6 +121,10 @@ fun PiezoMonitoringView(appState: AppState) {
lastMultiChannelResult.clear()
lastMultiChannelResult.addAll(channels)
var rawVolumeMl: Double? = null
var rawLrRatio = 1.0
var totalWallCount = 0
val validChannels = channels.filter { it.isValid }
if (validChannels.isNotEmpty()) {
val method = com.example.medilightv2android.managers.GreenZoneConstants.detectionMethod
@@ -181,7 +196,7 @@ fun PiezoMonitoringView(appState: AppState) {
// Count valid center channels (CH0~CH3)
val centerWallCount = (0..3).count { allWalls[it] != null }
val totalWallCount = allWalls.count { it != null }
totalWallCount = allWalls.count { it != null }
// ADC 로그 저장 (매 측정마다)
val logService = com.example.medilightv2android.services.MeasurementLogService.getInstance(context)
@@ -197,8 +212,7 @@ fun PiezoMonitoringView(appState: AppState) {
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
if (method == com.example.medilightv2android.managers.DetectionMethod.METHOD_C && methodCBvMl != null) {
lastVolumeMl = methodCBvMl
maxVolumeMl = maxOf(maxVolumeMl, methodCBvMl)
rawVolumeMl = methodCBvMl
Log.d("PiezoMonitor", "BV[C]: ${"%.1f".format(methodCBvMl)}ml (V4.1, walls=${totalWallCount}/6)")
bleManager.debugLogger.measurementResult(methodCBvMl, 1.0, totalWallCount,
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 })
@@ -207,8 +221,8 @@ fun PiezoMonitoringView(appState: AppState) {
} else if (centerWallCount >= 3) {
val bvResult = estimateBladderVolume6ch(allWalls)
if (bvResult != null) {
lastVolumeMl = bvResult.volumeMl
maxVolumeMl = maxOf(maxVolumeMl, bvResult.volumeMl)
rawVolumeMl = bvResult.volumeMl
rawLrRatio = bvResult.lrRatio
Log.d("PiezoMonitor", "BV[$methodLabel]: ${"%.1f".format(bvResult.volumeMl)}ml (6ch, lr=${"%.2f".format(bvResult.lrRatio)}, center=${centerWallCount}/4)")
bleManager.debugLogger.measurementResult(bvResult.volumeMl, bvResult.lrRatio, totalWallCount,
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 },
@@ -220,21 +234,46 @@ fun PiezoMonitoringView(appState: AppState) {
Log.w("PiezoMonitor", "Insufficient center channels: $centerWallCount/4 (need 3+)")
bleManager.debugLogger.warn("BV_SKIP scan=${com.example.medilightv2android.services.AdcCsvLogger.lastScanId} center=$centerWallCount/4 total=$totalWallCount/6")
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null)
android.widget.Toast.makeText(context,
"측정 부족 (${centerWallCount}/4채널) — 센서 위치를 조절해주세요",
android.widget.Toast.LENGTH_SHORT).show()
if (!isAutoMeasuring) {
android.widget.Toast.makeText(context,
"측정 부족 (${centerWallCount}/4채널) — 센서 위치를 조절해주세요",
android.widget.Toast.LENGTH_SHORT).show()
}
}
} else {
Log.d("PiezoMonitor", "No valid channels — volume unchanged")
bleManager.debugLogger.warn("BV_FAIL scan=${com.example.medilightv2android.services.AdcCsvLogger.lastScanId} no valid channels")
val failADC = channels.sortedBy { it.channel }.map { it.buffer }
com.example.medilightv2android.services.AdcCsvLogger.log(failADC, null)
android.widget.Toast.makeText(context,
"방광이 감지되지 않았습니다. 센서 접촉을 확인해주세요.",
android.widget.Toast.LENGTH_SHORT).show()
if (!isAutoMeasuring) {
android.widget.Toast.makeText(context,
"방광이 감지되지 않았습니다. 센서 접촉을 확인해주세요.",
android.widget.Toast.LENGTH_SHORT).show()
}
}
// 볼륨 업데이트: Auto면 윈도우 trimmed mean (valid≥4만), Spot이면 즉시
if (rawVolumeMl != null && rawVolumeMl > 0) {
if (isAutoMeasuring) {
if (totalWallCount >= 4) {
autoVolumeWindow.add(rawVolumeMl)
Log.d("PiezoMonitor", "AUTO window[${autoVolumeWindow.size}/$autoWindowSize] +${"%.1f".format(rawVolumeMl)}ml (valid=$totalWallCount)")
} else {
Log.d("PiezoMonitor", "AUTO skip ${"%.1f".format(rawVolumeMl)}ml (valid=$totalWallCount < 4)")
}
if (autoVolumeWindow.size >= autoWindowSize) {
val displayVol = trimmedMean(autoVolumeWindow.toList())
lastVolumeMl = displayVol
maxVolumeMl = maxOf(maxVolumeMl, displayVol)
Log.d("PiezoMonitor", "AUTO trimmedMean=${"%.1f".format(displayVol)}ml from ${autoVolumeWindow.map { "%.0f".format(it) }}")
autoVolumeWindow.clear()
}
} else {
lastVolumeMl = rawVolumeMl
maxVolumeMl = maxOf(maxVolumeMl, rawVolumeMl)
}
}
// Update level based on measured volume
if (lastVolumeMl > 0) {
val maxVol = appState.piezoSettings.maxVolume.toDouble()
val pct = if (maxVol > 0) (lastVolumeMl / maxVol).coerceAtMost(1.0) else 0.0
@@ -243,6 +282,7 @@ fun PiezoMonitoringView(appState: AppState) {
}
isMeasuring = false
measureRetryCount = 0
try { measureDoneSignal.complete(Unit) } catch (_: Exception) {}
}
}
bleManager.piezoCollector.onLog = { msg -> Log.d("PiezoCollector", msg) }
@@ -263,11 +303,13 @@ fun PiezoMonitoringView(appState: AppState) {
Log.w("PiezoMonitor", "Retry $measureRetryCount also timed out")
isMeasuring = false
measureRetryCount = 0
try { measureDoneSignal.complete(Unit) } catch (_: Exception) {}
}, 15_000)
} else {
Log.e("PiezoMonitor", "Measurement failed after $maxMeasureRetries retries")
isMeasuring = false
measureRetryCount = 0
try { measureDoneSignal.complete(Unit) } catch (_: Exception) {}
}
}, 15_000)
}
@@ -285,29 +327,34 @@ fun PiezoMonitoringView(appState: AppState) {
fun startAutoMeasure() {
isAutoMeasuring = true
autoTickCount = 0
measure() // 즉시 첫 전체 측정
autoVolumeWindow.clear()
measure()
}
fun stopAutoMeasure() {
isAutoMeasuring = false
autoTickCount = 0
autoVolumeWindow.clear()
}
// Auto-measure: 1초 IMU × 9 + 10초째 전체측정(mbb) 반복
// Auto-measure: 측정 완료 대기 + 최소 800ms 간격 보장
// 10개 모이면 trimmed mean (최대/최소 제외 8개 평균)
LaunchedEffect(isAutoMeasuring) {
if (isAutoMeasuring) {
while (isAutoMeasuring) {
kotlinx.coroutines.delay(1000)
if (!isAutoMeasuring) break
autoTickCount++
if (autoTickCount % 10 == 0) {
// 10초째: 6ch 측정
if (!isMeasuring) measure()
} else {
// 1~9초: IMU 쿼리
if (!isMeasuring) bleManager.sendImuQuery()
val measureStartMs = System.currentTimeMillis()
if (!isMeasuring) {
measure()
while (isMeasuring && isAutoMeasuring) {
kotlinx.coroutines.delay(100)
}
autoTickCount++
// 최소 1000ms 간격 보장 (기기 측정 한계 ~0.6초 + 여유)
val elapsed = System.currentTimeMillis() - measureStartMs
val remaining = 1000L - elapsed
if (remaining > 0) kotlinx.coroutines.delay(remaining)
}
kotlinx.coroutines.delay(50)
}
}
}
@@ -370,10 +370,8 @@ fun PlacementGuideView(appState: AppState) {
Text("Echo Waveforms", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
for (chData in lastChannelData.filter { it.buffer.isNotEmpty() }) {
val detected = if (chData.channel < channelDetected.size) channelDetected[chData.channel] else false
val showWalls = if (detected) {
val chAnalysis = analyzer.analyzeChannel(chData.buffer, chData.channel)
chAnalysis.result
} else null
val chAnalysis = analyzer.analyzeChannel(chData.buffer, chData.channel)
val showWalls = chAnalysis.result
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(