feat: BLE 자동 reconnect + 배터리 폴링 5초 (keep-alive)
- BleManager: 배터리 폴링 30초→5초 (펌웨어 15초 타임아웃 방지, iOS 동일) - BleManager: 자동 reconnect with 백오프 (3s→5s→10s, 최대 5회) - scheduleAutoReconnect() + attemptAutoReconnect() - GATT 에러 시에도 백오프 재시도 - 수동 disconnect 시 자동 reconnect 취소 - PiezoMonitoringView: 끊김 시 즉시 파란 배너(RECONNECTING) - 성공 시 배너 자동 숨김 + 배터리 폴링 재시작 - 5회 실패 시 빨간 배너 + 수동 Reconnect 버튼 - 측정 중 끊김 → isMeasuring 리셋 + 자동측정 중지 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,12 @@ class BleManager private constructor(private val context: Context) {
|
||||
private var batteryTimer: Runnable? = null
|
||||
private var connectionTimer: Runnable? = null
|
||||
private var isUserDisconnect = false
|
||||
var lastConnectedAddress: String? = null
|
||||
private set
|
||||
private var reconnectAttempt = 0
|
||||
private val maxReconnectAttempts = 5
|
||||
private val reconnectDelays = longArrayOf(3000, 5000, 10000, 10000, 10000)
|
||||
private var reconnectTimer: Runnable? = null
|
||||
|
||||
val discoveredDevices = mutableStateListOf<BleDevice>()
|
||||
val isScanning = mutableStateOf(false)
|
||||
@@ -212,6 +218,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
|
||||
fun disconnect() {
|
||||
isUserDisconnect = true
|
||||
cancelAutoReconnect()
|
||||
connectionTimer?.let { handler.removeCallbacks(it) }
|
||||
connectionTimer = null
|
||||
bluetoothGatt?.disconnect()
|
||||
@@ -321,16 +328,15 @@ class BleManager private constructor(private val context: Context) {
|
||||
|
||||
fun startBatteryPolling() {
|
||||
batteryTimer?.let { handler.removeCallbacks(it) }
|
||||
// Query immediately, then every 30s
|
||||
sendBatteryQuery()
|
||||
val poll = object : Runnable {
|
||||
override fun run() {
|
||||
sendBatteryQuery()
|
||||
handler.postDelayed(this, 30000)
|
||||
handler.postDelayed(this, 5000)
|
||||
}
|
||||
}
|
||||
batteryTimer = poll
|
||||
handler.postDelayed(poll, 30000)
|
||||
handler.postDelayed(poll, 5000)
|
||||
}
|
||||
|
||||
fun stopBatteryPolling() {
|
||||
@@ -338,6 +344,57 @@ class BleManager private constructor(private val context: Context) {
|
||||
batteryTimer = null
|
||||
}
|
||||
|
||||
// ── Auto Reconnect ──
|
||||
|
||||
private fun scheduleAutoReconnect() {
|
||||
if (reconnectAttempt >= maxReconnectAttempts) {
|
||||
logw { "Max reconnect attempts ($maxReconnectAttempts) reached" }
|
||||
isReconnecting.value = false
|
||||
onReconnectionFailed?.invoke()
|
||||
return
|
||||
}
|
||||
|
||||
val delay = reconnectDelays[reconnectAttempt.coerceAtMost(reconnectDelays.size - 1)]
|
||||
logd { "Auto-reconnect attempt ${reconnectAttempt + 1}/$maxReconnectAttempts in ${delay}ms" }
|
||||
isReconnecting.value = true
|
||||
|
||||
cancelReconnectTimer()
|
||||
val timer = Runnable { attemptAutoReconnect() }
|
||||
reconnectTimer = timer
|
||||
handler.postDelayed(timer, delay)
|
||||
}
|
||||
|
||||
private fun attemptAutoReconnect() {
|
||||
val address = lastConnectedAddress ?: return
|
||||
if (bluetoothAdapter?.isEnabled != true) {
|
||||
onReconnectionFailed?.invoke()
|
||||
isReconnecting.value = false
|
||||
return
|
||||
}
|
||||
|
||||
reconnectAttempt++
|
||||
logd { "Attempting auto-reconnect #$reconnectAttempt to $address" }
|
||||
|
||||
bluetoothGatt?.let { gatt ->
|
||||
try { gatt.close() } catch (_: Exception) {}
|
||||
bluetoothGatt = null
|
||||
}
|
||||
|
||||
val device = bluetoothAdapter.getRemoteDevice(address)
|
||||
bluetoothGatt = device.connectGatt(context, true, gattCallback, BluetoothDevice.TRANSPORT_LE)
|
||||
}
|
||||
|
||||
private fun cancelReconnectTimer() {
|
||||
reconnectTimer?.let { handler.removeCallbacks(it) }
|
||||
reconnectTimer = null
|
||||
}
|
||||
|
||||
fun cancelAutoReconnect() {
|
||||
cancelReconnectTimer()
|
||||
reconnectAttempt = 0
|
||||
isReconnecting.value = false
|
||||
}
|
||||
|
||||
private val gattCallback = object : BluetoothGattCallback() {
|
||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||
logd { "onConnectionStateChange: status=$status newState=$newState" }
|
||||
@@ -347,20 +404,24 @@ class BleManager private constructor(private val context: Context) {
|
||||
connectionTimer = null
|
||||
|
||||
if (status != BluetoothGatt.GATT_SUCCESS && newState != BluetoothProfile.STATE_CONNECTED) {
|
||||
// Connection failed with error
|
||||
loge { "GATT connection failed with status: $status" }
|
||||
bluetoothGatt = null
|
||||
txCharacteristic = null
|
||||
rxCharacteristic = null
|
||||
isConnected.value = false
|
||||
isServiceReady.value = false
|
||||
isReconnecting.value = false
|
||||
connectionError.value = "Connection failed (status $status). Try again."
|
||||
|
||||
val idx = discoveredDevices.indexOfFirst { it.address == gatt.device.address }
|
||||
if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnected = false, isConnecting = false)
|
||||
|
||||
gatt.close()
|
||||
|
||||
if (reconnectAttempt > 0 && lastConnectedAddress != null) {
|
||||
scheduleAutoReconnect()
|
||||
} else {
|
||||
isReconnecting.value = false
|
||||
connectionError.value = "Connection failed (status $status). Try again."
|
||||
}
|
||||
return@post
|
||||
}
|
||||
|
||||
@@ -378,7 +439,10 @@ class BleManager private constructor(private val context: Context) {
|
||||
com.example.medilightv2android.managers.PiezoHW.autoDetectPreset(connectedDeviceName.value)
|
||||
logd { "Device preset: ${com.example.medilightv2android.managers.PiezoHW.presetName} (from '${connectedDeviceName.value}')" }
|
||||
|
||||
// Save last connected device address
|
||||
lastConnectedAddress = gatt.device.address
|
||||
reconnectAttempt = 0
|
||||
cancelReconnectTimer()
|
||||
|
||||
try {
|
||||
val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE)
|
||||
prefs.edit().putString("last_device_address", gatt.device.address).apply()
|
||||
@@ -403,8 +467,9 @@ class BleManager private constructor(private val context: Context) {
|
||||
|
||||
onConnectionStateChanged?.invoke(false)
|
||||
|
||||
if (!isUserDisconnect) {
|
||||
if (!isUserDisconnect && lastConnectedAddress != null) {
|
||||
onUnexpectedDisconnect?.invoke()
|
||||
scheduleAutoReconnect()
|
||||
}
|
||||
isUserDisconnect = false
|
||||
gatt.close()
|
||||
|
||||
Reference in New Issue
Block a user