feat: Placement Guide 실시간 연속 스캔 (3초 간격)

- 수동 1회 → 자동 반복 스캔 (Start/Stop 토글)
- 3초마다 maa 전송 → Score 실시간 갱신
- 사용자가 센서 움직이면서 점수 변화를 즉시 확인 가능
- "Scanning every 3s — move sensor and watch the score" 안내 문구

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 11:29:02 +09:00
parent c114592ab1
commit 079774c869
@@ -57,6 +57,7 @@ fun PlacementGuideView(appState: AppState) {
val analyzer = remember { PiezoEchoAnalyzer.shared } val analyzer = remember { PiezoEchoAnalyzer.shared }
var isScanning by remember { mutableStateOf(false) } var isScanning by remember { mutableStateOf(false) }
var isContinuousMode by remember { mutableStateOf(false) }
val channelDetected = remember { mutableStateListOf(false, false, false, false, false, false) } val channelDetected = remember { mutableStateListOf(false, false, false, false, false, false) }
val channelUrineLen = remember { mutableStateListOf(0, 0, 0, 0, 0, 0) } val channelUrineLen = remember { mutableStateListOf(0, 0, 0, 0, 0, 0) }
var directionHint by remember { mutableStateOf("") } var directionHint by remember { mutableStateOf("") }
@@ -280,10 +281,9 @@ fun PlacementGuideView(appState: AppState) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Check Position button // Scan function (reusable)
Button( fun performScan() {
onClick = { if (isScanning) return
if (!isScanning) {
isScanning = true isScanning = true
bleManager.piezoCollector.onMultiChannelComplete = { channels -> bleManager.piezoCollector.onMultiChannelComplete = { channels ->
isScanning = false isScanning = false
@@ -314,41 +314,52 @@ fun PlacementGuideView(appState: AppState) {
} }
lastChannelData = channels lastChannelData = channels
// Direction guidance (urineLen-aware)
val guideResult = computePlacementGuide(detected, urineLens) val guideResult = computePlacementGuide(detected, urineLens)
directionHint = guideResult.hint directionHint = guideResult.hint
directionIcon = guideResult.icon directionIcon = guideResult.icon
placementScore = guideResult.score placementScore = guideResult.score
// Lock if score >= 70 or 4+ detected
if (guideResult.score >= 70 || detected.count { it } >= 4) { if (guideResult.score >= 70 || detected.count { it } >= 4) {
isLocked = true isLocked = true
} }
Log.d("PlacementGuide", "Placement: ${detected.count { it }}/6 -- ${ Log.d("PlacementGuide", "Scan #$scanCount: ${detected.count { it }}/6, score=$placementScore%")
detected.mapIndexed { idx, d -> if (d) "CH$idx OK" else "CH$idx X" }.joinToString(" ")
}")
// Save placement log
logService.logPlacement( logService.logPlacement(
channelDetected = detected, channelDetected = detected,
direction = directionHint direction = directionHint
) )
} }
bleManager.piezoCollector.onLog = { msg -> bleManager.piezoCollector.onLog = { msg -> Log.d("PlacementGuide", msg) }
Log.d("PlacementGuide", msg)
}
bleManager.sendAllChannels() bleManager.sendAllChannels()
} }
// Continuous scan loop (3초 간격)
LaunchedEffect(isContinuousMode) {
if (isContinuousMode) {
performScan()
while (isContinuousMode) {
kotlinx.coroutines.delay(3000)
if (isContinuousMode && !isScanning) performScan()
}
}
}
// Start/Stop button
Button(
onClick = {
if (isContinuousMode) {
isContinuousMode = false
} else {
isContinuousMode = true
}
}, },
enabled = !isScanning,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(52.dp), .height(52.dp),
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = if (isScanning) Color.Gray else MlPrimary, containerColor = if (isContinuousMode) Color.Red else MlPrimary
disabledContainerColor = Color.Gray
) )
) { ) {
if (isScanning) { if (isScanning) {
@@ -360,13 +371,26 @@ fun PlacementGuideView(appState: AppState) {
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
Text( Text(
text = if (isScanning) "Scanning..." else "Check Position", text = when {
isContinuousMode -> "Stop Scanning"
scanCount == 0 -> "Start Scanning"
else -> "Resume Scanning"
},
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White color = Color.White
) )
} }
if (isContinuousMode) {
Text(
"Scanning every 3s — move sensor and watch the score",
fontSize = 11.sp, color = MlSecondaryText,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
// Start Measurement button (when 4+ channels detected) // Start Measurement button (when 4+ channels detected)
if (isLocked || detectedCount >= 4) { if (isLocked || detectedCount >= 4) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))