From 3bd8669cbe24b47068d0bad5b63bca5312c86f42 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 21 Mar 2026 07:49:03 -0500 Subject: [PATCH] fix: Implement reconnection logic and stabilize BLE connection flow (#4870) --- .../core/network/radio/BleRadioInterface.kt | 4 +- .../desktop/radio/DesktopBleInterface.kt | 75 ++++++++++++------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterface.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterface.kt index af4b9f320..dfe7a07bc 100644 --- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterface.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterface.kt @@ -145,8 +145,6 @@ class BleRadioInterface( private fun connect() { connectionScope.launch { - val device = findDevice() - bleConnection.connectionState .onEach { state -> if (state is BleConnectionState.Disconnected && isFullyConnected) { @@ -171,6 +169,8 @@ class BleRadioInterface( connectionStartTime = nowMillis Logger.i { "[$address] BLE connection attempt started" } + val device = findDevice() + var state = bleConnection.connectAndAwait(device, CONNECTION_TIMEOUT_MS) if (state !is BleConnectionState.Connected) { diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopBleInterface.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopBleInterface.kt index bd2b3dd83..0d7e4a1f2 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopBleInterface.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopBleInterface.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex @@ -142,37 +143,55 @@ class DesktopBleInterface( private fun connect() { connectionScope.launch { - try { - connectionStartTime = nowMillis - Logger.i { "[$address] BLE connection attempt started" } - - bleConnection.connectionState - .onEach { state -> - if (state is BleConnectionState.Disconnected) { - onDisconnected(state) - } + bleConnection.connectionState + .onEach { state -> + if (state is BleConnectionState.Disconnected) { + onDisconnected(state) } - .catch { e -> - Logger.w(e) { "[$address] bleConnection.connectionState flow crashed!" } - handleFailure(e) - } - .launchIn(connectionScope) - - val device = findDevice() - val state = bleConnection.connectAndAwait(device, CONNECTION_TIMEOUT_MS) - if (state !is BleConnectionState.Connected) { - throw RadioNotConnectedException("Failed to connect to device at address $address") } + .catch { e -> + Logger.w(e) { "[$address] bleConnection.connectionState flow crashed!" } + handleFailure(e) + } + .launchIn(connectionScope) - onConnected() - discoverServicesAndSetupCharacteristics() - } catch (e: kotlinx.coroutines.CancellationException) { - Logger.d { "[$address] BLE connection coroutine cancelled" } - throw e - } catch (e: Exception) { - val failureTime = nowMillis - connectionStartTime - Logger.w(e) { "[$address] Failed to connect to device after ${failureTime}ms" } - handleFailure(e) + while (isActive) { + try { + // Add a delay to allow any pending background disconnects (from a previous close() call) + // to complete before we attempt a new connection. + @Suppress("MagicNumber") + val connectDelayMs = 1000L + kotlinx.coroutines.delay(connectDelayMs) + + connectionStartTime = nowMillis + Logger.i { "[$address] BLE connection attempt started" } + + val device = findDevice() + + val state = bleConnection.connectAndAwait(device, CONNECTION_TIMEOUT_MS) + if (state !is BleConnectionState.Connected) { + throw RadioNotConnectedException("Failed to connect to device at address $address") + } + + onConnected() + discoverServicesAndSetupCharacteristics() + + // Suspend here until Kable drops the connection + bleConnection.connectionState.first { it is BleConnectionState.Disconnected } + + Logger.i { "[$address] BLE connection dropped, preparing to reconnect..." } + } catch (e: kotlinx.coroutines.CancellationException) { + Logger.d { "[$address] BLE connection coroutine cancelled" } + throw e + } catch (e: Exception) { + val failureTime = nowMillis - connectionStartTime + Logger.w(e) { "[$address] Failed to connect to device after ${failureTime}ms" } + handleFailure(e) + + // Wait before retrying to prevent hot loops + @Suppress("MagicNumber") + kotlinx.coroutines.delay(5000L) + } } } }