feat: UI 대량 개선 + Placement 안정성 + BV 수정
[Placement] - Start 버튼 필수 (자동 스캔 안 함) - 화면 진입 시 전체 상태 리셋 (Step 2 직행 방지) - Step 1 완료 조건: ch0~ch3 4개 전부 감지 필수 - Step 2 전환 시 "Checking left/right balance..." 3스캔 보호 - Step 전환 힌트 강제 유지 (phaseHoldHint) - GREEN hold 7초 + 3회 연속 실패 후 이탈 - GREEN 이탈 시 스캔 중지 + Start 재시작 대기 - Detach 시 스캔 중지 + Start 버튼 표시 - 힌트 점 순환 (Raise.. / Raise... 업데이트 표시) - 텍스트: "Raise the sensor up" 등 자연어 - 초기/재시작 시 화살표 제거 - Step 2 스킵 제거 (항상 3단계 순서) - GREEN에서 ch4/ch5 개별 탈락 시 방향 안내 - gradient dead zone에 4채널 필수 [도넛차트] - Max Volume 5초 간격 갱신 - Current Measurement / Fill 카드 (Fill = 65% + 325/500 mL) - Auto 윈도우 < 5일 때 Current Measurement "—" - Spot 완료 시 displayMaxVolumeMl 즉시 갱신 - ml → mL 통일 - Sensor Detached 상단 문구 제거 - 카테터 기본값 15개 [기능] - Auto 2ch 미수신 5회 연속 → Realign 팝업 - Spot 간격 800ms→100ms (throttle이 보장) - Auto Stop 버튼 깜빡임 해결 - Plot fallback 제거 (allWalls null이면 피크 안 보임) - Pairing 미완료 시 다음 화면 차단 - 설정 패널 Save/Cancel 버튼 - 뒤로가기 앱 종료 확인 [코드 품질] - Thread → coroutine (measureScope + Dispatchers.Default) - mutableStateOf 메인 스레드 보장 - measureDoneSignal var→val mutableStateOf - @Volatile (BleManager, GreenZoneConstants, PiezoHW) - maa throttle deadlock 방지 (Boolean 반환) - channelLogs allWalls 기준 통일 README v4 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 작성일: 2026-04-23
|
## 작성일: 2026-04-23
|
||||||
## 작성자: dwjang
|
## 작성자: dwjang
|
||||||
## 마지막 업데이트: 2026-05-07 (v3)
|
## 마지막 업데이트: 2026-05-09 (v4) — VBTAND0101
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class UserStorage private constructor(context: Context) {
|
|||||||
set(value) { prefs.edit().putInt("bladder_level", value).apply() }
|
set(value) { prefs.edit().putInt("bladder_level", value).apply() }
|
||||||
|
|
||||||
var catheterCount: Int
|
var catheterCount: Int
|
||||||
get() = prefs.getInt("catheter_count", 0)
|
get() = prefs.getInt("catheter_count", 15)
|
||||||
set(value) { prefs.edit().putInt("catheter_count", value).apply() }
|
set(value) { prefs.edit().putInt("catheter_count", value).apply() }
|
||||||
|
|
||||||
var hasCompletedOnboarding: Boolean
|
var hasCompletedOnboarding: Boolean
|
||||||
|
|||||||
+14
-8
@@ -529,7 +529,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
Text(stringResource(R.string.voiding_record), fontWeight = FontWeight.Bold, fontSize = 18.sp)
|
Text(stringResource(R.string.voiding_record), fontWeight = FontWeight.Bold, fontSize = 18.sp)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
"${appState.estimatedVolume} ml",
|
"${appState.estimatedVolume} mL",
|
||||||
fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MlPrimary
|
fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MlPrimary
|
||||||
)
|
)
|
||||||
Text("추정 용량", fontSize = 12.sp, color = MlSecondaryText)
|
Text("추정 용량", fontSize = 12.sp, color = MlSecondaryText)
|
||||||
@@ -611,7 +611,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
Icon(Icons.Default.FlashOn, contentDescription = null, tint = Color(0xFF4CAF50))
|
Icon(Icons.Default.FlashOn, contentDescription = null, tint = Color(0xFF4CAF50))
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text("Quick Save", fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
Text("Quick Save", fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||||
Text("$quickVol ml", color = MlSecondaryText, fontSize = 14.sp)
|
Text("$quickVol mL", color = MlSecondaryText, fontSize = 14.sp)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Icon(Icons.Default.ChevronRight, contentDescription = null, tint = MlSecondaryText)
|
Icon(Icons.Default.ChevronRight, contentDescription = null, tint = MlSecondaryText)
|
||||||
}
|
}
|
||||||
@@ -913,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 (displayMaxVolumeMl > 0) "${"%.0f".format(displayMaxVolumeMl)} 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,
|
||||||
@@ -927,9 +927,14 @@ 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 "—"
|
// Auto 윈도우 < 5일 때는 표시 안 함
|
||||||
InfoCard("Measurement", currentMeasurement, "ml", urgency.accentColor, Modifier.weight(1f))
|
val showMeasurement = if (isAutoMeasuring) autoVolumeWindow.size >= 5 && lastVolumeMl > 0 else lastVolumeMl > 0
|
||||||
InfoCard("Fill", if (fillPct > 0) "%.0f".format(fillPct) else "—", "%", urgency.accentColor, Modifier.weight(1f))
|
val currentMeasurement = if (showMeasurement) "%.0f".format(lastVolumeMl) else "—"
|
||||||
|
InfoCard("Current Measurement", currentMeasurement, "mL", urgency.accentColor, Modifier.weight(1f))
|
||||||
|
val maxMeasured = if (displayMaxVolumeMl > 0) "%.0f".format(displayMaxVolumeMl) else "—"
|
||||||
|
val bladderMax = "${appState.piezoSettings.maxVolume}"
|
||||||
|
val fillMain = if (fillPct > 0) "${"%.0f".format(fillPct)}%" else "—"
|
||||||
|
InfoCard("$maxMeasured mL / $bladderMax mL", fillMain, "", urgency.accentColor, Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control buttons: Void/Catheterize / Auto / Spot
|
// Control buttons: Void/Catheterize / Auto / Spot
|
||||||
@@ -1008,6 +1013,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
val result = trimmedMean(spotVolumes.toList())
|
val result = trimmedMean(spotVolumes.toList())
|
||||||
lastVolumeMl = result
|
lastVolumeMl = result
|
||||||
maxVolumeMl = maxOf(maxVolumeMl, result)
|
maxVolumeMl = maxOf(maxVolumeMl, result)
|
||||||
|
displayMaxVolumeMl = maxVolumeMl // Spot은 즉시 갱신
|
||||||
Log.d("PiezoMonitor", "SPOT trimmedMean=${"%.1f".format(result)}ml from ${spotVolumes.map { "%.0f".format(it) }}")
|
Log.d("PiezoMonitor", "SPOT trimmedMean=${"%.1f".format(result)}ml from ${spotVolumes.map { "%.0f".format(it) }}")
|
||||||
val maxVol = appState.piezoSettings.maxVolume.toDouble()
|
val maxVol = appState.piezoSettings.maxVolume.toDouble()
|
||||||
val pct = if (maxVol > 0) (result / maxVol).coerceAtMost(1.0) else 0.0
|
val pct = if (maxVol > 0) (result / maxVol).coerceAtMost(1.0) else 0.0
|
||||||
@@ -1469,8 +1475,8 @@ private fun ChannelPanel(
|
|||||||
|
|
||||||
// 실제 BV 계산에 사용된 allWalls 값으로 표시
|
// 실제 BV 계산에 사용된 allWalls 값으로 표시
|
||||||
val wall = allWalls.getOrNull(chData.channel)
|
val wall = allWalls.getOrNull(chData.channel)
|
||||||
val antIdx = wall?.first ?: chAnalysis.result?.ant
|
val antIdx = wall?.first
|
||||||
val postIdx = wall?.second ?: chAnalysis.result?.post
|
val postIdx = wall?.second
|
||||||
val wallLen = if (wall != null) wall.second - wall.first else null
|
val wallLen = if (wall != null) wall.second - wall.first else null
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||||
|
|||||||
+59
-21
@@ -62,6 +62,9 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
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) }
|
var startPressedAtMs by remember { mutableStateOf(0L) }
|
||||||
|
var lastPhaseChangeMs by remember { mutableStateOf(0L) }
|
||||||
|
var phaseHoldScans by remember { mutableIntStateOf(0) }
|
||||||
|
var phaseHoldHint by remember { mutableStateOf("") } // 강제 힌트
|
||||||
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 ==
|
||||||
@@ -103,18 +106,29 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
var sweepLastDetected by remember { mutableIntStateOf(0) }
|
var sweepLastDetected by remember { mutableIntStateOf(0) }
|
||||||
var sweepEverSawAll by remember { mutableStateOf(false) }
|
var sweepEverSawAll by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 화면 진입 시 상태 리셋
|
// 화면 진입 시 전체 상태 리셋
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
isContinuousMode = false
|
||||||
|
waitingForStart = true
|
||||||
placementPhase = PlacementPhase.VERTICAL
|
placementPhase = PlacementPhase.VERTICAL
|
||||||
isLocked = false
|
isLocked = false
|
||||||
|
greenLockedAtMs = 0L
|
||||||
|
greenFailCount = 0
|
||||||
scanCount = 0
|
scanCount = 0
|
||||||
placementScore = 0
|
placementScore = 0
|
||||||
|
debounceCount = 0
|
||||||
|
lastHintChangeMs = 0L
|
||||||
|
lastGuidePhase = PlacementPhase.VERTICAL
|
||||||
|
phaseHoldScans = 0
|
||||||
directionHint = "Place the sensor above the pubic bone, then press Start"
|
directionHint = "Place the sensor above the pubic bone, then press Start"
|
||||||
directionIcon = ""
|
directionIcon = ""
|
||||||
|
lastCv = Double.NaN
|
||||||
for (i in 0 until 6) { channelDetected[i] = false; channelUrineLen[i] = 0 }
|
for (i in 0 until 6) { channelDetected[i] = false; channelUrineLen[i] = 0 }
|
||||||
|
lastWallResults = List(6) { null }
|
||||||
rTracker.reset()
|
rTracker.reset()
|
||||||
currentRMm = null; bestRMm = 0.0
|
currentRMm = null; bestRMm = 0.0
|
||||||
sagittalArgMaxCh = null; lateralOffsetMm = null
|
sagittalArgMaxCh = null; lateralOffsetMm = null
|
||||||
|
maxCenterSeen = 0; stableAtMax = 0; boundaryHit = false
|
||||||
sweepLastDetected = 0; sweepEverSawAll = false
|
sweepLastDetected = 0; sweepEverSawAll = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +362,7 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
// 센서 기기 이미지 (방광 타원 중앙에 정렬)
|
// 센서 기기 이미지 (방광 타원 중앙에 정렬)
|
||||||
// 타원 중심 = (navelY+pubisY)/2 ≈ 높이의 68%. Alignment.Center = 50%. 차이 ≈ 18%
|
// 타원 중심 = (navelY+pubisY)/2 ≈ 높이의 68%. Alignment.Center = 50%. 차이 ≈ 18%
|
||||||
val boxHeightDp = with(androidx.compose.ui.platform.LocalDensity.current) {
|
val boxHeightDp = with(androidx.compose.ui.platform.LocalDensity.current) {
|
||||||
(this@Box).let { 100.dp } // fallback
|
(this@Box).let { 110.dp } // fallback
|
||||||
}
|
}
|
||||||
androidx.compose.foundation.Image(
|
androidx.compose.foundation.Image(
|
||||||
painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.sensor_device),
|
painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.sensor_device),
|
||||||
@@ -368,8 +382,16 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
val hintGreen = isGreenZone
|
val hintGreen = isGreenZone
|
||||||
val hintBg = if (hintGreen) Color(0xFF4CAF50).copy(alpha = 0.12f) else Color(0xFFFFF3E0)
|
val hintBg = if (hintGreen) Color(0xFF4CAF50).copy(alpha = 0.12f) else Color(0xFFFFF3E0)
|
||||||
val hintFg = if (hintGreen) Color(0xFF4CAF50) else Color(0xFFFF9800)
|
val hintFg = if (hintGreen) Color(0xFF4CAF50) else Color(0xFFFF9800)
|
||||||
|
// 방향 힌트에 점 순환 (. / .. / ...) — 업데이트 중임을 시각적으로 표시
|
||||||
|
val dots = ".".repeat((scanCount % 3) + 1)
|
||||||
val displayHint = buildString {
|
val displayHint = buildString {
|
||||||
append(directionHint.ifEmpty { " " })
|
val hint = directionHint.ifEmpty { " " }
|
||||||
|
append(hint)
|
||||||
|
// 방향 안내 중일 때만 점 추가 (Optimal, Place sensor 등은 제외)
|
||||||
|
if (hint.startsWith("Raise") || hint.startsWith("Lower") || hint.startsWith("Move") ||
|
||||||
|
hint.startsWith("Fine-tune") || hint.startsWith("Checking")) {
|
||||||
|
append(dots)
|
||||||
|
}
|
||||||
if (appState.isDevMode && !lastCv.isNaN()) {
|
if (appState.isDevMode && !lastCv.isNaN()) {
|
||||||
append(" (CV=${"%.3f".format(lastCv)})")
|
append(" (CV=${"%.3f".format(lastCv)})")
|
||||||
}
|
}
|
||||||
@@ -638,18 +660,32 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
wasGreen != isNowGreen
|
wasGreen != isNowGreen
|
||||||
}
|
}
|
||||||
val phaseChanged = guideResult.nextPhase != placementPhase
|
val phaseChanged = guideResult.nextPhase != placementPhase
|
||||||
|
// phase 전환 후 3스캔 보호 (카운터 기반, 타이밍 무관)
|
||||||
|
val phaseProtected = phaseHoldScans > 0
|
||||||
|
if (phaseProtected) phaseHoldScans--
|
||||||
// Start 후 5초간 같은 phase 내 힌트는 즉시, phase 전환은 3초 유지
|
// Start 후 5초간 같은 phase 내 힌트는 즉시, phase 전환은 3초 유지
|
||||||
val justStarted = (now - startPressedAtMs) < 5000
|
val justStarted = (now - startPressedAtMs) < 5000
|
||||||
val hintUpdate = justStarted && !phaseChanged // 같은 step 내 힌트만 즉시
|
val hintUpdate = justStarted && !phaseChanged && !phaseProtected
|
||||||
if (hintUpdate || greenChanged || (phaseChanged && !greenHeld && debounceCount >= 2 && elapsed >= 3000) || (!phaseChanged && !greenHeld && debounceCount >= 2 && elapsed >= 1500)) {
|
if (hintUpdate || greenChanged || (phaseChanged && !greenHeld && debounceCount >= 2 && elapsed >= 3000) || (!phaseChanged && !phaseProtected && !greenHeld && debounceCount >= 2 && elapsed >= 1500)) {
|
||||||
directionHint = guideResult.hint
|
directionHint = guideResult.hint
|
||||||
directionIcon = guideResult.icon
|
directionIcon = guideResult.icon
|
||||||
placementScore = guideResult.score
|
placementScore = guideResult.score
|
||||||
|
if (phaseChanged) {
|
||||||
|
lastPhaseChangeMs = now
|
||||||
|
phaseHoldScans = 3
|
||||||
|
phaseHoldHint = guideResult.hint // 전환 힌트 저장
|
||||||
|
}
|
||||||
placementPhase = guideResult.nextPhase
|
placementPhase = guideResult.nextPhase
|
||||||
lastHintChangeMs = now
|
lastHintChangeMs = now
|
||||||
if (!guideResult.cv.isNaN()) lastCv = guideResult.cv
|
if (!guideResult.cv.isNaN()) lastCv = guideResult.cv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// phase 보호 중이면 전환 힌트 강제 유지 (무조건 덮어쓰기)
|
||||||
|
if (phaseProtected && phaseHoldHint.isNotEmpty()) {
|
||||||
|
directionHint = phaseHoldHint
|
||||||
|
directionIcon = "checkmark.circle"
|
||||||
|
}
|
||||||
|
|
||||||
// GREEN hold 중이면 힌트 강제 유지
|
// GREEN hold 중이면 힌트 강제 유지
|
||||||
if (greenHeld) {
|
if (greenHeld) {
|
||||||
directionHint = "Optimal position!"
|
directionHint = "Optimal position!"
|
||||||
@@ -748,6 +784,7 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
lastHintChangeMs = 0L
|
lastHintChangeMs = 0L
|
||||||
scanCount = 0
|
scanCount = 0
|
||||||
startPressedAtMs = System.currentTimeMillis()
|
startPressedAtMs = System.currentTimeMillis()
|
||||||
|
phaseHoldScans = 0
|
||||||
placementPhase = PlacementPhase.VERTICAL
|
placementPhase = PlacementPhase.VERTICAL
|
||||||
isLocked = false
|
isLocked = false
|
||||||
greenLockedAtMs = 0L
|
greenLockedAtMs = 0L
|
||||||
@@ -1332,25 +1369,16 @@ private fun computePlacementGuide(
|
|||||||
"VERTICAL gradient=%.2f (center=%.2f) variation=%.2f detect=$centerDetected lens=$centerLens"
|
"VERTICAL gradient=%.2f (center=%.2f) variation=%.2f detect=$centerDetected lens=$centerLens"
|
||||||
.format(gradient, weightedCenter, lenVariation))
|
.format(gradient, weightedCenter, lenVariation))
|
||||||
|
|
||||||
if (kotlin.math.abs(gradient) < gradientThr && lenVariation < 0.35) {
|
if (centerDetected == 4 && kotlin.math.abs(gradient) < gradientThr && lenVariation < 0.35) {
|
||||||
// 상하 OK → 좌우 확인
|
// 상하 OK → 좌우 확인
|
||||||
val l4 = urineLens.getOrElse(4) { 0 }
|
val l4 = urineLens.getOrElse(4) { 0 }
|
||||||
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 }
|
||||||
|
|
||||||
// 항상 LATERAL 단계를 거침 (Step 1→2→3 순서 강제)
|
// 항상 LATERAL 단계를 거침 — 전환 시 항상 Checking 표시
|
||||||
|
val lrHint = "Checking left/right balance..."
|
||||||
val lrHint = when {
|
val lrIcon = "checkmark.circle"
|
||||||
l4 > 0 && l5 > 0 && l4 > l5 -> "Move left ←"
|
|
||||||
l4 > 0 && l5 > 0 && l5 > l4 -> "Move right →"
|
|
||||||
else -> "Checking left/right balance..."
|
|
||||||
}
|
|
||||||
val lrIcon = when {
|
|
||||||
l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.left"
|
|
||||||
l4 > 0 && l5 > 0 && l5 > l4 -> "arrow.right"
|
|
||||||
else -> "checkmark.circle"
|
|
||||||
}
|
|
||||||
val score = (40 + ((1.0 - kotlin.math.abs(gradient) / gradientThr) * 20).toInt())
|
val score = (40 + ((1.0 - kotlin.math.abs(gradient) / gradientThr) * 20).toInt())
|
||||||
.coerceIn(40, 60)
|
.coerceIn(40, 60)
|
||||||
return PlacementGuideResult(lrHint, lrIcon, score,
|
return PlacementGuideResult(lrHint, lrIcon, score,
|
||||||
@@ -1359,7 +1387,7 @@ private fun computePlacementGuide(
|
|||||||
|
|
||||||
// gradient 기반 방향 안내 (dead zone: |gradient|<0.15 → LATERAL로)
|
// gradient 기반 방향 안내 (dead zone: |gradient|<0.15 → LATERAL로)
|
||||||
// ch0=위, ch3=아래. gradient>0 = ch3쪽 길다 = 방광 중심이 아래 = 내려야
|
// ch0=위, ch3=아래. gradient>0 = ch3쪽 길다 = 방광 중심이 아래 = 내려야
|
||||||
if (kotlin.math.abs(gradient) < 0.15) {
|
if (centerDetected == 4 && kotlin.math.abs(gradient) < 0.15) {
|
||||||
// gradient 거의 0 — LATERAL로 진행 (위아래 깜빡임 방지)
|
// gradient 거의 0 — LATERAL로 진행 (위아래 깜빡임 방지)
|
||||||
val l4 = urineLens.getOrElse(4) { 0 }
|
val l4 = urineLens.getOrElse(4) { 0 }
|
||||||
val l5 = urineLens.getOrElse(5) { 0 }
|
val l5 = urineLens.getOrElse(5) { 0 }
|
||||||
@@ -1532,13 +1560,23 @@ private fun computePlacementGuide(
|
|||||||
|
|
||||||
// 방향이 틀리면 해당 방향으로 안내, 방향 OK면 GREEN에 머물며 재시도
|
// 방향이 틀리면 해당 방향으로 안내, 방향 OK면 GREEN에 머물며 재시도
|
||||||
val adjustHint: String; val adjustIcon: String
|
val adjustHint: String; val adjustIcon: String
|
||||||
|
val ch4det = detected.getOrElse(4) { false }
|
||||||
|
val ch5det = detected.getOrElse(5) { false }
|
||||||
when {
|
when {
|
||||||
|
// ch4/ch5 중 하나만 빠짐 → 반대 방향 안내
|
||||||
|
!lrPass && !ch4det && ch5det -> {
|
||||||
|
adjustHint = "Move left ←"; adjustIcon = "arrow.left"
|
||||||
|
}
|
||||||
|
!lrPass && ch4det && !ch5det -> {
|
||||||
|
adjustHint = "Move right →"; adjustIcon = "arrow.right"
|
||||||
|
}
|
||||||
|
!lrPass && !ch4det && !ch5det -> {
|
||||||
|
adjustHint = "Adjust left or right"; adjustIcon = "arrow.right"
|
||||||
|
}
|
||||||
!lrPass && lrDev > DEFAULT_LR_DEV_THR -> {
|
!lrPass && lrDev > DEFAULT_LR_DEV_THR -> {
|
||||||
// d4 > d5 → 방광 왼쪽 → 왼쪽으로
|
|
||||||
adjustHint = "Move left ←"; adjustIcon = "arrow.left"
|
adjustHint = "Move left ←"; adjustIcon = "arrow.left"
|
||||||
}
|
}
|
||||||
!lrPass && lrDev < -DEFAULT_LR_DEV_THR -> {
|
!lrPass && lrDev < -DEFAULT_LR_DEV_THR -> {
|
||||||
// d5 > d4 → 방광 오른쪽 → 오른쪽으로
|
|
||||||
adjustHint = "Move right →"; adjustIcon = "arrow.right"
|
adjustHint = "Move right →"; adjustIcon = "arrow.right"
|
||||||
}
|
}
|
||||||
gradient > 0.3 -> {
|
gradient > 0.3 -> {
|
||||||
|
|||||||
Reference in New Issue
Block a user