diff --git a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt index b76fc8d..083d055 100644 --- a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt +++ b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt @@ -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() @@ -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