feat: full Piezo measurement pipeline + waveform charts in PiezoMonitoringView
Major update matching iOS PiezoMonitoringView:
measure() — real BLE measurement via maa:
- sendAllChannels → piezoCollector → analyzeMultiChannel
- Build walls → estimateBladderVolume (Frustum)
- Auto-retry on timeout (max 2, 15s each)
- Volume-based bladder level update
UI additions:
- Max Volume info card (orange)
- Volume card shows measured value
- Developer Tools (collapsible):
- 6-Channel Analysis with Measure button
- Zone indicator (GOOD/PARTIAL/ADJUST)
- Channel status bar (green/red, urine length)
- Per-channel waveform charts (Canvas):
- Raw (light blue) + Denoised (blue) overlay
- Threshold line (1600, orange dashed)
- Low-echo span (yellow)
- Urine region (cyan) with ant(green)/post(red) markers
- Y-axis 0-2500 fixed, X-axis depth in mm
resetMeasurement() on catheterize
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+425
-2
@@ -2,8 +2,10 @@ package com.example.medilightv2android.ui.views.monitoring
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.example.medilightv2android.AppScreen
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -20,8 +22,13 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -29,6 +36,10 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.medilightv2android.AppState
|
||||
import com.example.medilightv2android.ble.BleManager
|
||||
import com.example.medilightv2android.ble.PiezoChannelData
|
||||
import com.example.medilightv2android.managers.PiezoEchoAnalyzer
|
||||
import com.example.medilightv2android.managers.PiezoConstants
|
||||
import com.example.medilightv2android.managers.estimateBladderVolume
|
||||
import com.example.medilightv2android.models.BladderLevel
|
||||
import com.example.medilightv2android.models.UrgencyLevel
|
||||
import com.example.medilightv2android.ui.components.BladdyRiveView
|
||||
@@ -56,6 +67,14 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
var showDisconnectionBanner by remember { mutableStateOf(false) }
|
||||
var reconnectionState by remember { mutableStateOf(BleReconnectionState.DISCONNECTED) }
|
||||
var isAutoMeasuring by remember { mutableStateOf(false) }
|
||||
var showDevPanels by remember { mutableStateOf(false) }
|
||||
var isPlacementTesting by remember { mutableStateOf(false) }
|
||||
var measureRetryCount by remember { mutableIntStateOf(0) }
|
||||
val maxMeasureRetries = 2
|
||||
var lastVolumeMl by remember { mutableStateOf(0.0) }
|
||||
var maxVolumeMl by remember { mutableStateOf(0.0) }
|
||||
val lastMultiChannelResult = remember { mutableStateListOf<PiezoChannelData>() }
|
||||
val analyzer = remember { PiezoEchoAnalyzer.shared }
|
||||
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp
|
||||
val isCompact = screenWidth < 400
|
||||
@@ -68,10 +87,100 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
|
||||
fun measure() {
|
||||
isMeasuring = true
|
||||
|
||||
// Simulation / demo mode — just increment level
|
||||
if (isDemoMode) {
|
||||
handler.postDelayed({
|
||||
appState.incrementLevel()
|
||||
isMeasuring = false
|
||||
}, 1500)
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
|
||||
// Real BLE measurement — 6-channel
|
||||
Log.d("PiezoMonitor", "Sending maa (all channels)...")
|
||||
|
||||
bleManager.piezoCollector.onMultiChannelComplete = { channels ->
|
||||
handler.post {
|
||||
lastMultiChannelResult.clear()
|
||||
lastMultiChannelResult.addAll(channels)
|
||||
|
||||
val validChannels = channels.filter { it.isValid }
|
||||
if (validChannels.isNotEmpty()) {
|
||||
val analysisResult = analyzer.analyzeMultiChannel(validChannels)
|
||||
|
||||
// Build walls list for BV estimation (5 channels)
|
||||
val walls: MutableList<Pair<Int, Int>?> = MutableList(5) { null }
|
||||
for (ch in analysisResult.channels) {
|
||||
if (ch.channel < 5 && ch.result != null) {
|
||||
walls[ch.channel] = Pair(ch.result.ant, ch.result.post)
|
||||
}
|
||||
}
|
||||
|
||||
// Frustum BV estimation
|
||||
val bvResult = estimateBladderVolume(walls = walls)
|
||||
if (bvResult != null) {
|
||||
lastVolumeMl = bvResult.volumeMl
|
||||
maxVolumeMl = maxOf(maxVolumeMl, bvResult.volumeMl)
|
||||
Log.d("PiezoMonitor", "BV: ${"%.1f".format(bvResult.volumeMl)}ml (frustum, ${bvResult.validChannels.size}ch)")
|
||||
} else {
|
||||
Log.d("PiezoMonitor", "BV: estimation failed (< 2 valid channels)")
|
||||
}
|
||||
|
||||
// Log per-channel detail
|
||||
for (ch in analysisResult.channels) {
|
||||
if (ch.isValid && ch.result != null) {
|
||||
Log.d("PiezoMonitor", " CH${ch.channel}: ant=${ch.result.ant} post=${ch.result.post} urine=${ch.result.urineLen} score=${"%.1f".format(ch.result.score)}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d("PiezoMonitor", "No valid channels — volume unchanged")
|
||||
}
|
||||
|
||||
// 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
|
||||
val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(1, BladderLevel.MAX_LEVEL)
|
||||
appState.bladderLevel.currentLevel = level
|
||||
}
|
||||
isMeasuring = false
|
||||
measureRetryCount = 0
|
||||
}
|
||||
}
|
||||
bleManager.piezoCollector.onLog = { msg -> Log.d("PiezoCollector", msg) }
|
||||
|
||||
bleManager.sendAllChannels()
|
||||
|
||||
// Timeout with auto-retry (15s)
|
||||
handler.postDelayed({
|
||||
if (!isMeasuring) return@postDelayed
|
||||
if (measureRetryCount < maxMeasureRetries) {
|
||||
measureRetryCount++
|
||||
Log.w("PiezoMonitor", "Timeout — auto-retry $measureRetryCount/$maxMeasureRetries")
|
||||
bleManager.piezoCollector.reset()
|
||||
bleManager.sendAllChannels()
|
||||
// Second timeout for retry
|
||||
handler.postDelayed({
|
||||
if (!isMeasuring) return@postDelayed
|
||||
Log.w("PiezoMonitor", "Retry $measureRetryCount also timed out")
|
||||
isMeasuring = false
|
||||
measureRetryCount = 0
|
||||
}, 15_000)
|
||||
} else {
|
||||
Log.e("PiezoMonitor", "Measurement failed after $maxMeasureRetries retries")
|
||||
isMeasuring = false
|
||||
measureRetryCount = 0
|
||||
}
|
||||
}, 15_000)
|
||||
}
|
||||
|
||||
fun resetMeasurement() {
|
||||
lastVolumeMl = 0.0
|
||||
maxVolumeMl = 0.0
|
||||
lastMultiChannelResult.clear()
|
||||
measureRetryCount = 0
|
||||
isMeasuring = false
|
||||
}
|
||||
|
||||
fun stopAutoMeasure() {
|
||||
@@ -128,6 +237,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
appState.resetLevel()
|
||||
resetMeasurement()
|
||||
showCatheterizeConfirm = false
|
||||
}) { Text("Catheterize", color = Color.Red) }
|
||||
},
|
||||
@@ -429,7 +539,9 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
|
||||
// Info cards
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
InfoCard("Volume", "${appState.estimatedVolume}", "ml", urgency.accentColor, Modifier.weight(1f))
|
||||
val displayVol = if (lastVolumeMl > 0) "%.0f".format(lastVolumeMl) else "${appState.estimatedVolume}"
|
||||
InfoCard("Volume", displayVol, "ml", urgency.accentColor, Modifier.weight(1f))
|
||||
InfoCard("Max Vol", if (maxVolumeMl > 0) "%.0f".format(maxVolumeMl) else "—", "ml", Color(0xFFFF9500), Modifier.weight(1f))
|
||||
InfoCard("Fill", String.format("%.0f", appState.fillPercentage), "%", urgency.accentColor, Modifier.weight(1f))
|
||||
InfoCard("Level", "${appState.bladderLevel.currentLevel}/${BladderLevel.MAX_LEVEL}", "", urgency.accentColor, Modifier.weight(1f))
|
||||
}
|
||||
@@ -493,6 +605,317 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
fontSize = 11.sp,
|
||||
color = if (isAutoMeasuring) Color.Red.copy(alpha = 0.7f) else MlSecondaryText
|
||||
)
|
||||
|
||||
// ── Developer Tools toggle ──
|
||||
Button(
|
||||
onClick = { showDevPanels = !showDevPanels },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color.Gray.copy(alpha = 0.7f))
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Build, "Dev", tint = Color.White, modifier = Modifier.size(14.dp))
|
||||
Text("Developer Tools", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
if (showDevPanels) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||
"Toggle", tint = Color.White, modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDevPanels) {
|
||||
ChannelPanel(
|
||||
lastMultiChannelResult = lastMultiChannelResult,
|
||||
analyzer = analyzer,
|
||||
isPlacementTesting = isPlacementTesting,
|
||||
onMeasure = {
|
||||
if (!isPlacementTesting && !isMeasuring) {
|
||||
isPlacementTesting = true
|
||||
measure()
|
||||
// Reset placement flag when measurement completes
|
||||
handler.postDelayed({ isPlacementTesting = false }, 20_000)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 6-Channel Analysis Panel ──
|
||||
|
||||
@Composable
|
||||
private fun ChannelPanel(
|
||||
lastMultiChannelResult: List<PiezoChannelData>,
|
||||
analyzer: PiezoEchoAnalyzer,
|
||||
isPlacementTesting: Boolean,
|
||||
onMeasure: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MlCardBackground),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(4.dp, RoundedCornerShape(12.dp), spotColor = Color.Black.copy(alpha = 0.04f))
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
// Header
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("6-Channel Analysis", fontSize = 13.sp, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Button(
|
||||
onClick = onMeasure,
|
||||
enabled = !isPlacementTesting,
|
||||
modifier = Modifier.height(28.dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isPlacementTesting) Color.Gray else MlPrimary
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp)
|
||||
) {
|
||||
Text(
|
||||
if (isPlacementTesting) "Measuring..." else "Measure",
|
||||
fontSize = 10.sp, fontWeight = FontWeight.SemiBold, color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (lastMultiChannelResult.isEmpty()) {
|
||||
// Empty state
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().height(80.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(Icons.Default.GraphicEq, "Sensor", tint = Color.Gray.copy(alpha = 0.3f), modifier = Modifier.size(32.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text("Tap Measure or Spot to capture 6-channel data", fontSize = 11.sp, color = Color.Gray)
|
||||
}
|
||||
} else {
|
||||
// Zone indicator
|
||||
val analyses = lastMultiChannelResult.map { analyzer.analyzeChannel(it.buffer, it.channel) }
|
||||
val detectedCount = analyses.count { it.isValid }
|
||||
val zoneColor = when {
|
||||
detectedCount >= 4 -> Color.Green
|
||||
detectedCount >= 2 -> Color.Yellow
|
||||
else -> Color.Red
|
||||
}
|
||||
val zoneText = when {
|
||||
detectedCount >= 4 -> "GOOD ($detectedCount/6)"
|
||||
detectedCount >= 2 -> "PARTIAL ($detectedCount/6)"
|
||||
else -> "ADJUST POSITION"
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(zoneColor.copy(alpha = 0.1f))
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Box(modifier = Modifier.size(14.dp).clip(CircleShape).background(zoneColor))
|
||||
Text(zoneText, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = zoneColor)
|
||||
}
|
||||
|
||||
// Channel status bar (6 boxes)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
for (ch in 0 until 6) {
|
||||
val chAnalysis = analyses.firstOrNull { it.channel == ch }
|
||||
val isValid = chAnalysis?.isValid == true
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(24.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(if (isValid) Color.Green else Color.Red.copy(alpha = 0.4f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val label = chAnalysis?.result?.let { "u=${it.urineLen}" } ?: "—"
|
||||
Text(label, fontSize = 7.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
||||
}
|
||||
Text("CH$ch", fontSize = 8.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-channel waveforms
|
||||
for (chData in lastMultiChannelResult.filter { it.buffer.isNotEmpty() }) {
|
||||
val chAnalysis = analyzer.analyzeChannel(chData.buffer, chData.channel)
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
// Channel header
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (chAnalysis.isValid) Color.Green else Color.Gray.copy(alpha = 0.4f))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("CH${chData.channel}", fontSize = 12.sp, fontWeight = FontWeight.Bold)
|
||||
|
||||
if (chAnalysis.result != null) {
|
||||
val r = chAnalysis.result
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
"urine idx ${r.ant}–${r.post}",
|
||||
fontSize = 10.sp, fontWeight = FontWeight.SemiBold, color = Color.Cyan
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
"(${"%.0f".format(r.antDepthMm)}–${"%.0f".format(r.postDepthMm)}mm)",
|
||||
fontSize = 9.sp, color = MlSecondaryText
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
"len=${r.urineLen}",
|
||||
fontSize = 9.sp, fontWeight = FontWeight.SemiBold, color = Color.Cyan
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("no urine", fontSize = 9.sp, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
|
||||
// Waveform chart
|
||||
WaveformChart(
|
||||
buffer = chData.buffer,
|
||||
peakIndex = chData.peakIndex.toInt(),
|
||||
analyzer = analyzer,
|
||||
antIndex = chAnalysis.result?.ant,
|
||||
postIndex = chAnalysis.result?.post,
|
||||
lowStart = chAnalysis.result?.lowStart,
|
||||
lowEnd = chAnalysis.result?.lowEnd,
|
||||
modifier = Modifier.fillMaxWidth().height(120.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (chData.channel < 5) {
|
||||
HorizontalDivider(color = Color.Gray.copy(alpha = 0.3f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Waveform Chart (Canvas-based) ──
|
||||
|
||||
@Composable
|
||||
private fun WaveformChart(
|
||||
buffer: List<UShort>,
|
||||
peakIndex: Int,
|
||||
analyzer: PiezoEchoAnalyzer,
|
||||
antIndex: Int?,
|
||||
postIndex: Int?,
|
||||
lowStart: Int?,
|
||||
lowEnd: Int?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val count = buffer.size
|
||||
if (count < 2) return
|
||||
|
||||
val raw = buffer.map { it.toDouble() }.toDoubleArray()
|
||||
val denoised = analyzer.denoise(raw)
|
||||
val maxVal = 2500f
|
||||
val threshold = analyzer.lowEchoAmpDefault.toFloat()
|
||||
|
||||
Canvas(modifier = modifier) {
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
val stepX = w / (count - 1).toFloat()
|
||||
|
||||
// Background grid (4 horizontal lines)
|
||||
for (i in 0..4) {
|
||||
val y = h * i / 4f
|
||||
drawLine(
|
||||
color = Color.Gray.copy(alpha = 0.15f),
|
||||
start = Offset(0f, y), end = Offset(w, y),
|
||||
strokeWidth = 0.5f
|
||||
)
|
||||
}
|
||||
|
||||
// Threshold line (orange dashed at 1600)
|
||||
val threshY = h - (threshold / maxVal * h)
|
||||
drawLine(
|
||||
color = Color(0xFFFF9500),
|
||||
start = Offset(0f, threshY), end = Offset(w, threshY),
|
||||
strokeWidth = 1f,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(8f, 6f))
|
||||
)
|
||||
|
||||
// Low-echo span highlight (yellow)
|
||||
if (lowStart != null && lowEnd != null && lowEnd > lowStart) {
|
||||
val lsX = lowStart * stepX
|
||||
val leX = lowEnd * stepX
|
||||
drawRect(
|
||||
color = Color.Yellow.copy(alpha = 0.15f),
|
||||
topLeft = Offset(lsX, 0f),
|
||||
size = Size((leX - lsX).coerceAtLeast(1f), h)
|
||||
)
|
||||
}
|
||||
|
||||
// Urine region shading (cyan)
|
||||
if (antIndex != null && postIndex != null && antIndex >= 0 && postIndex > antIndex && postIndex < count) {
|
||||
val antX = antIndex * stepX
|
||||
val postX = postIndex * stepX
|
||||
drawRect(
|
||||
color = Color.Cyan.copy(alpha = 0.15f),
|
||||
topLeft = Offset(antX, 0f),
|
||||
size = Size((postX - antX).coerceAtLeast(1f), h)
|
||||
)
|
||||
}
|
||||
|
||||
// Raw waveform (light blue)
|
||||
val rawPath = Path()
|
||||
for (i in raw.indices) {
|
||||
val x = i * stepX
|
||||
val y = h - (raw[i].toFloat() / maxVal * h)
|
||||
if (i == 0) rawPath.moveTo(x, y) else rawPath.lineTo(x, y)
|
||||
}
|
||||
drawPath(rawPath, color = Color.Blue.copy(alpha = 0.3f), style = Stroke(width = 1.5f))
|
||||
|
||||
// Denoised waveform (solid blue)
|
||||
val denoisedPath = Path()
|
||||
for (i in denoised.indices) {
|
||||
val x = i * stepX
|
||||
val y = h - (denoised[i].toFloat() / maxVal * h)
|
||||
if (i == 0) denoisedPath.moveTo(x, y) else denoisedPath.lineTo(x, y)
|
||||
}
|
||||
drawPath(denoisedPath, color = Color.Blue, style = Stroke(width = 2.5f))
|
||||
|
||||
// Anterior wall marker (green line)
|
||||
if (antIndex != null && antIndex in 0 until count) {
|
||||
val antX = antIndex * stepX
|
||||
drawLine(color = Color.Green, start = Offset(antX, 0f), end = Offset(antX, h), strokeWidth = 3f)
|
||||
val antY = h - (denoised.getOrElse(antIndex) { 0.0 }.toFloat() / maxVal * h)
|
||||
drawCircle(color = Color.Green, radius = 5f, center = Offset(antX, antY))
|
||||
}
|
||||
|
||||
// Posterior wall marker (red line)
|
||||
if (postIndex != null && postIndex in 0 until count) {
|
||||
val postX = postIndex * stepX
|
||||
drawLine(color = Color.Red, start = Offset(postX, 0f), end = Offset(postX, h), strokeWidth = 3f)
|
||||
val postY = h - (denoised.getOrElse(postIndex) { 0.0 }.toFloat() / maxVal * h)
|
||||
drawCircle(color = Color.Red, radius = 5f, center = Offset(postX, postY))
|
||||
} else if (peakIndex in 0 until count) {
|
||||
// Fallback: show peak dot if no walls
|
||||
val px = peakIndex * stepX
|
||||
val py = h - (raw[peakIndex].toFloat() / maxVal * h)
|
||||
drawCircle(color = Color.Red, radius = 4f, center = Offset(px, py))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user