feat: 10건 UI 피드백 + Auto fail 팝업 + 텍스트 정리

#1 Sensor Detached 상단 문구 삭제 (urgency 메시지 항상 유지)
#2 Max Volume 5초 간격 갱신 (displayMaxVolumeMl)
#3 Fill/Level → Current Measurement/Fill
#4 detach 시 팝업→Placement 이동 (showRealignPrompt)
#5 Auto 2ch 이상 미수신 연속 5회 → 팝업 (바로 이동 아님)
#6 팝업 텍스트 "Sensor Position Issue" + 안내 문구
#7 Step2 진입 시 "Checking left/right balance..."
#8 detach 후 waitingForStart=true → Start 버튼 표시
#9 초기/재시작 안내 시 화살표 제거 (directionIcon="")
#10 Raise/Lower 텍스트 → "Raise the sensor up" 등 자연어

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 17:21:11 +09:00
parent 8f2eb15af3
commit b97d3d8052
2 changed files with 105 additions and 57 deletions
@@ -81,6 +81,8 @@ fun PiezoMonitoringView(appState: AppState) {
var lastVolumeMl by remember { mutableStateOf(0.0) } var lastVolumeMl by remember { mutableStateOf(0.0) }
var wasDetached by remember { mutableStateOf(false) } var wasDetached by remember { mutableStateOf(false) }
var maxVolumeMl by remember { mutableStateOf(0.0) } var maxVolumeMl by remember { mutableStateOf(0.0) }
var displayMaxVolumeMl by remember { mutableStateOf(0.0) } // 5초 간격 갱신용
var lastMaxUpdateMs by remember { mutableStateOf(0L) }
var isSpotInProgress by remember { mutableStateOf(false) } var isSpotInProgress by remember { mutableStateOf(false) }
val spotVolumes = remember { mutableListOf<Double>() } val spotVolumes = remember { mutableListOf<Double>() }
val measureDoneSignal = remember { mutableStateOf(kotlinx.coroutines.CompletableDeferred<Unit>()) } val measureDoneSignal = remember { mutableStateOf(kotlinx.coroutines.CompletableDeferred<Unit>()) }
@@ -330,9 +332,18 @@ fun PiezoMonitoringView(appState: AppState) {
} }
// Auto 연속 실패 → UI에서 처리 // Auto 연속 실패 → UI에서 처리
val autoFailed = isAutoMeasuring && (finalRawVol == null || finalRawVol <= 0 || !allChannelsReceived) // 채널 2개 이상 미수신 또는 BV 계산 불가
val centerWallsFinal = (0..3).count { allWalls[it] != null }
val autoFailed = isAutoMeasuring && (centerWallsFinal < 2 || finalRawVol == null || finalRawVol <= 0)
val autoSucceeded = isAutoMeasuring && finalRawVol != null && finalRawVol > 0 && allChannelsReceived val autoSucceeded = isAutoMeasuring && finalRawVol != null && finalRawVol > 0 && allChannelsReceived
// displayMaxVolumeMl 5초 간격 갱신
val nowMs = System.currentTimeMillis()
if (maxVolumeMl != displayMaxVolumeMl && nowMs - lastMaxUpdateMs >= 5000) {
displayMaxVolumeMl = maxVolumeMl
lastMaxUpdateMs = nowMs
}
if (lastVolumeMl > 0 && !isSpotInProgress) { if (lastVolumeMl > 0 && !isSpotInProgress) {
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
@@ -343,9 +354,10 @@ fun PiezoMonitoringView(appState: AppState) {
if (autoSucceeded) autoFailCount = 0 if (autoSucceeded) autoFailCount = 0
if (autoFailed) { if (autoFailed) {
autoFailCount++ autoFailCount++
if (autoFailCount >= 3) { if (autoFailCount >= 5) {
isAutoMeasuring = false isAutoMeasuring = false
autoVolumeWindow.clear() autoVolumeWindow.clear()
autoFailCount = 0
showRealignPrompt = true showRealignPrompt = true
} }
} }
@@ -843,9 +855,9 @@ fun PiezoMonitoringView(appState: AppState) {
Arrangement.spacedBy(if (isCompact) 32.dp else 40.dp) Arrangement.spacedBy(if (isCompact) 32.dp else 40.dp)
) { ) {
// Status badge (미부착 시 경고) // Status badge (항상 urgency 메시지)
val statusText = if (wasDetached) "Sensor detached" else urgency.message val statusText = urgency.message
val statusColor = if (wasDetached) Color.Red else urgency.accentColor val statusColor = urgency.accentColor
Text( Text(
text = statusText, text = statusText,
fontSize = 14.sp, fontSize = 14.sp,
@@ -901,7 +913,7 @@ fun PiezoMonitoringView(appState: AppState) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
val volFontSize = if (appState.isDevMode) 18.sp else 24.sp val volFontSize = if (appState.isDevMode) 18.sp else 24.sp
Text( Text(
if (maxVolumeMl > 0) "${"%.0f".format(maxVolumeMl)} ml" else "${appState.estimatedVolume} ml", if (displayMaxVolumeMl > 0) "${"%.0f".format(displayMaxVolumeMl)} ml" else "${appState.estimatedVolume} ml",
fontSize = volFontSize, fontSize = volFontSize,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = urgency.accentColor, color = urgency.accentColor,
@@ -915,8 +927,9 @@ fun PiezoMonitoringView(appState: AppState) {
val fillPct = if (maxVol > 0 && maxVolumeMl > 0) (maxVolumeMl / maxVol * 100).coerceAtMost(100.0) else 0.0 val fillPct = if (maxVol > 0 && maxVolumeMl > 0) (maxVolumeMl / maxVol * 100).coerceAtMost(100.0) else 0.0
val levelVal = if (maxVol > 0 && maxVolumeMl > 0) ((maxVolumeMl / maxVol) * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL) else 0 val levelVal = if (maxVol > 0 && maxVolumeMl > 0) ((maxVolumeMl / maxVol) * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL) else 0
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
val currentMeasurement = if (lastVolumeMl > 0) "%.0f".format(lastVolumeMl) else "—"
InfoCard("Measurement", currentMeasurement, "ml", urgency.accentColor, Modifier.weight(1f))
InfoCard("Fill", if (fillPct > 0) "%.0f".format(fillPct) else "—", "%", urgency.accentColor, Modifier.weight(1f)) InfoCard("Fill", if (fillPct > 0) "%.0f".format(fillPct) else "—", "%", urgency.accentColor, Modifier.weight(1f))
InfoCard("Level", if (levelVal > 0) "$levelVal/${BladderLevel.MAX_LEVEL}" else "—", "", urgency.accentColor, Modifier.weight(1f))
} }
// Control buttons: Void/Catheterize / Auto / Spot // Control buttons: Void/Catheterize / Auto / Spot
@@ -988,7 +1001,7 @@ fun PiezoMonitoringView(appState: AppState) {
attempts++ attempts++
measure() measure()
while (isMeasuring) { kotlinx.coroutines.delay(100) } while (isMeasuring) { kotlinx.coroutines.delay(100) }
kotlinx.coroutines.delay(800) kotlinx.coroutines.delay(100) // throttle(800ms)이 간격 보장
} }
Log.d("PiezoMonitor", "SPOT attempts=$attempts results=${spotVolumes.size}") Log.d("PiezoMonitor", "SPOT attempts=$attempts results=${spotVolumes.size}")
if (spotVolumes.size >= 3) { if (spotVolumes.size >= 3) {
@@ -1313,8 +1326,8 @@ fun PiezoMonitoringView(appState: AppState) {
if (showRealignPrompt) { if (showRealignPrompt) {
AlertDialog( AlertDialog(
onDismissRequest = { showRealignPrompt = false }, onDismissRequest = { showRealignPrompt = false },
title = { Text("Sensor Alignment Required") }, title = { Text("Sensor Position Issue") },
text = { Text("Measurement failed multiple times. Please re-align the sensor.") }, text = { Text("The sensor may have shifted from the optimal position. Please re-run Sensor Alignment to ensure accurate measurements.") },
confirmButton = { confirmButton = {
Button(onClick = { Button(onClick = {
showRealignPrompt = false showRealignPrompt = false
@@ -61,6 +61,7 @@ fun PlacementGuideView(appState: AppState) {
var isScanning by remember { mutableStateOf(false) } var isScanning by remember { mutableStateOf(false) }
var isContinuousMode by remember { mutableStateOf(false) } // Start 버튼으로 시작 var isContinuousMode by remember { mutableStateOf(false) } // Start 버튼으로 시작
var waitingForStart by remember { mutableStateOf(true) } // 초기 안내 상태 var waitingForStart by remember { mutableStateOf(true) } // 초기 안내 상태
var startPressedAtMs by remember { mutableStateOf(0L) }
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) }
val isSweepMode = com.example.medilightv2android.managers.GreenZoneConstants.placementGuideMode == val isSweepMode = com.example.medilightv2android.managers.GreenZoneConstants.placementGuideMode ==
@@ -75,7 +76,8 @@ fun PlacementGuideView(appState: AppState) {
var scanCount by remember { mutableIntStateOf(0) } var scanCount by remember { mutableIntStateOf(0) }
var isLocked by remember { mutableStateOf(false) } var isLocked by remember { mutableStateOf(false) }
var greenLockedAtMs by remember { mutableStateOf(0L) } // GREEN 진입 시각 var greenLockedAtMs by remember { mutableStateOf(0L) } // GREEN 진입 시각
val greenHoldMs = 4000L // GREEN 후 최소 4초 유지 val greenHoldMs = 7000L // GREEN 후 최소 7초 유지
var greenFailCount by remember { mutableIntStateOf(0) } // GREEN 이탈 디바운스
var lastChannelData by remember { mutableStateOf<List<PiezoChannelData>>(emptyList()) } var lastChannelData by remember { mutableStateOf<List<PiezoChannelData>>(emptyList()) }
var lastWallResults by remember { mutableStateOf<List<Pair<Int, Int>?>>(List(6) { null }) } var lastWallResults by remember { mutableStateOf<List<Pair<Int, Int>?>>(List(6) { null }) }
var debounceCount by remember { mutableIntStateOf(0) } var debounceCount by remember { mutableIntStateOf(0) }
@@ -354,7 +356,7 @@ fun PlacementGuideView(appState: AppState) {
modifier = Modifier modifier = Modifier
.size(width = 192.dp, height = 120.dp) .size(width = 192.dp, height = 120.dp)
.align(Alignment.Center) .align(Alignment.Center)
.offset(x = 0.dp, y = 110.dp), .offset(x = 0.dp, y = 95.dp),
contentScale = androidx.compose.ui.layout.ContentScale.Fit, contentScale = androidx.compose.ui.layout.ContentScale.Fit,
alpha = 0.95f alpha = 0.95f
) )
@@ -574,10 +576,13 @@ fun PlacementGuideView(appState: AppState) {
val detachResult = if (fullCh.size >= 6) DetachmentDetection.detect(buffers) else null val detachResult = if (fullCh.size >= 6) DetachmentDetection.detect(buffers) else null
if (detachResult != null && detachResult.isDetached) { if (detachResult != null && detachResult.isDetached) {
directionHint = if (isSweepMode) "Sensor detached. Place near navel" directionHint = "Sensor detached. Place above pubic bone, then press Start"
else "Sensor detached. Place above pubic bone" directionIcon = "" // 화살표 없음
directionIcon = "exclamationmark.triangle"
placementScore = 0 placementScore = 0
isContinuousMode = false
waitingForStart = true
placementPhase = PlacementPhase.VERTICAL
isLocked = false
if (lastLedMode != 4) { bleManager.sendLedMode(4); lastLedMode = 4 } if (lastLedMode != 4) { bleManager.sendLedMode(4); lastLedMode = 4 }
bleManager.debugLogger.warn("DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}") bleManager.debugLogger.warn("DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
Log.w("PlacementGuide", "DETACHED mean=${ Log.w("PlacementGuide", "DETACHED mean=${
@@ -621,11 +626,22 @@ fun PlacementGuideView(appState: AppState) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val elapsed = now - lastHintChangeMs val elapsed = now - lastHintChangeMs
// GREEN 진입/이탈 및 phase 전환은 즉시, 같은 phase 내 힌트만 디바운스 (1.5초) // GREEN 진입/이탈 및 phase 전환은 즉시, 같은 phase 내 힌트만 디바운스 (1.5초)
// GREEN hold 체크 (디바운스보다 먼저)
val greenHeld = isLocked && (now - greenLockedAtMs < greenHoldMs)
val wasGreen = isLocked val wasGreen = isLocked
val isNowGreen = guideResult.isPass val isNowGreen = guideResult.isPass
val greenChanged = wasGreen != isNowGreen // GREEN hold 중에는 이탈 무시
val greenChanged = if (greenHeld) {
!wasGreen && isNowGreen // 진입만 허용, 이탈은 무시
} else {
wasGreen != isNowGreen
}
val phaseChanged = guideResult.nextPhase != placementPhase val phaseChanged = guideResult.nextPhase != placementPhase
if (greenChanged || phaseChanged || (debounceCount >= 2 && elapsed >= 1500)) { // Start 후 5초간 같은 phase 내 힌트는 즉시, phase 전환은 3초 유지
val justStarted = (now - startPressedAtMs) < 5000
val hintUpdate = justStarted && !phaseChanged // 같은 step 내 힌트만 즉시
if (hintUpdate || greenChanged || (phaseChanged && !greenHeld && debounceCount >= 2 && elapsed >= 3000) || (!phaseChanged && !greenHeld && debounceCount >= 2 && elapsed >= 1500)) {
directionHint = guideResult.hint directionHint = guideResult.hint
directionIcon = guideResult.icon directionIcon = guideResult.icon
placementScore = guideResult.score placementScore = guideResult.score
@@ -634,29 +650,36 @@ fun PlacementGuideView(appState: AppState) {
if (!guideResult.cv.isNaN()) lastCv = guideResult.cv if (!guideResult.cv.isNaN()) lastCv = guideResult.cv
} }
// GREEN hold: 한번 optimal 진입 후 5초간 유지 (미세 변화 무시) // GREEN hold 중이면 힌트 강제 유지
val greenHeld = isLocked && (now - greenLockedAtMs < greenHoldMs) if (greenHeld) {
if (guideResult.isPass || greenHeld) {
if (!isLocked) greenLockedAtMs = now // 최초 진입 시각 기록
isLocked = true
if (lastLedMode != 6) { bleManager.sendLedMode(6); lastLedMode = 6 }
// GREEN hold 중이면 힌트도 optimal 유지
if (greenHeld && !guideResult.isPass) {
directionHint = "Optimal position!" directionHint = "Optimal position!"
directionIcon = "checkmark.circle" directionIcon = "checkmark.circle"
placementPhase = PlacementPhase.GREEN placementPhase = PlacementPhase.GREEN
if (!guideResult.cv.isNaN()) lastCv = guideResult.cv
} }
if (guideResult.isPass) {
if (!isLocked) greenLockedAtMs = now
isLocked = true
greenFailCount = 0 // 성공 → 실패 카운트 리셋
if (lastLedMode != 6) { bleManager.sendLedMode(6); lastLedMode = 6 }
bleManager.debugLogger.info("PLACEMENT_GREEN cv=${"%.3f".format(guideResult.cv)} lr_dev=${"%.3f".format(guideResult.lrDev)} score=${guideResult.score} otsu=${"%.0f".format(analyzer.lastOtsuThreshold)}") bleManager.debugLogger.info("PLACEMENT_GREEN cv=${"%.3f".format(guideResult.cv)} lr_dev=${"%.3f".format(guideResult.lrDev)} score=${guideResult.score} otsu=${"%.0f".format(analyzer.lastOtsuThreshold)}")
} else if (isLocked && !greenHeld) {
// hold 지남 + 실패 → 3회 연속 실패해야 이탈
greenFailCount++
if (greenFailCount < 3) {
// 아직 이탈 안 함 — GREEN 유지
directionHint = "Optimal position!"
directionIcon = "checkmark.circle"
placementPhase = PlacementPhase.GREEN
} else { } else {
val wasGreenBefore = isLocked // 3회 연속 실패 → 진짜 이탈
isLocked = false isLocked = false
greenLockedAtMs = 0L greenLockedAtMs = 0L
greenFailCount = 0
if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 } if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 }
// GREEN에서 이탈 → 스캔 중지, 재시작 대기
if (wasGreenBefore) {
directionHint = "Position lost. Place sensor above pubic bone, then press Start" directionHint = "Position lost. Place sensor above pubic bone, then press Start"
directionIcon = "arrow.up" directionIcon = "" // 화살표 없음
placementScore = 10 placementScore = 10
placementPhase = PlacementPhase.VERTICAL placementPhase = PlacementPhase.VERTICAL
lastHintChangeMs = now lastHintChangeMs = now
@@ -666,7 +689,11 @@ fun PlacementGuideView(appState: AppState) {
maxCenterSeen = 0; stableAtMax = 0; boundaryHit = false maxCenterSeen = 0; stableAtMax = 0; boundaryHit = false
sweepLastDetected = 0; sweepEverSawAll = false sweepLastDetected = 0; sweepEverSawAll = false
rTracker.reset() rTracker.reset()
bleManager.debugLogger.info("PLACEMENT_GREEN_EXIT after 3 consecutive fails")
} }
} else {
// GREEN 아닌 상태에서 실패
if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 }
bleManager.debugLogger.info("PLACEMENT_${guideResult.phase.name} score=${guideResult.score} otsu=${"%.0f".format(analyzer.lastOtsuThreshold)} hint=${guideResult.hint}") bleManager.debugLogger.info("PLACEMENT_${guideResult.phase.name} score=${guideResult.score} otsu=${"%.0f".format(analyzer.lastOtsuThreshold)} hint=${guideResult.hint}")
} }
} }
@@ -716,6 +743,23 @@ fun PlacementGuideView(appState: AppState) {
onClick = { onClick = {
waitingForStart = false waitingForStart = false
isContinuousMode = true isContinuousMode = true
// 전체 상태 리셋
debounceCount = 0
lastHintChangeMs = 0L
scanCount = 0
startPressedAtMs = System.currentTimeMillis()
placementPhase = PlacementPhase.VERTICAL
isLocked = false
greenLockedAtMs = 0L
greenFailCount = 0
placementScore = 0
maxCenterSeen = 0; stableAtMax = 0; boundaryHit = false
sweepLastDetected = 0; sweepEverSawAll = false
for (i in 0 until 6) { channelDetected[i] = false; channelUrineLen[i] = 0 }
lastWallResults = List(6) { null }
rTracker.reset()
currentRMm = null; bestRMm = 0.0
sagittalArgMaxCh = null; lateralOffsetMm = null
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -1151,7 +1195,7 @@ private fun computePlacementGuide(
val detectedCount = detected.count { it } val detectedCount = detected.count { it }
if (detectedCount == 0) { if (detectedCount == 0) {
return PlacementGuideResult("Raise the sensor ↑", "arrow.up", 0, return PlacementGuideResult("Raise the sensor up", "arrow.up", 0,
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
} }
@@ -1209,7 +1253,7 @@ private fun computePlacementGuide(
val lrHint = when { val lrHint = when {
l4 > 0 && l5 > 0 && l4 > l5 -> "Move left ←" l4 > 0 && l5 > 0 && l4 > l5 -> "Move left ←"
l4 > 0 && l5 > 0 && l5 > l4 -> "Move right →" l4 > 0 && l5 > 0 && l5 > l4 -> "Move right →"
else -> "Checking lateral..." else -> "Checking left/right balance..."
} }
val lrIcon = when { val lrIcon = when {
l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.left" l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.left"
@@ -1245,13 +1289,13 @@ private fun computePlacementGuide(
val hint: String; val icon: String val hint: String; val icon: String
when { when {
(ch0det || ch1det) && !ch2det && !ch3det -> { (ch0det || ch1det) && !ch2det && !ch3det -> {
hint = "Raise the sensor ↑"; icon = "arrow.up" hint = "Raise the sensor up"; icon = "arrow.up"
} }
(ch3det || ch2det) && !ch0det && !ch1det -> { (ch3det || ch2det) && !ch0det && !ch1det -> {
hint = "Lower the sensor ↓"; icon = "arrow.down" hint = "Lower the sensor down"; icon = "arrow.down"
} }
else -> { else -> {
hint = "Raise the sensor ↑"; icon = "arrow.up" hint = "Raise the sensor up"; icon = "arrow.up"
} }
} }
return PlacementGuideResult(hint, icon, centerDetected * 10, return PlacementGuideResult(hint, icon, centerDetected * 10,
@@ -1295,17 +1339,12 @@ private fun computePlacementGuide(
val ch4ok = detected.getOrElse(4) { false } val ch4ok = detected.getOrElse(4) { false }
val ch5ok = detected.getOrElse(5) { false } val ch5ok = detected.getOrElse(5) { false }
// 좌우도 이미 균등하면 LATERAL 건너뛰고 바로 GREEN // 항상 LATERAL 단계를 거침 (Step 1→2→3 순서 강제)
if (ch4ok && ch5ok && kotlin.math.abs(l4 - l5) <= 5) {
return PlacementGuideResult("Position OK! Final check...",
"checkmark.circle", 65,
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.GREEN)
}
val lrHint = when { val lrHint = when {
l4 > 0 && l5 > 0 && l4 > l5 -> "Move left ←" l4 > 0 && l5 > 0 && l4 > l5 -> "Move left ←"
l4 > 0 && l5 > 0 && l5 > l4 -> "Move right →" l4 > 0 && l5 > 0 && l5 > l4 -> "Move right →"
else -> "Checking lateral..." else -> "Checking left/right balance..."
} }
val lrIcon = when { val lrIcon = when {
l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.left" l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.left"
@@ -1326,21 +1365,17 @@ private fun computePlacementGuide(
val l5 = urineLens.getOrElse(5) { 0 } val l5 = urineLens.getOrElse(5) { 0 }
val ch4ok = detected.getOrElse(4) { false } val ch4ok = detected.getOrElse(4) { false }
val ch5ok = detected.getOrElse(5) { false } val ch5ok = detected.getOrElse(5) { false }
if (ch4ok && ch5ok && kotlin.math.abs(l4 - l5) <= 5) { // 항상 LATERAL 단계를 거침
return PlacementGuideResult("Position OK! Final check...", return PlacementGuideResult("Checking left/right balance...",
"checkmark.circle", 60,
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.GREEN)
}
return PlacementGuideResult("Checking lateral...",
"checkmark.circle", 50, "checkmark.circle", 50,
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL) phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL)
} else if (gradient > 0) { } else if (gradient > 0) {
val intensity = if (gradient > 0.8) "Lower ↓" else "Lower slightly ↓" val intensity = if (gradient > 0.8) "Lower the sensor down" else "Lower the sensor slightly"
return PlacementGuideResult(intensity, "arrow.down", return PlacementGuideResult(intensity, "arrow.down",
(20 + centerDetected * 5).coerceAtMost(35), (20 + centerDetected * 5).coerceAtMost(35),
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
} else { } else {
val intensity = if (gradient < -0.8) "Raise ↑" else "Raise slightly ↑" val intensity = if (gradient < -0.8) "Raise the sensor up" else "Raise the sensor slightly"
return PlacementGuideResult(intensity, "arrow.up", return PlacementGuideResult(intensity, "arrow.up",
(20 + centerDetected * 5).coerceAtMost(35), (20 + centerDetected * 5).coerceAtMost(35),
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
@@ -1507,10 +1542,10 @@ private fun computePlacementGuide(
adjustHint = "Move right →"; adjustIcon = "arrow.right" adjustHint = "Move right →"; adjustIcon = "arrow.right"
} }
gradient > 0.3 -> { gradient > 0.3 -> {
adjustHint = "Lower ↓"; adjustIcon = "arrow.down" adjustHint = "Lower the sensor down"; adjustIcon = "arrow.down"
} }
gradient < -0.3 -> { gradient < -0.3 -> {
adjustHint = "Raise ↑"; adjustIcon = "arrow.up" adjustHint = "Raise the sensor up"; adjustIcon = "arrow.up"
} }
else -> { else -> {
// 방향 애매 + CV 미달 → ch별 len 분석으로 방향 제시 // 방향 애매 + CV 미달 → ch별 len 분석으로 방향 제시
@@ -1521,10 +1556,10 @@ private fun computePlacementGuide(
// 최소 채널이 최대의 60% 미만 → 약한 쪽으로 안내 // 최소 채널이 최대의 60% 미만 → 약한 쪽으로 안내
if (minCh <= 1) { if (minCh <= 1) {
// ch0/ch1 약함 → 위로 // ch0/ch1 약함 → 위로
adjustHint = "Raise slightly ↑"; adjustIcon = "arrow.up" adjustHint = "Raise the sensor slightly"; adjustIcon = "arrow.up"
} else { } else {
// ch2/ch3 약함 → 아래로 // ch2/ch3 약함 → 아래로
adjustHint = "Lower slightly ↓"; adjustIcon = "arrow.down" adjustHint = "Lower the sensor slightly"; adjustIcon = "arrow.down"
} }
} else { } else {
adjustHint = "Fine-tune position"; adjustIcon = "checkmark.circle" adjustHint = "Fine-tune position"; adjustIcon = "checkmark.circle"