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:
+71
-24
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user