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 batteryTimer: Runnable? = null
|
||||||
private var connectionTimer: Runnable? = null
|
private var connectionTimer: Runnable? = null
|
||||||
private var isUserDisconnect = false
|
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 discoveredDevices = mutableStateListOf<BleDevice>()
|
||||||
val isScanning = mutableStateOf(false)
|
val isScanning = mutableStateOf(false)
|
||||||
@@ -212,6 +218,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
|
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
isUserDisconnect = true
|
isUserDisconnect = true
|
||||||
|
cancelAutoReconnect()
|
||||||
connectionTimer?.let { handler.removeCallbacks(it) }
|
connectionTimer?.let { handler.removeCallbacks(it) }
|
||||||
connectionTimer = null
|
connectionTimer = null
|
||||||
bluetoothGatt?.disconnect()
|
bluetoothGatt?.disconnect()
|
||||||
@@ -321,16 +328,15 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
|
|
||||||
fun startBatteryPolling() {
|
fun startBatteryPolling() {
|
||||||
batteryTimer?.let { handler.removeCallbacks(it) }
|
batteryTimer?.let { handler.removeCallbacks(it) }
|
||||||
// Query immediately, then every 30s
|
|
||||||
sendBatteryQuery()
|
sendBatteryQuery()
|
||||||
val poll = object : Runnable {
|
val poll = object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
sendBatteryQuery()
|
sendBatteryQuery()
|
||||||
handler.postDelayed(this, 30000)
|
handler.postDelayed(this, 5000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
batteryTimer = poll
|
batteryTimer = poll
|
||||||
handler.postDelayed(poll, 30000)
|
handler.postDelayed(poll, 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopBatteryPolling() {
|
fun stopBatteryPolling() {
|
||||||
@@ -338,6 +344,57 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
batteryTimer = null
|
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() {
|
private val gattCallback = object : BluetoothGattCallback() {
|
||||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||||
logd { "onConnectionStateChange: status=$status newState=$newState" }
|
logd { "onConnectionStateChange: status=$status newState=$newState" }
|
||||||
@@ -347,20 +404,24 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
connectionTimer = null
|
connectionTimer = null
|
||||||
|
|
||||||
if (status != BluetoothGatt.GATT_SUCCESS && newState != BluetoothProfile.STATE_CONNECTED) {
|
if (status != BluetoothGatt.GATT_SUCCESS && newState != BluetoothProfile.STATE_CONNECTED) {
|
||||||
// Connection failed with error
|
|
||||||
loge { "GATT connection failed with status: $status" }
|
loge { "GATT connection failed with status: $status" }
|
||||||
bluetoothGatt = null
|
bluetoothGatt = null
|
||||||
txCharacteristic = null
|
txCharacteristic = null
|
||||||
rxCharacteristic = null
|
rxCharacteristic = null
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
isServiceReady.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 }
|
val idx = discoveredDevices.indexOfFirst { it.address == gatt.device.address }
|
||||||
if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnected = false, isConnecting = false)
|
if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnected = false, isConnecting = false)
|
||||||
|
|
||||||
gatt.close()
|
gatt.close()
|
||||||
|
|
||||||
|
if (reconnectAttempt > 0 && lastConnectedAddress != null) {
|
||||||
|
scheduleAutoReconnect()
|
||||||
|
} else {
|
||||||
|
isReconnecting.value = false
|
||||||
|
connectionError.value = "Connection failed (status $status). Try again."
|
||||||
|
}
|
||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +439,10 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
com.example.medilightv2android.managers.PiezoHW.autoDetectPreset(connectedDeviceName.value)
|
com.example.medilightv2android.managers.PiezoHW.autoDetectPreset(connectedDeviceName.value)
|
||||||
logd { "Device preset: ${com.example.medilightv2android.managers.PiezoHW.presetName} (from '${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 {
|
try {
|
||||||
val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE)
|
||||||
prefs.edit().putString("last_device_address", gatt.device.address).apply()
|
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)
|
onConnectionStateChanged?.invoke(false)
|
||||||
|
|
||||||
if (!isUserDisconnect) {
|
if (!isUserDisconnect && lastConnectedAddress != null) {
|
||||||
onUnexpectedDisconnect?.invoke()
|
onUnexpectedDisconnect?.invoke()
|
||||||
|
scheduleAutoReconnect()
|
||||||
}
|
}
|
||||||
isUserDisconnect = false
|
isUserDisconnect = false
|
||||||
gatt.close()
|
gatt.close()
|
||||||
|
|||||||
+16
-5
@@ -204,15 +204,27 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup disconnect handling
|
// Setup disconnect handling — auto reconnect integrated
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
bleManager.onUnexpectedDisconnect = {
|
bleManager.onUnexpectedDisconnect = {
|
||||||
reconnectionState = BleReconnectionState.DISCONNECTED
|
reconnectionState = BleReconnectionState.RECONNECTING
|
||||||
showDisconnectionBanner = true
|
showDisconnectionBanner = true
|
||||||
|
if (isMeasuring) {
|
||||||
|
isMeasuring = false
|
||||||
|
measureRetryCount = 0
|
||||||
|
}
|
||||||
|
if (isAutoMeasuring) stopAutoMeasure()
|
||||||
}
|
}
|
||||||
bleManager.onReconnectionFailed = {
|
bleManager.onReconnectionFailed = {
|
||||||
reconnectionState = BleReconnectionState.FAILED
|
reconnectionState = BleReconnectionState.FAILED
|
||||||
}
|
}
|
||||||
|
bleManager.onConnectionStateChanged = { connected ->
|
||||||
|
if (connected && showDisconnectionBanner) {
|
||||||
|
showDisconnectionBanner = false
|
||||||
|
reconnectionState = BleReconnectionState.DISCONNECTED
|
||||||
|
bleManager.startBatteryPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add catheter dialog
|
// Add catheter dialog
|
||||||
@@ -544,9 +556,8 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
state = reconnectionState,
|
state = reconnectionState,
|
||||||
onReconnect = {
|
onReconnect = {
|
||||||
reconnectionState = BleReconnectionState.RECONNECTING
|
reconnectionState = BleReconnectionState.RECONNECTING
|
||||||
bleManager.reconnectToLastDevice(
|
bleManager.cancelAutoReconnect()
|
||||||
com.example.medilightv2android.services.UserStorage.getInstance(context).lastDeviceAddress
|
bleManager.reconnectToLastDevice(bleManager.lastConnectedAddress)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
Reference in New Issue
Block a user