feat(dev): dev-mode RSSI live overlay · PiezoMonitoringView 도넛차트 위
BleManager: - val rssi: MutableStateOf<Int?> — 미연결 시 null - startRssiPolling() / stopRssiPolling() — 2초 주기 readRemoteRssi() - Service ready 3 경로에 startRssiPolling() wire - disconnect · gatt error · reconnect · forceDisconnect 5 경로에 stopRssiPolling() + rssi.value = null PiezoMonitoringView: - val liveRssi by bleManager.rssi - Donut 위에 dev-mode gate overlay (badge chip): · null → "-- dBm" gray · >= -60 dBm → green (strong) · >= -75 dBm → amber (ok) · < -75 dBm → red (weak) - 개발자모드 (3-tap Bladdy) 진입 시에만 표시 일반 사용자 UI 영향 없음 (isDevMode=false 기본).
This commit is contained in:
@@ -64,6 +64,8 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
private val BATTERY_POLL_INTERVAL_MS = 30_000L // 정상 polling 간격
|
private val BATTERY_POLL_INTERVAL_MS = 30_000L // 정상 polling 간격
|
||||||
private val BATTERY_RETRY_DELAY_MS = 3_000L // 첫 응답 재시도 지연
|
private val BATTERY_RETRY_DELAY_MS = 3_000L // 첫 응답 재시도 지연
|
||||||
private val BATTERY_RETRY_MAX = 2 // 최대 재시도 횟수
|
private val BATTERY_RETRY_MAX = 2 // 최대 재시도 횟수
|
||||||
|
private var rssiTimer: Runnable? = null
|
||||||
|
private val RSSI_POLL_INTERVAL_MS = 2_000L // dev-mode UI polling 간격
|
||||||
private var connectionTimer: Runnable? = null
|
private var connectionTimer: Runnable? = null
|
||||||
private var isUserDisconnect = false
|
private var isUserDisconnect = false
|
||||||
var lastConnectedAddress: String? = null
|
var lastConnectedAddress: String? = null
|
||||||
@@ -112,6 +114,8 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val batteryLevel = mutableStateOf(0)
|
val batteryLevel = mutableStateOf(0)
|
||||||
|
/** 현재 연결된 디바이스 RSSI (dBm) · 미측정/미연결 시 null. dev-mode UI 오버레이용. */
|
||||||
|
val rssi = mutableStateOf<Int?>(null)
|
||||||
val bluetoothEnabled = mutableStateOf(bluetoothAdapter?.isEnabled == true)
|
val bluetoothEnabled = mutableStateOf(bluetoothAdapter?.isEnabled == true)
|
||||||
val isServiceReady = mutableStateOf(false)
|
val isServiceReady = mutableStateOf(false)
|
||||||
|
|
||||||
@@ -364,6 +368,8 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
cancelAutoReconnect()
|
cancelAutoReconnect()
|
||||||
stopWatchdog()
|
stopWatchdog()
|
||||||
stopBatteryPolling()
|
stopBatteryPolling()
|
||||||
|
stopRssiPolling()
|
||||||
|
rssi.value = null
|
||||||
com.medithings.vesiscan.services.BleForegroundService.stop(context)
|
com.medithings.vesiscan.services.BleForegroundService.stop(context)
|
||||||
connectionTimer?.let { handler.removeCallbacks(it) }
|
connectionTimer?.let { handler.removeCallbacks(it) }
|
||||||
connectionTimer = null
|
connectionTimer = null
|
||||||
@@ -733,6 +739,29 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
batteryRetryTimer = null
|
batteryRetryTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSSI polling — 2 초 주기로 `readRemoteRssi()` 요청 → `onReadRemoteRssi` 콜백이
|
||||||
|
* `rssi.value` 업데이트. dev-mode UI 오버레이가 이를 관찰한다.
|
||||||
|
* 실패해도 필수기능 영향 없음 (UC-05).
|
||||||
|
*/
|
||||||
|
fun startRssiPolling() {
|
||||||
|
stopRssiPolling()
|
||||||
|
val pollRunnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (!isConnected.value) return
|
||||||
|
try { bluetoothGatt?.readRemoteRssi() } catch (_: Exception) {}
|
||||||
|
handler.postDelayed(this, RSSI_POLL_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rssiTimer = pollRunnable
|
||||||
|
handler.post(pollRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopRssiPolling() {
|
||||||
|
rssiTimer?.let { handler.removeCallbacks(it) }
|
||||||
|
rssiTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
// ── Watchdog (zombie connection detection, runs on separate thread) ──
|
// ── Watchdog (zombie connection detection, runs on separate thread) ──
|
||||||
|
|
||||||
private var watchdogJob: Thread? = null
|
private var watchdogJob: Thread? = null
|
||||||
@@ -780,6 +809,8 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
mtbTimeoutRunnable?.let { handler.removeCallbacks(it); mtbTimeoutRunnable = null }
|
mtbTimeoutRunnable?.let { handler.removeCallbacks(it); mtbTimeoutRunnable = null }
|
||||||
cccdRetryTimer?.let { handler.removeCallbacks(it); cccdRetryTimer = null }
|
cccdRetryTimer?.let { handler.removeCallbacks(it); cccdRetryTimer = null }
|
||||||
stopBatteryPolling()
|
stopBatteryPolling()
|
||||||
|
stopRssiPolling()
|
||||||
|
rssi.value = null
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
isServiceReady.value = false
|
isServiceReady.value = false
|
||||||
connectedDeviceName.value = ""
|
connectedDeviceName.value = ""
|
||||||
@@ -800,6 +831,8 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
|
|
||||||
fun forceDisconnectAndReconnect() {
|
fun forceDisconnectAndReconnect() {
|
||||||
stopBatteryPolling()
|
stopBatteryPolling()
|
||||||
|
stopRssiPolling()
|
||||||
|
rssi.value = null
|
||||||
stopWatchdog()
|
stopWatchdog()
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
isServiceReady.value = false
|
isServiceReady.value = false
|
||||||
@@ -937,6 +970,8 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
debugLogger.error("GATT_ERROR status=$status newState=$newState wasConnected=$wasConnected")
|
debugLogger.error("GATT_ERROR status=$status newState=$newState wasConnected=$wasConnected")
|
||||||
|
|
||||||
stopBatteryPolling()
|
stopBatteryPolling()
|
||||||
|
stopRssiPolling()
|
||||||
|
rssi.value = null
|
||||||
stopWatchdog()
|
stopWatchdog()
|
||||||
// 2026-07-07 fix: 연결 실패 시 대기 중인 timer 취소.
|
// 2026-07-07 fix: 연결 실패 시 대기 중인 timer 취소.
|
||||||
fwFallbackTimer?.let { handler.removeCallbacks(it); fwFallbackTimer = null }
|
fwFallbackTimer?.let { handler.removeCallbacks(it); fwFallbackTimer = null }
|
||||||
@@ -1011,6 +1046,8 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
logd { "Disconnected from ${gatt.device.address}" }
|
logd { "Disconnected from ${gatt.device.address}" }
|
||||||
isConnecting.value = false
|
isConnecting.value = false
|
||||||
stopBatteryPolling()
|
stopBatteryPolling()
|
||||||
|
stopRssiPolling()
|
||||||
|
rssi.value = null
|
||||||
// 2026-07-07 fix: disconnect 시 대기 중인 timer 취소.
|
// 2026-07-07 fix: disconnect 시 대기 중인 timer 취소.
|
||||||
fwFallbackTimer?.let { handler.removeCallbacks(it); fwFallbackTimer = null }
|
fwFallbackTimer?.let { handler.removeCallbacks(it); fwFallbackTimer = null }
|
||||||
mtbTimeoutRunnable?.let { handler.removeCallbacks(it); mtbTimeoutRunnable = null }
|
mtbTimeoutRunnable?.let { handler.removeCallbacks(it); mtbTimeoutRunnable = null }
|
||||||
@@ -1085,6 +1122,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
isServiceReady.value = true
|
isServiceReady.value = true
|
||||||
onConnectionStateChanged?.invoke(true)
|
onConnectionStateChanged?.invoke(true)
|
||||||
startBatteryPolling()
|
startBatteryPolling()
|
||||||
|
startRssiPolling()
|
||||||
startWatchdog()
|
startWatchdog()
|
||||||
logd { "Service ready (no CCCD)" }
|
logd { "Service ready (no CCCD)" }
|
||||||
}
|
}
|
||||||
@@ -1096,6 +1134,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
isServiceReady.value = true
|
isServiceReady.value = true
|
||||||
onConnectionStateChanged?.invoke(true)
|
onConnectionStateChanged?.invoke(true)
|
||||||
startBatteryPolling()
|
startBatteryPolling()
|
||||||
|
startRssiPolling()
|
||||||
startWatchdog()
|
startWatchdog()
|
||||||
logd { "Service ready (no RX)" }
|
logd { "Service ready (no RX)" }
|
||||||
}
|
}
|
||||||
@@ -1124,6 +1163,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
com.medithings.vesiscan.services.AdcCsvLogger.newSession()
|
com.medithings.vesiscan.services.AdcCsvLogger.newSession()
|
||||||
com.medithings.vesiscan.services.BleForegroundService.start(context)
|
com.medithings.vesiscan.services.BleForegroundService.start(context)
|
||||||
startBatteryPolling()
|
startBatteryPolling()
|
||||||
|
startRssiPolling()
|
||||||
startWatchdog()
|
startWatchdog()
|
||||||
sendDeviceInfoQuery()
|
sendDeviceInfoQuery()
|
||||||
// 4초 후에도 펌웨어 미수신이면 mfv? fallback (구 펌웨어는 mid? 미지원 가능성).
|
// 4초 후에도 펌웨어 미수신이면 mfv? fallback (구 펌웨어는 mid? 미지원 가능성).
|
||||||
@@ -1196,7 +1236,10 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
|
|
||||||
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
handler.post { debugLogger.rssi(rssi) }
|
handler.post {
|
||||||
|
debugLogger.rssi(rssi)
|
||||||
|
this@BleManager.rssi.value = rssi
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
val urgency = appState.bladderLevel.urgency
|
val urgency = appState.bladderLevel.urgency
|
||||||
val batteryLvl by bleManager.batteryLevel
|
val batteryLvl by bleManager.batteryLevel
|
||||||
val isConnected by bleManager.isConnected
|
val isConnected by bleManager.isConnected
|
||||||
|
val liveRssi by bleManager.rssi
|
||||||
val catheterCount by appState.catheterCount
|
val catheterCount by appState.catheterCount
|
||||||
|
|
||||||
// Auto 측정 윈도우 (10개 모아서 최대/최소 각 1개 제외 → 8개 평균)
|
// Auto 측정 윈도우 (10개 모아서 최대/최소 각 1개 제외 → 8개 평균)
|
||||||
@@ -1404,6 +1405,40 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dev-mode: RSSI live overlay — 도넛 위. 미연결/미측정 시 "-- dBm".
|
||||||
|
if (appState.isDevMode) {
|
||||||
|
val rssiValue = liveRssi
|
||||||
|
val (rssiLabel, rssiTint) = when {
|
||||||
|
rssiValue == null -> "-- dBm" to Color.Gray
|
||||||
|
rssiValue >= -60 -> "$rssiValue dBm" to Color(0xFF2E7D32) // strong · green
|
||||||
|
rssiValue >= -75 -> "$rssiValue dBm" to Color(0xFFF9A825) // ok · amber
|
||||||
|
else -> "$rssiValue dBm" to Color(0xFFD32F2F) // weak · red
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 4.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(rssiTint.copy(alpha = 0.12f))
|
||||||
|
.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"RSSI",
|
||||||
|
fontSize = (11 * fontScale).sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = rssiTint,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
rssiLabel,
|
||||||
|
fontSize = (12 * fontScale).sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = rssiTint,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Donut + Bladdy sizing — 회의 결정: dev/일반 통일.
|
// Donut + Bladdy sizing — 회의 결정: dev/일반 통일.
|
||||||
val baseDonutSize = donutFitSize
|
val baseDonutSize = donutFitSize
|
||||||
val donutSize = baseDonutSize * 0.95f
|
val donutSize = baseDonutSize * 0.95f
|
||||||
|
|||||||
Reference in New Issue
Block a user