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:
+65
-18
@@ -78,6 +78,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
val maxMeasureRetries = 2
|
val maxMeasureRetries = 2
|
||||||
var lastVolumeMl by remember { mutableStateOf(0.0) }
|
var lastVolumeMl by remember { mutableStateOf(0.0) }
|
||||||
var maxVolumeMl 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 lastMultiChannelResult = remember { mutableStateListOf<PiezoChannelData>() }
|
||||||
val analyzer = remember { PiezoEchoAnalyzer.shared }
|
val analyzer = remember { PiezoEchoAnalyzer.shared }
|
||||||
|
|
||||||
@@ -90,8 +91,18 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
val isConnected by bleManager.isConnected
|
val isConnected by bleManager.isConnected
|
||||||
val catheterCount by appState.catheterCount
|
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() {
|
fun measure() {
|
||||||
isMeasuring = true
|
isMeasuring = true
|
||||||
|
measureDoneSignal = kotlinx.coroutines.CompletableDeferred()
|
||||||
|
|
||||||
// Simulation / demo mode — just increment level
|
// Simulation / demo mode — just increment level
|
||||||
if (isDemoMode) {
|
if (isDemoMode) {
|
||||||
@@ -110,6 +121,10 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
lastMultiChannelResult.clear()
|
lastMultiChannelResult.clear()
|
||||||
lastMultiChannelResult.addAll(channels)
|
lastMultiChannelResult.addAll(channels)
|
||||||
|
|
||||||
|
var rawVolumeMl: Double? = null
|
||||||
|
var rawLrRatio = 1.0
|
||||||
|
var totalWallCount = 0
|
||||||
|
|
||||||
val validChannels = channels.filter { it.isValid }
|
val validChannels = channels.filter { it.isValid }
|
||||||
if (validChannels.isNotEmpty()) {
|
if (validChannels.isNotEmpty()) {
|
||||||
val method = com.example.medilightv2android.managers.GreenZoneConstants.detectionMethod
|
val method = com.example.medilightv2android.managers.GreenZoneConstants.detectionMethod
|
||||||
@@ -181,7 +196,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
|
|
||||||
// Count valid center channels (CH0~CH3)
|
// Count valid center channels (CH0~CH3)
|
||||||
val centerWallCount = (0..3).count { allWalls[it] != null }
|
val centerWallCount = (0..3).count { allWalls[it] != null }
|
||||||
val totalWallCount = allWalls.count { it != null }
|
totalWallCount = allWalls.count { it != null }
|
||||||
|
|
||||||
// ADC 로그 저장 (매 측정마다)
|
// ADC 로그 저장 (매 측정마다)
|
||||||
val logService = com.example.medilightv2android.services.MeasurementLogService.getInstance(context)
|
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 }
|
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
||||||
|
|
||||||
if (method == com.example.medilightv2android.managers.DetectionMethod.METHOD_C && methodCBvMl != null) {
|
if (method == com.example.medilightv2android.managers.DetectionMethod.METHOD_C && methodCBvMl != null) {
|
||||||
lastVolumeMl = methodCBvMl
|
rawVolumeMl = methodCBvMl
|
||||||
maxVolumeMl = maxOf(maxVolumeMl, methodCBvMl)
|
|
||||||
Log.d("PiezoMonitor", "BV[C]: ${"%.1f".format(methodCBvMl)}ml (V4.1, walls=${totalWallCount}/6)")
|
Log.d("PiezoMonitor", "BV[C]: ${"%.1f".format(methodCBvMl)}ml (V4.1, 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 })
|
||||||
@@ -207,8 +221,8 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
} else if (centerWallCount >= 3) {
|
} else if (centerWallCount >= 3) {
|
||||||
val bvResult = estimateBladderVolume6ch(allWalls)
|
val bvResult = estimateBladderVolume6ch(allWalls)
|
||||||
if (bvResult != null) {
|
if (bvResult != null) {
|
||||||
lastVolumeMl = bvResult.volumeMl
|
rawVolumeMl = bvResult.volumeMl
|
||||||
maxVolumeMl = maxOf(maxVolumeMl, bvResult.volumeMl)
|
rawLrRatio = bvResult.lrRatio
|
||||||
Log.d("PiezoMonitor", "BV[$methodLabel]: ${"%.1f".format(bvResult.volumeMl)}ml (6ch, lr=${"%.2f".format(bvResult.lrRatio)}, center=${centerWallCount}/4)")
|
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,
|
bleManager.debugLogger.measurementResult(bvResult.volumeMl, bvResult.lrRatio, 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 },
|
||||||
@@ -220,21 +234,46 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
Log.w("PiezoMonitor", "Insufficient center channels: $centerWallCount/4 (need 3+)")
|
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")
|
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)
|
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null)
|
||||||
|
if (!isAutoMeasuring) {
|
||||||
android.widget.Toast.makeText(context,
|
android.widget.Toast.makeText(context,
|
||||||
"측정 부족 (${centerWallCount}/4채널) — 센서 위치를 조절해주세요",
|
"측정 부족 (${centerWallCount}/4채널) — 센서 위치를 조절해주세요",
|
||||||
android.widget.Toast.LENGTH_SHORT).show()
|
android.widget.Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d("PiezoMonitor", "No valid channels — volume unchanged")
|
Log.d("PiezoMonitor", "No valid channels — volume unchanged")
|
||||||
bleManager.debugLogger.warn("BV_FAIL scan=${com.example.medilightv2android.services.AdcCsvLogger.lastScanId} no valid channels")
|
bleManager.debugLogger.warn("BV_FAIL scan=${com.example.medilightv2android.services.AdcCsvLogger.lastScanId} no valid channels")
|
||||||
val failADC = channels.sortedBy { it.channel }.map { it.buffer }
|
val failADC = channels.sortedBy { it.channel }.map { it.buffer }
|
||||||
com.example.medilightv2android.services.AdcCsvLogger.log(failADC, null)
|
com.example.medilightv2android.services.AdcCsvLogger.log(failADC, null)
|
||||||
|
if (!isAutoMeasuring) {
|
||||||
android.widget.Toast.makeText(context,
|
android.widget.Toast.makeText(context,
|
||||||
"방광이 감지되지 않았습니다. 센서 접촉을 확인해주세요.",
|
"방광이 감지되지 않았습니다. 센서 접촉을 확인해주세요.",
|
||||||
android.widget.Toast.LENGTH_SHORT).show()
|
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) {
|
if (lastVolumeMl > 0) {
|
||||||
val maxVol = appState.piezoSettings.maxVolume.toDouble()
|
val maxVol = appState.piezoSettings.maxVolume.toDouble()
|
||||||
val pct = if (maxVol > 0) (lastVolumeMl / maxVol).coerceAtMost(1.0) else 0.0
|
val pct = if (maxVol > 0) (lastVolumeMl / maxVol).coerceAtMost(1.0) else 0.0
|
||||||
@@ -243,6 +282,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
isMeasuring = false
|
isMeasuring = false
|
||||||
measureRetryCount = 0
|
measureRetryCount = 0
|
||||||
|
try { measureDoneSignal.complete(Unit) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bleManager.piezoCollector.onLog = { msg -> Log.d("PiezoCollector", msg) }
|
bleManager.piezoCollector.onLog = { msg -> Log.d("PiezoCollector", msg) }
|
||||||
@@ -263,11 +303,13 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
Log.w("PiezoMonitor", "Retry $measureRetryCount also timed out")
|
Log.w("PiezoMonitor", "Retry $measureRetryCount also timed out")
|
||||||
isMeasuring = false
|
isMeasuring = false
|
||||||
measureRetryCount = 0
|
measureRetryCount = 0
|
||||||
|
try { measureDoneSignal.complete(Unit) } catch (_: Exception) {}
|
||||||
}, 15_000)
|
}, 15_000)
|
||||||
} else {
|
} else {
|
||||||
Log.e("PiezoMonitor", "Measurement failed after $maxMeasureRetries retries")
|
Log.e("PiezoMonitor", "Measurement failed after $maxMeasureRetries retries")
|
||||||
isMeasuring = false
|
isMeasuring = false
|
||||||
measureRetryCount = 0
|
measureRetryCount = 0
|
||||||
|
try { measureDoneSignal.complete(Unit) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
}, 15_000)
|
}, 15_000)
|
||||||
}
|
}
|
||||||
@@ -285,29 +327,34 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
fun startAutoMeasure() {
|
fun startAutoMeasure() {
|
||||||
isAutoMeasuring = true
|
isAutoMeasuring = true
|
||||||
autoTickCount = 0
|
autoTickCount = 0
|
||||||
measure() // 즉시 첫 전체 측정
|
autoVolumeWindow.clear()
|
||||||
|
measure()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopAutoMeasure() {
|
fun stopAutoMeasure() {
|
||||||
isAutoMeasuring = false
|
isAutoMeasuring = false
|
||||||
autoTickCount = 0
|
autoTickCount = 0
|
||||||
|
autoVolumeWindow.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-measure: 1초 IMU × 9 + 10초째 전체측정(mbb) 반복
|
// Auto-measure: 측정 완료 대기 + 최소 800ms 간격 보장
|
||||||
|
// 10개 모이면 trimmed mean (최대/최소 제외 8개 평균)
|
||||||
LaunchedEffect(isAutoMeasuring) {
|
LaunchedEffect(isAutoMeasuring) {
|
||||||
if (isAutoMeasuring) {
|
if (isAutoMeasuring) {
|
||||||
while (isAutoMeasuring) {
|
while (isAutoMeasuring) {
|
||||||
kotlinx.coroutines.delay(1000)
|
val measureStartMs = System.currentTimeMillis()
|
||||||
if (!isAutoMeasuring) break
|
if (!isMeasuring) {
|
||||||
autoTickCount++
|
measure()
|
||||||
|
while (isMeasuring && isAutoMeasuring) {
|
||||||
if (autoTickCount % 10 == 0) {
|
kotlinx.coroutines.delay(100)
|
||||||
// 10초째: 6ch 측정
|
|
||||||
if (!isMeasuring) measure()
|
|
||||||
} else {
|
|
||||||
// 1~9초: IMU 쿼리
|
|
||||||
if (!isMeasuring) bleManager.sendImuQuery()
|
|
||||||
}
|
}
|
||||||
|
autoTickCount++
|
||||||
|
// 최소 1000ms 간격 보장 (기기 측정 한계 ~0.6초 + 여유)
|
||||||
|
val elapsed = System.currentTimeMillis() - measureStartMs
|
||||||
|
val remaining = 1000L - elapsed
|
||||||
|
if (remaining > 0) kotlinx.coroutines.delay(remaining)
|
||||||
|
}
|
||||||
|
kotlinx.coroutines.delay(50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -370,10 +370,8 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
Text("Echo Waveforms", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
Text("Echo Waveforms", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||||
for (chData in lastChannelData.filter { it.buffer.isNotEmpty() }) {
|
for (chData in lastChannelData.filter { it.buffer.isNotEmpty() }) {
|
||||||
val detected = if (chData.channel < channelDetected.size) channelDetected[chData.channel] else false
|
val detected = if (chData.channel < channelDetected.size) channelDetected[chData.channel] else false
|
||||||
val showWalls = if (detected) {
|
|
||||||
val chAnalysis = analyzer.analyzeChannel(chData.buffer, chData.channel)
|
val chAnalysis = analyzer.analyzeChannel(chData.buffer, chData.channel)
|
||||||
chAnalysis.result
|
val showWalls = chAnalysis.result
|
||||||
} else null
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
Reference in New Issue
Block a user