fix: watchdog heartbeat, 배터리 계산, placement 디바운스+방향 수정
BLE: - watchdog idle heartbeat: 10초 무통신 시 IMU query로 keepalive - 배터리: 3500-3700mV→0-5%, 3700-4100mV→5-100%, 증가 무시 - 배터리 표시: 아이콘→퍼센트 숫자 (색상 유지) Placement: - 힌트 디바운스: 같은 phase 2회 연속일 때만 변경 (깜빡임 방지) - 방향 힌트 반전: CH3만 잡힘→아래로↓, CH0만 잡힘→위로↑ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -477,11 +477,15 @@ class BleManager private constructor(private val context: Context) {
|
||||
if (!isConnected.value) break
|
||||
|
||||
val silenceMs = System.currentTimeMillis() - lastRxTimestamp
|
||||
|
||||
// Idle 10초 이상이면 IMU query로 heartbeat
|
||||
if (silenceMs > 10000 && silenceMs <= watchdogTimeoutMs) {
|
||||
handler.post { sendImuQuery() }
|
||||
}
|
||||
|
||||
if (silenceMs > watchdogTimeoutMs) {
|
||||
// 직접 로그 기록 (handler 블로킹 무관)
|
||||
debugLogger.error("WATCHDOG_TIMEOUT silence=${silenceMs}ms — forcing reconnect")
|
||||
|
||||
// GATT close는 어느 스레드에서든 호출 가능
|
||||
watchdogRunning = false
|
||||
try {
|
||||
bluetoothGatt?.disconnect()
|
||||
@@ -491,7 +495,6 @@ class BleManager private constructor(private val context: Context) {
|
||||
txCharacteristic = null
|
||||
rxCharacteristic = null
|
||||
|
||||
// UI 업데이트 + reconnect는 메인 스레드
|
||||
handler.post {
|
||||
stopBatteryPolling()
|
||||
isConnected.value = false
|
||||
@@ -872,8 +875,14 @@ class BleManager private constructor(private val context: Context) {
|
||||
"rsn:" -> {
|
||||
if (data.size >= 6) {
|
||||
val millivolts = ((data[4].toInt() and 0xFF) shl 8) or (data[5].toInt() and 0xFF)
|
||||
val pct = ((millivolts - 3700) * 100 / 500).coerceIn(0, 100)
|
||||
batteryLevel.value = pct
|
||||
val pct = when {
|
||||
millivolts <= 3500 -> 0
|
||||
millivolts <= 3700 -> ((millivolts - 3500) * 5 / 200).coerceIn(0, 5)
|
||||
else -> (5 + (millivolts - 3700) * 95 / 400).coerceIn(5, 100)
|
||||
}
|
||||
if (pct <= batteryLevel.value || batteryLevel.value == 0) {
|
||||
batteryLevel.value = pct
|
||||
}
|
||||
debugLogger.rx("rsn", data.size, "battery=${millivolts}mV (${pct}%)")
|
||||
}
|
||||
}
|
||||
|
||||
+2
-7
@@ -543,19 +543,14 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Battery icon (color by level)
|
||||
// Battery percentage
|
||||
if (!isDemoMode) {
|
||||
val batteryColor = when {
|
||||
batteryLvl >= 60 -> Color(0xFF4CAF50)
|
||||
batteryLvl >= 30 -> Color(0xFFFF9800)
|
||||
else -> Color.Red
|
||||
}
|
||||
val batteryIcon = when {
|
||||
batteryLvl >= 60 -> Icons.Default.BatteryFull
|
||||
batteryLvl >= 30 -> Icons.Default.Battery4Bar
|
||||
else -> Icons.Default.Battery1Bar
|
||||
}
|
||||
Icon(batteryIcon, "Battery $batteryLvl%", tint = batteryColor, modifier = Modifier.size(iconSize))
|
||||
Text("$batteryLvl%", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = batteryColor)
|
||||
}
|
||||
|
||||
// Connection dot
|
||||
|
||||
+25
-13
@@ -68,6 +68,8 @@ fun PlacementGuideView(appState: AppState) {
|
||||
var scanCount by remember { mutableIntStateOf(0) }
|
||||
var isLocked by remember { mutableStateOf(false) }
|
||||
var lastChannelData by remember { mutableStateOf<List<PiezoChannelData>>(emptyList()) }
|
||||
var debounceCount by remember { mutableIntStateOf(0) }
|
||||
var lastGuidePhase by remember { mutableStateOf(PlacementPhase.VERTICAL) }
|
||||
|
||||
// 2단계 순차 탐색: VERTICAL → LATERAL → GREEN
|
||||
var placementPhase by remember { mutableStateOf(PlacementPhase.VERTICAL) }
|
||||
@@ -338,17 +340,28 @@ fun PlacementGuideView(appState: AppState) {
|
||||
"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
||||
} else {
|
||||
val guideResult = computePlacementGuide(detected, urineLens, allWalls, scanCount, placementPhase)
|
||||
directionHint = guideResult.hint
|
||||
directionIcon = guideResult.icon
|
||||
placementScore = guideResult.score
|
||||
placementPhase = guideResult.nextPhase
|
||||
|
||||
// 디바운스: 같은 phase가 2회 연속일 때만 힌트 변경
|
||||
if (guideResult.nextPhase == lastGuidePhase) {
|
||||
debounceCount++
|
||||
} else {
|
||||
debounceCount = 1
|
||||
lastGuidePhase = guideResult.nextPhase
|
||||
}
|
||||
|
||||
if (debounceCount >= 2 || guideResult.isPass) {
|
||||
directionHint = guideResult.hint
|
||||
directionIcon = guideResult.icon
|
||||
placementScore = guideResult.score
|
||||
placementPhase = guideResult.nextPhase
|
||||
}
|
||||
|
||||
if (guideResult.isPass) {
|
||||
isLocked = true
|
||||
bleManager.sendLedMode(6) // ALIGN_COMPLETE
|
||||
bleManager.debugLogger.info("PLACEMENT_GREEN cv=${"%.3f".format(guideResult.cv)} lr_dev=${"%.3f".format(guideResult.lrDev)} score=${guideResult.score}")
|
||||
} else {
|
||||
isLocked = false // Green 벗어나면 다시 가이드
|
||||
isLocked = false
|
||||
bleManager.sendLedMode(5) // ALIGN_SEARCHING
|
||||
bleManager.debugLogger.info("PLACEMENT_${guideResult.phase.name} score=${guideResult.score} hint=${guideResult.hint}")
|
||||
}
|
||||
@@ -713,14 +726,13 @@ private fun computePlacementGuide(
|
||||
val ch0 = detected.getOrElse(0) { false }
|
||||
val ch3 = detected.getOrElse(3) { false }
|
||||
val hint = when {
|
||||
ch3 && !ch0 -> "센서를 위로 이동하세요 ↑"
|
||||
!ch3 && ch0 -> "센서를 아래로 이동하세요 ↓"
|
||||
!ch3 && !ch0 -> "센서를 아래에서 위로 천천히 이동하세요"
|
||||
else -> "센서를 아래에서 위로 천천히 이동하세요"
|
||||
ch3 && !ch0 -> "센서를 아래로 이동하세요 ↓"
|
||||
!ch3 && ch0 -> "센서를 위로 이동하세요 ↑"
|
||||
else -> "센서를 천천히 위아래로 이동하세요"
|
||||
}
|
||||
val icon = when {
|
||||
ch3 && !ch0 -> "arrow.up"
|
||||
!ch3 && ch0 -> "arrow.down"
|
||||
ch3 && !ch0 -> "arrow.down"
|
||||
!ch3 && ch0 -> "arrow.up"
|
||||
else -> "arrow.up.arrow.down"
|
||||
}
|
||||
return PlacementGuideResult(hint, icon, centerDetected * 10,
|
||||
@@ -746,10 +758,10 @@ private fun computePlacementGuide(
|
||||
} else {
|
||||
// 크기 편차 큼 → 상하 더 조절
|
||||
return if (len3 > len0 * 2) {
|
||||
PlacementGuideResult("센서를 위로 더 이동하세요 ↑ (CH0 약함)", "arrow.up", 25,
|
||||
PlacementGuideResult("센서를 아래로 더 이동하세요 ↓ (CH0 약함)", "arrow.down", 25,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
} else {
|
||||
PlacementGuideResult("센서를 아래로 더 이동하세요 ↓ (CH3 약함)", "arrow.down", 25,
|
||||
PlacementGuideResult("센서를 위로 더 이동하세요 ↑ (CH3 약함)", "arrow.up", 25,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user