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 083d055..f6c30ee 100644 --- a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt +++ b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt @@ -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() @@ -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) - startBatteryPolling() - startRssiPolling() + // Watchdog 은 즉시 시작 (freeze 감지 필요). startWatchdog() + // 2026-08-04: 첫 TX 를 INITIAL_TX_DELAY_MS 지연. + // R100 등 신규 HW 는 boot 후 응답까지 여러 초 걸림 → 즉시 발사 시 + // 초기 msn/mid 가 CMDQ timeout 반복 → 사용자에게 노출. fwRetryCount = 0 - sendDeviceInfoQuery() - // 4초 후에도 펌웨어 미수신이면 mfv? fallback (구 펌웨어는 mid? 미지원 가능성). - // 2026-07-07 fix: disconnect 시 취소되도록 Runnable 참조 유지. - 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() + handler.postDelayed({ + if (!isConnected.value) return@postDelayed + startBatteryPolling() + startRssiPolling() + sendDeviceInfoQuery() + // 4초 후에도 펌웨어 미수신이면 mfv? fallback. + fwFallbackTimer?.let { handler.removeCallbacks(it) } + val fwFallback = Runnable { + if (firmwareVersion.value.isEmpty() && isConnected.value) { + debugLogger.info("FW fallback: sending mfv?") + sendFirmwareVersionQuery() + scheduleFirmwareRetry() + } } - } - fwFallbackTimer = fwFallback - handler.postDelayed(fwFallback, 4_000) - logd { "Service ready! (CCCD written)" } + fwFallbackTimer = fwFallback + handler.postDelayed(fwFallback, 4_000) + }, 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" } diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt index 03fcaa8..53e86d1 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt @@ -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 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 015df0e..52ad2e2 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -323,7 +323,7 @@ 최적의 위치입니다! 위치를 놓쳤습니다. 치골 위에서 다시 시작해주세요. 기기 응답이 지연되고 있습니다. 잠시만 기다려주세요… - 기기가 응답하지 않습니다. 연결을 재시도합니다. + 기기 응답 실패 · 재부팅이 필요할 수 있습니다 (전원 15초 롱프레스). 센서가 분리되었습니다. 치골 위에서 다시 시작해주세요. 신호를 놓쳤습니다 — 위로 올려주세요 ↑ 배꼽 근처에서 시작해 천천히 아래로 내려주세요 ↓ @@ -383,6 +383,10 @@ 이 기기는 다른 앱과 이미 페어링되어 있습니다. 기기 전원 버튼을 15초 롱프레스로 본딩 정보를 삭제한 뒤 다시 시도하세요. 이 폰의 본딩 정보 초기화 본딩 정보 삭제 완료. 다시 연결을 시도하세요. + + 기기가 응답하지 않습니다. 전원 버튼을 15초 롱프레스로 재부팅한 뒤 다시 연결하세요. + + 기기 재연결에 여러 번 실패했습니다. 기기가 켜져 있는지 확인 후 다시 시도하세요. 뒤로 경고 오류 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c06ef3..f936cf9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -323,7 +323,7 @@ Optimal position! Position lost. Restart above pubic bone. Device response delayed. Please wait… - Device not responding. Reconnecting. + Device not responding. A reboot may be required (long-press power 15 seconds). Sensor detached. Restart above pubic bone. Signal lost — slide up ↑ Start near navel, slide down slowly ↓ @@ -383,6 +383,10 @@ This device is already paired with another app. Long-press the device power button for 15 seconds to clear its pairing, then try again. Reset Pairing (this phone) Pairing info removed. Please try connecting again. + + Device stopped responding. Long-press the device power button for 15 seconds to reboot, then reconnect. + + Could not reconnect to device after several attempts. Please check the device is powered on and try again. Back Warning Error