From 297d67b4d72e6ee63ecb1c9414418c30bb28d395 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Wed, 29 Apr 2026 18:12:49 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Auto=20=EC=B8=A1=EC=A0=95=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=EB=8C=80=EA=B8=B0+=EC=B5=9C=EC=86=8C1=EC=B4=88=20?= =?UTF-8?q?=EA=B0=84=EA=B2=A9,=2010=EA=B0=9C=20trimmed=20mean,=20=ED=86=A0?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=96=B5=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../views/monitoring/PiezoMonitoringView.kt | 95 ++++++++++++++----- .../ui/views/monitoring/PlacementGuideView.kt | 6 +- 2 files changed, 73 insertions(+), 28 deletions(-) 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 45412b9..5ed5efb 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 @@ -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() } val lastMultiChannelResult = remember { mutableStateListOf() } 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() } + val autoWindowSize = 10 + fun trimmedMean(values: List): 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) } } } 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 1da8602..db905e9 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 @@ -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(