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:
+23
-10
@@ -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
|
||||||
|
|||||||
+82
-47
@@ -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) {
|
||||||
|
directionHint = "Optimal position!"
|
||||||
|
directionIcon = "checkmark.circle"
|
||||||
|
placementPhase = PlacementPhase.GREEN
|
||||||
|
if (!guideResult.cv.isNaN()) lastCv = guideResult.cv
|
||||||
|
}
|
||||||
|
|
||||||
if (guideResult.isPass || greenHeld) {
|
if (guideResult.isPass) {
|
||||||
if (!isLocked) greenLockedAtMs = now // 최초 진입 시각 기록
|
if (!isLocked) greenLockedAtMs = now
|
||||||
isLocked = true
|
isLocked = true
|
||||||
|
greenFailCount = 0 // 성공 → 실패 카운트 리셋
|
||||||
if (lastLedMode != 6) { bleManager.sendLedMode(6); lastLedMode = 6 }
|
if (lastLedMode != 6) { bleManager.sendLedMode(6); lastLedMode = 6 }
|
||||||
// GREEN hold 중이면 힌트도 optimal 유지
|
bleManager.debugLogger.info("PLACEMENT_GREEN cv=${"%.3f".format(guideResult.cv)} lr_dev=${"%.3f".format(guideResult.lrDev)} score=${guideResult.score} otsu=${"%.0f".format(analyzer.lastOtsuThreshold)}")
|
||||||
if (greenHeld && !guideResult.isPass) {
|
} else if (isLocked && !greenHeld) {
|
||||||
|
// hold 지남 + 실패 → 3회 연속 실패해야 이탈
|
||||||
|
greenFailCount++
|
||||||
|
if (greenFailCount < 3) {
|
||||||
|
// 아직 이탈 안 함 — GREEN 유지
|
||||||
directionHint = "Optimal position!"
|
directionHint = "Optimal position!"
|
||||||
directionIcon = "checkmark.circle"
|
directionIcon = "checkmark.circle"
|
||||||
placementPhase = PlacementPhase.GREEN
|
placementPhase = PlacementPhase.GREEN
|
||||||
}
|
} else {
|
||||||
bleManager.debugLogger.info("PLACEMENT_GREEN cv=${"%.3f".format(guideResult.cv)} lr_dev=${"%.3f".format(guideResult.lrDev)} score=${guideResult.score} otsu=${"%.0f".format(analyzer.lastOtsuThreshold)}")
|
// 3회 연속 실패 → 진짜 이탈
|
||||||
} else {
|
isLocked = false
|
||||||
val wasGreenBefore = isLocked
|
greenLockedAtMs = 0L
|
||||||
isLocked = false
|
greenFailCount = 0
|
||||||
greenLockedAtMs = 0L
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user