feat(ble): R100 견고성 3종 · startup 지연 대응 + hang 안내 + 재연결 제한
배경 (2026-08-04 · VBT0607R100 로그 분석): - 연결 직후 첫 msn/mid 명령이 9초간 무응답 → CMDQ timeout 3회 · 사용자 노출 - mpa → mtb 발사 후 GATT status=8 (link supervision timeout) · 기기 hang - 이후 재연결 5회+ 실패 · 기기가 air 상에 안 잡힘 (advertising 죽음) - FW 사이드 문제 · 앱 층에서 UX 완화 가능한 부분 개선 변경: 1. INITIAL_TX_DELAY_MS = 2_000ms — CCCD write 후 첫 명령 2초 지연 R100 boot 늦은 케이스에서 초기 timeout 반복 방지. Watchdog 은 즉시 시작. 2. GATT status 8/19/22 별도 처리 - status=8 GATT_CONN_TIMEOUT (link supervision timeout · FW hang) - status=19 GATT_CONN_TERMINATE_PEER_USER - status=22 GATT_CONN_TERMINATE_LOCAL_HOST → connectionError = "DEVICE_UNRESPONSIVE:\$status" marker → UI 에서 "전원 15초 롱프레스로 재부팅 후 다시 연결하세요" 안내 Bond conflict (status=5) 는 기존대로 별도 처리 유지. 3. MAX_RECONNECT_ATTEMPTS 10 → 5 실패 후 connectionError = "RECONNECT_GAVE_UP" marker + onReconnectionFailed → UI 에서 "기기가 켜져 있는지 확인 후 다시 시도" 안내 무한 시도 로그 스팸 방지. 4. mtb_response_reconnect 문구 개선 "연결을 재시도합니다" → "재부팅이 필요할 수 있습니다 (전원 15초 롱프레스)" 3회 consecutive mtb timeout 시 사용자에게 더 actionable 안내. strings.xml (KR/EN): - ble_error_device_unresponsive (재부팅 안내) - ble_error_reconnect_gave_up (재연결 포기 안내) - mtb_response_reconnect 갱신 FW 팀 조사와 병행. 로그 (2026-08-04_134048) FW 팀 전달 예정.
This commit is contained in:
@@ -73,7 +73,9 @@ class BleManager private constructor(private val context: Context) {
|
||||
private var reconnectAttempt = 0
|
||||
private val reconnectIntervalMs: Long = 3000
|
||||
/** max retry — 10회 시도 후 포기 (≈ 8s scan × 10 + 간격, 약 100~120초). */
|
||||
private val MAX_RECONNECT_ATTEMPTS = 10
|
||||
// 2026-08-04: 10 → 5 축소. 지속 실패 시 사용자에게 명확한 안내가 낫고 무한 시도로
|
||||
// 상단 배너/로그 스팸 방지. 실패 후 사용자가 명시적으로 재시도 (앱 재시작 · 재연결).
|
||||
private val MAX_RECONNECT_ATTEMPTS = 5
|
||||
private var reconnectTimer: Runnable? = null
|
||||
@Volatile private var lastRxTimestamp: Long = 0
|
||||
private var watchdogTimer: Runnable? = null
|
||||
@@ -91,6 +93,14 @@ class BleManager private constructor(private val context: Context) {
|
||||
@Volatile private var fwRetryCount: Int = 0
|
||||
private val FW_RETRY_MAX = 3
|
||||
private val FW_RETRY_INTERVAL_MS = 5_000L
|
||||
/**
|
||||
* CONNECTED (CCCD write 성공) 후 첫 명령까지 대기 시간.
|
||||
* 2026-08-04: R100 목업 유닛이 boot 후 응답까지 9초 걸리는 케이스 관찰 (VBT0607R100 로그).
|
||||
* 첫 msn/mid 를 즉시 발사 → 3초 CMDQ timeout 3회 반복 → 사용자에게 노출.
|
||||
* 2초 지연 두면 FW startup 이 조금 여유 있을 때 첫 명령 통과 확률 상승.
|
||||
* 여전히 매우 느린 FW 는 재시도 로직 (scheduleFirmwareRetry) 이 커버.
|
||||
*/
|
||||
private val INITIAL_TX_DELAY_MS = 2_000L
|
||||
private var cccdRetryTimer: Runnable? = null
|
||||
|
||||
val discoveredDevices = mutableStateListOf<BleDevice>()
|
||||
@@ -1085,6 +1095,9 @@ class BleManager private constructor(private val context: Context) {
|
||||
debugLogger.warn("RECONNECT_GIVE_UP after ${reconnectAttempt} attempts — manual retry required")
|
||||
isReconnecting.value = false
|
||||
reconnectAttempt = 0
|
||||
// 2026-08-04: 사용자에게 명확한 안내. UI (DeviceScanView 등) 가 이 marker 감지 시
|
||||
// "재연결 실패 · 기기 상태 확인 후 재시도" 안내 표시.
|
||||
connectionError.value = "RECONNECT_GAVE_UP"
|
||||
onReconnectionFailed?.invoke()
|
||||
return
|
||||
}
|
||||
@@ -1214,9 +1227,18 @@ class BleManager private constructor(private val context: Context) {
|
||||
// 다른 central 과 이미 bond 중이면 peripheral 이 인증 거부.
|
||||
val failedAddr = gatt.device.address
|
||||
if (status == 5) {
|
||||
// GATT_INSUFFICIENT_AUTHENTICATION — FW allowlist bond 충돌
|
||||
bondConflictAddress.value = failedAddr
|
||||
connectionError.value = "BOND_CONFLICT:$failedAddr"
|
||||
debugLogger.warn("BOND_CONFLICT addr=$failedAddr — FW allowlist rejected (status 5)")
|
||||
} else if (status == 8 || status == 22 || status == 19) {
|
||||
// status=8 · GATT_CONN_TIMEOUT — link supervision timeout (peripheral 사망)
|
||||
// status=22 · GATT_CONN_TERMINATE_LOCAL_HOST
|
||||
// status=19 · GATT_CONN_TERMINATE_PEER_USER — peripheral 이 명시적 disconnect
|
||||
// → 세 케이스 모두 peripheral 문제 · 사용자에게 재부팅 안내 권장
|
||||
bondConflictAddress.value = null
|
||||
connectionError.value = "DEVICE_UNRESPONSIVE:$status"
|
||||
debugLogger.warn("DEVICE_UNRESPONSIVE status=$status — FW hang/crash · user reboot recommended")
|
||||
} else {
|
||||
bondConflictAddress.value = null
|
||||
connectionError.value = "Connection failed (status $status). Try again."
|
||||
@@ -1394,27 +1416,30 @@ class BleManager private constructor(private val context: Context) {
|
||||
debugLogger.info("CONN_PRIORITY HIGH requested (ok=$priorityOk)")
|
||||
com.medithings.vesiscan.services.AdcCsvLogger.newSession()
|
||||
com.medithings.vesiscan.services.BleForegroundService.start(context)
|
||||
// Watchdog 은 즉시 시작 (freeze 감지 필요).
|
||||
startWatchdog()
|
||||
// 2026-08-04: 첫 TX 를 INITIAL_TX_DELAY_MS 지연.
|
||||
// R100 등 신규 HW 는 boot 후 응답까지 여러 초 걸림 → 즉시 발사 시
|
||||
// 초기 msn/mid 가 CMDQ timeout 반복 → 사용자에게 노출.
|
||||
fwRetryCount = 0
|
||||
handler.postDelayed({
|
||||
if (!isConnected.value) return@postDelayed
|
||||
startBatteryPolling()
|
||||
startRssiPolling()
|
||||
startWatchdog()
|
||||
fwRetryCount = 0
|
||||
sendDeviceInfoQuery()
|
||||
// 4초 후에도 펌웨어 미수신이면 mfv? fallback (구 펌웨어는 mid? 미지원 가능성).
|
||||
// 2026-07-07 fix: disconnect 시 취소되도록 Runnable 참조 유지.
|
||||
// 4초 후에도 펌웨어 미수신이면 mfv? fallback.
|
||||
fwFallbackTimer?.let { handler.removeCallbacks(it) }
|
||||
val fwFallback = Runnable {
|
||||
if (firmwareVersion.value.isEmpty() && isConnected.value) {
|
||||
debugLogger.info("FW fallback: sending mfv?")
|
||||
sendFirmwareVersionQuery()
|
||||
// 2026-08-04: 첫 mfv? fallback 실패 대비 재시도 스케줄러 시작.
|
||||
// 5초 간격으로 mid?/mfv? 교차 최대 3회 · "기기 응답 없음"
|
||||
// 배너 false positive 대폭 감소 (알림 창 노출 조건 §Rev1 참고).
|
||||
scheduleFirmwareRetry()
|
||||
}
|
||||
}
|
||||
fwFallbackTimer = fwFallback
|
||||
handler.postDelayed(fwFallback, 4_000)
|
||||
logd { "Service ready! (CCCD written)" }
|
||||
}, INITIAL_TX_DELAY_MS)
|
||||
logd { "Service ready! (CCCD written) · initial TX in ${INITIAL_TX_DELAY_MS}ms" }
|
||||
}
|
||||
} else {
|
||||
loge { "CCCD write failed with status: $status — retrying in 500ms" }
|
||||
|
||||
@@ -226,11 +226,13 @@ fun DeviceScanView(appState: AppState) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
// Connection error — GATT status=5 (bond conflict) 시 별도 UX 안내 + reset action.
|
||||
// Connection error — marker 기반 분기 (BOND_CONFLICT / DEVICE_UNRESPONSIVE / RECONNECT_GAVE_UP / plain).
|
||||
val curError = connectionError
|
||||
val curBondAddr = bondConflictAddress
|
||||
if (curError != null) {
|
||||
val isBondConflict = curError.startsWith("BOND_CONFLICT:") && curBondAddr != null
|
||||
val isDeviceUnresponsive = curError.startsWith("DEVICE_UNRESPONSIVE:")
|
||||
val isReconnectGaveUp = curError == "RECONNECT_GAVE_UP"
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -242,10 +244,12 @@ fun DeviceScanView(appState: AppState) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Close, stringResource(R.string.device_scan_error_desc), tint = Color.White)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
val displayText = if (isBondConflict)
|
||||
stringResource(R.string.ble_error_bond_conflict)
|
||||
else
|
||||
curError
|
||||
val displayText = when {
|
||||
isBondConflict -> stringResource(R.string.ble_error_bond_conflict)
|
||||
isDeviceUnresponsive -> stringResource(R.string.ble_error_device_unresponsive)
|
||||
isReconnectGaveUp -> stringResource(R.string.ble_error_reconnect_gave_up)
|
||||
else -> curError
|
||||
}
|
||||
Text(displayText, color = Color.White, fontSize = 13.sp, modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = {
|
||||
bleManager.connectionError.value = null
|
||||
|
||||
@@ -323,7 +323,7 @@
|
||||
<string name="placement_in_position">최적의 위치입니다!</string>
|
||||
<string name="placement_position_lost">위치를 놓쳤습니다. 치골 위에서 다시 시작해주세요.</string>
|
||||
<string name="mtb_response_delayed">기기 응답이 지연되고 있습니다. 잠시만 기다려주세요…</string>
|
||||
<string name="mtb_response_reconnect">기기가 응답하지 않습니다. 연결을 재시도합니다.</string>
|
||||
<string name="mtb_response_reconnect">기기 응답 실패 · 재부팅이 필요할 수 있습니다 (전원 15초 롱프레스).</string>
|
||||
<string name="placement_detached_restart">센서가 분리되었습니다. 치골 위에서 다시 시작해주세요.</string>
|
||||
<string name="placement_signal_lost">신호를 놓쳤습니다 — 위로 올려주세요 ↑</string>
|
||||
<string name="placement_sweep_start">배꼽 근처에서 시작해 천천히 아래로 내려주세요 ↓</string>
|
||||
@@ -383,6 +383,10 @@
|
||||
<string name="ble_error_bond_conflict">이 기기는 다른 앱과 이미 페어링되어 있습니다. 기기 전원 버튼을 15초 롱프레스로 본딩 정보를 삭제한 뒤 다시 시도하세요.</string>
|
||||
<string name="ble_action_reset_bond">이 폰의 본딩 정보 초기화</string>
|
||||
<string name="ble_toast_bond_removed">본딩 정보 삭제 완료. 다시 연결을 시도하세요.</string>
|
||||
<!-- Device unresponsive (GATT status 8/19/22 · FW hang) — 2026-08-04 -->
|
||||
<string name="ble_error_device_unresponsive">기기가 응답하지 않습니다. 전원 버튼을 15초 롱프레스로 재부팅한 뒤 다시 연결하세요.</string>
|
||||
<!-- Reconnect give-up (MAX_RECONNECT_ATTEMPTS reached) -->
|
||||
<string name="ble_error_reconnect_gave_up">기기 재연결에 여러 번 실패했습니다. 기기가 켜져 있는지 확인 후 다시 시도하세요.</string>
|
||||
<string name="device_scan_back_desc">뒤로</string>
|
||||
<string name="device_scan_warning_desc">경고</string>
|
||||
<string name="device_scan_error_desc">오류</string>
|
||||
|
||||
@@ -323,7 +323,7 @@
|
||||
<string name="placement_in_position">Optimal position!</string>
|
||||
<string name="placement_position_lost">Position lost. Restart above pubic bone.</string>
|
||||
<string name="mtb_response_delayed">Device response delayed. Please wait…</string>
|
||||
<string name="mtb_response_reconnect">Device not responding. Reconnecting.</string>
|
||||
<string name="mtb_response_reconnect">Device not responding. A reboot may be required (long-press power 15 seconds).</string>
|
||||
<string name="placement_detached_restart">Sensor detached. Restart above pubic bone.</string>
|
||||
<string name="placement_signal_lost">Signal lost — slide up ↑</string>
|
||||
<string name="placement_sweep_start">Start near navel, slide down slowly ↓</string>
|
||||
@@ -383,6 +383,10 @@
|
||||
<string name="ble_error_bond_conflict">This device is already paired with another app. Long-press the device power button for 15 seconds to clear its pairing, then try again.</string>
|
||||
<string name="ble_action_reset_bond">Reset Pairing (this phone)</string>
|
||||
<string name="ble_toast_bond_removed">Pairing info removed. Please try connecting again.</string>
|
||||
<!-- Device unresponsive (GATT status 8/19/22 · FW hang) — 2026-08-04 -->
|
||||
<string name="ble_error_device_unresponsive">Device stopped responding. Long-press the device power button for 15 seconds to reboot, then reconnect.</string>
|
||||
<!-- Reconnect give-up (MAX_RECONNECT_ATTEMPTS reached) -->
|
||||
<string name="ble_error_reconnect_gave_up">Could not reconnect to device after several attempts. Please check the device is powered on and try again.</string>
|
||||
<string name="device_scan_back_desc">Back</string>
|
||||
<string name="device_scan_warning_desc">Warning</string>
|
||||
<string name="device_scan_error_desc">Error</string>
|
||||
|
||||
Reference in New Issue
Block a user