From 95572ac0760f892f8c505ab5df3e968fe6d46e9c Mon Sep 17 00:00:00 2001 From: jjangddu Date: Fri, 5 Jun 2026 15:37:23 +0900 Subject: [PATCH] =?UTF-8?q?BLE=20fix:=20=EB=B0=B0=ED=84=B0=EB=A6=AC=20poll?= =?UTF-8?q?ing=20=EC=8B=A4=EC=A0=9C=EB=A1=9C=20polling=20=E2=80=94=20?= =?UTF-8?q?=EC=B2=AB=20=EC=9D=91=EB=8B=B5=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?+=2030s=20=EC=A3=BC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: 연결 직후 가끔 배터리 값이 0%로 남아 표시 안 됨 / 의미 없는 값. 원인: startBatteryPolling()이 이름과 달리 연결 시 1회 msn? 송신만 함. 그 한 번이 priority HIGH 협상 직전 큐로 들어가거나, 직후 mtb?/maa? 측정 루프와 큐 경합으로 손실되면 영원히 batteryLevel.value = 0 으로 남음. 이후 측정 응답에 배터리가 안 들어있어 갱신 기회 없음. 수정: 세 단계 보장 구조로 변경 1. 즉시 1회 송신 (기존 동작 유지) 2. 3s 뒤 batteryLevel == 0 이면 최대 2회 재송신 (첫 응답 누락 보강) 3. 30s 주기 정상 polling 추가 (충전 반영 + 장기 추적) stopBatteryPolling()도 retry/poll 양쪽 타이머 정리하도록 확장. 끊김 시 isConnected.value == false이면 콜백에서 조기 return하여 좀비 송신 방지. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../medilightv2android/ble/BleManager.kt | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt index a006fac..523c1a2 100644 --- a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt +++ b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt @@ -58,6 +58,10 @@ class BleManager private constructor(private val context: Context) { val debugLogger = com.example.medilightv2android.services.BleDebugLogger.getInstance(context) private var scanTimer: Runnable? = null private var batteryTimer: Runnable? = null + private var batteryRetryTimer: Runnable? = null + private val BATTERY_POLL_INTERVAL_MS = 30_000L // 정상 polling 간격 + private val BATTERY_RETRY_DELAY_MS = 3_000L // 첫 응답 재시도 지연 + private val BATTERY_RETRY_MAX = 2 // 최대 재시도 횟수 private var connectionTimer: Runnable? = null private var isUserDisconnect = false var lastConnectedAddress: String? = null @@ -514,13 +518,54 @@ class BleManager private constructor(private val context: Context) { return true } + /** + * 배터리 polling 시작. + * + * 이전엔 "1회 송신만" 이었고, 그 한 번이 큐 경합/타이밍 문제로 손실되면 배터리가 + * 영원히 0%로 표시되는 이슈가 있었음. 이제 세 단계로 보장: + * 1. 즉시 1회 송신 (best case) + * 2. 3초 내 응답 없으면(batteryLevel.value == 0) 최대 2회 재송신 (첫 응답 누락 보강) + * 3. 30초 주기 정상 polling (충전 반영 + 장기 추적) + * + * BLE 끊김 / 측정 시작 시 stopBatteryPolling()으로 정리. + */ fun startBatteryPolling() { - sendBatteryQuery() // 연결 시 1회만 + stopBatteryPolling() // 기존 타이머 안전 정리 + sendBatteryQuery() // 1단계: 즉시 1회 + + // 2단계: 첫 응답 재시도 — batteryLevel.value가 0인 동안 재송신 + var retries = 0 + val retryRunnable = object : Runnable { + override fun run() { + if (!isConnected.value) return + if (batteryLevel.value == 0 && retries < BATTERY_RETRY_MAX) { + retries++ + logd { "battery query retry #$retries (no response yet)" } + sendBatteryQuery() + handler.postDelayed(this, BATTERY_RETRY_DELAY_MS) + } + } + } + batteryRetryTimer = retryRunnable + handler.postDelayed(retryRunnable, BATTERY_RETRY_DELAY_MS) + + // 3단계: 30초 주기 polling + val pollRunnable = object : Runnable { + override fun run() { + if (!isConnected.value) return + sendBatteryQuery() + handler.postDelayed(this, BATTERY_POLL_INTERVAL_MS) + } + } + batteryTimer = pollRunnable + handler.postDelayed(pollRunnable, BATTERY_POLL_INTERVAL_MS) } fun stopBatteryPolling() { batteryTimer?.let { handler.removeCallbacks(it) } batteryTimer = null + batteryRetryTimer?.let { handler.removeCallbacks(it) } + batteryRetryTimer = null } // ── Watchdog (zombie connection detection, runs on separate thread) ──