feat(ble): FW query 자동 재시도 · "기기 응답 없음" 배너 false-positive 감소
배경 (2026-08-04 · 사용자 · Alignment idle 시 배너 flap): 연결 직후 mid? (t=0) → mfv? fallback (t=4s) 만으로도 응답 없으면 firmwareVersion 이 empty · 8초 후 FirmwareWarningBanner 의 timedOut=true → Alignment idle 처럼 트래픽 적은 화면에서 배너 뜸. 신규 로직 scheduleFirmwareRetry(): - 5초 간격으로 mid? / mfv? 교차 발사 (최대 3회) - 첫 응답 오면 즉시 중단 (firmwareVersion.isNotEmpty()) - 3회 모두 실패 → 재시도 종료 · 배너는 그때 노출 (진짜 문제) Timing: t=0 mid? (기존) t=4s mfv? fallback (기존) · 여기서 scheduleFirmwareRetry() 시작 t=9s mid? retry #1 (신규) t=14s mfv? retry #2 (신규) t=19s mid? retry #3 (신규) t=24s+ 재시도 종료 · 응답 없으면 배너 Disconnect / GATT error / 좀비 경로에도 stopFirmwareRetry() cleanup. 검증: BUILD SUCCESSFUL 2s
This commit is contained in:
@@ -84,8 +84,13 @@ class BleManager private constructor(private val context: Context) {
|
||||
private val watchdogTimeoutMs: Long = 25000
|
||||
// 2026-07-07 fix: disconnect 시 취소 위해 Runnable 참조 유지.
|
||||
// fwFallback: onDescriptorWrite 성공 후 4초 mfv? fallback.
|
||||
// fwRetry: 2026-08-04 신규 · mid?/mfv? 초기 응답 실패 시 5초 간격 재시도 최대 3회.
|
||||
// cccdRetry: onDescriptorWrite 실패 후 500ms retry.
|
||||
private var fwFallbackTimer: Runnable? = null
|
||||
private var fwRetryTimer: Runnable? = null
|
||||
@Volatile private var fwRetryCount: Int = 0
|
||||
private val FW_RETRY_MAX = 3
|
||||
private val FW_RETRY_INTERVAL_MS = 5_000L
|
||||
private var cccdRetryTimer: Runnable? = null
|
||||
|
||||
val discoveredDevices = mutableStateListOf<BleDevice>()
|
||||
@@ -731,6 +736,60 @@ class BleManager private constructor(private val context: Context) {
|
||||
sendRaw(CRC16.buildCommandASCII("mfv", " "))
|
||||
}
|
||||
|
||||
/**
|
||||
* FW query 자동 재시도 (2026-08-04 · GUARDIAN §Rev2 후속).
|
||||
*
|
||||
* 배경: 연결 직후 mid? (t=0) → mfv? fallback (t=4s) 만으로도 응답 없으면
|
||||
* firmwareVersion 이 empty 로 남고, 8초 후 FirmwareWarningBanner 의
|
||||
* `timedOut = true` 로 배너 노출. Alignment idle 처럼 트래픽이 적은
|
||||
* 화면에서 특히 눈에 띔.
|
||||
*
|
||||
* 신규 로직: `scheduleFirmwareRetry()` 호출 시점부터 5초 간격으로 mid?/mfv?
|
||||
* 를 교차 발사. 최대 3회. 응답 오면 (firmwareVersion 채워짐) 즉시 중단.
|
||||
*
|
||||
* 실패 시: 3회 모두 응답 없음 → 재시도 종료. 배너는 그때 뜸 (진짜 문제 상황).
|
||||
*/
|
||||
fun scheduleFirmwareRetry() {
|
||||
fwRetryTimer?.let { handler.removeCallbacks(it) }
|
||||
val runnable = object : Runnable {
|
||||
override fun run() {
|
||||
// 성공 · 연결 끊김 · 최대 시도 도달 → 중단
|
||||
if (firmwareVersion.value.isNotEmpty()) {
|
||||
debugLogger.info("FW retry: success (fw=${firmwareVersion.value})")
|
||||
fwRetryTimer = null
|
||||
return
|
||||
}
|
||||
if (!isConnected.value) {
|
||||
fwRetryTimer = null
|
||||
return
|
||||
}
|
||||
if (fwRetryCount >= FW_RETRY_MAX) {
|
||||
debugLogger.warn("FW retry: gave up after $FW_RETRY_MAX attempts")
|
||||
fwRetryTimer = null
|
||||
return
|
||||
}
|
||||
fwRetryCount++
|
||||
// mid? / mfv? 교차 (홀수=mid, 짝수=mfv). 다른 명령이 서로 다른 FW 지원.
|
||||
if (fwRetryCount % 2 == 1) {
|
||||
debugLogger.info("FW retry #$fwRetryCount: mid?")
|
||||
sendDeviceInfoQuery()
|
||||
} else {
|
||||
debugLogger.info("FW retry #$fwRetryCount: mfv?")
|
||||
sendFirmwareVersionQuery()
|
||||
}
|
||||
handler.postDelayed(this, FW_RETRY_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
fwRetryTimer = runnable
|
||||
handler.postDelayed(runnable, FW_RETRY_INTERVAL_MS)
|
||||
}
|
||||
|
||||
private fun stopFirmwareRetry() {
|
||||
fwRetryTimer?.let { handler.removeCallbacks(it) }
|
||||
fwRetryTimer = null
|
||||
fwRetryCount = 0
|
||||
}
|
||||
|
||||
fun sendLedMode(state: Int) {
|
||||
sendRaw(CRC16.buildCommandBE("mls", intArrayOf(state)))
|
||||
}
|
||||
@@ -948,6 +1007,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
txCharacteristic = null
|
||||
rxCharacteristic = null
|
||||
fwFallbackTimer?.let { handler.removeCallbacks(it); fwFallbackTimer = null }
|
||||
stopFirmwareRetry()
|
||||
mtbTimeoutRunnable?.let { handler.removeCallbacks(it); mtbTimeoutRunnable = null }
|
||||
cccdRetryTimer?.let { handler.removeCallbacks(it); cccdRetryTimer = null }
|
||||
stopBatteryPolling()
|
||||
@@ -1128,6 +1188,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
stopWatchdog()
|
||||
// 2026-07-07 fix: 연결 실패 시 대기 중인 timer 취소.
|
||||
fwFallbackTimer?.let { handler.removeCallbacks(it); fwFallbackTimer = null }
|
||||
stopFirmwareRetry()
|
||||
mtbTimeoutRunnable?.let { handler.removeCallbacks(it); mtbTimeoutRunnable = null }
|
||||
cccdRetryTimer?.let { handler.removeCallbacks(it); cccdRetryTimer = null }
|
||||
bluetoothGatt = null
|
||||
@@ -1220,6 +1281,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
commandQueue.clear("state-disconnected")
|
||||
// 2026-07-07 fix: disconnect 시 대기 중인 timer 취소.
|
||||
fwFallbackTimer?.let { handler.removeCallbacks(it); fwFallbackTimer = null }
|
||||
stopFirmwareRetry()
|
||||
mtbTimeoutRunnable?.let { handler.removeCallbacks(it); mtbTimeoutRunnable = null }
|
||||
cccdRetryTimer?.let { handler.removeCallbacks(it); cccdRetryTimer = null }
|
||||
bluetoothGatt = null
|
||||
@@ -1335,6 +1397,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
startBatteryPolling()
|
||||
startRssiPolling()
|
||||
startWatchdog()
|
||||
fwRetryCount = 0
|
||||
sendDeviceInfoQuery()
|
||||
// 4초 후에도 펌웨어 미수신이면 mfv? fallback (구 펌웨어는 mid? 미지원 가능성).
|
||||
// 2026-07-07 fix: disconnect 시 취소되도록 Runnable 참조 유지.
|
||||
@@ -1343,6 +1406,10 @@ class BleManager private constructor(private val context: Context) {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user