fix(ble): stop BLE scan on background and downgrade connection priority (#5644)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-28 12:16:19 -07:00
committed by GitHub
parent 672b77b4e1
commit 1d221e2dde
8 changed files with 65 additions and 20 deletions
@@ -79,3 +79,10 @@ internal actual fun Peripheral.requestHighConnectionPriority(): Boolean {
.onFailure { Logger.w(it) { "requestConnectionPriority(High) threw" } }
.getOrDefault(false)
}
internal actual fun Peripheral.requestBalancedConnectionPriority(): Boolean {
val androidPeripheral = this as? AndroidPeripheral ?: return false
return runCatching { androidPeripheral.requestConnectionPriority(AndroidPeripheral.Priority.Balanced) }
.onFailure { Logger.w(it) { "requestConnectionPriority(Balanced) threw" } }
.getOrDefault(false)
}
@@ -78,6 +78,13 @@ interface BleConnection {
* Default implementation returns `false` for platforms that don't support it.
*/
fun requestHighConnectionPriority(): Boolean = false
/**
* Requests the platform to return to balanced BLE connection priority (default ~3050 ms interval). Call after
* latency-sensitive operations (initial config drain, DFU) to reduce ongoing battery draw. Default implementation
* returns `false` for platforms that don't support it.
*/
fun requestBalancedConnectionPriority(): Boolean = false
}
/** Represents a BLE service for commonMain. */
@@ -245,6 +245,8 @@ class KableBleConnection(private val scope: CoroutineScope, private val loggingC
override fun requestHighConnectionPriority(): Boolean = peripheral?.requestHighConnectionPriority() == true
override fun requestBalancedConnectionPriority(): Boolean = peripheral?.requestBalancedConnectionPriority() == true
/** Ensures the previous peripheral's GATT resources are fully released. */
private suspend fun cleanUpPeripheral(tag: String) {
withContext(NonCancellable) { safeClosePeripheral(tag) }
@@ -38,3 +38,9 @@ internal expect fun Peripheral.negotiatedMaxWriteLength(): Int?
* no-op returning `false`. Used by latency-sensitive flows such as DFU firmware streaming.
*/
internal expect fun Peripheral.requestHighConnectionPriority(): Boolean
/**
* Requests balanced BLE connection priority (default ~3050 ms interval) to reduce battery draw after latency-sensitive
* operations complete. On platforms without an equivalent API (JVM/iOS) this is a no-op.
*/
internal expect fun Peripheral.requestBalancedConnectionPriority(): Boolean
@@ -30,3 +30,5 @@ internal actual fun createPeripheral(address: String, builderAction: PeripheralB
internal actual fun Peripheral.negotiatedMaxWriteLength(): Int? = null
internal actual fun Peripheral.requestHighConnectionPriority(): Boolean = false
internal actual fun Peripheral.requestBalancedConnectionPriority(): Boolean = false
@@ -33,4 +33,6 @@ internal actual fun Peripheral.negotiatedMaxWriteLength(): Int? = DEFAULT_JVM_MT
internal actual fun Peripheral.requestHighConnectionPriority(): Boolean = false
internal actual fun Peripheral.requestBalancedConnectionPriority(): Boolean = false
private const val DEFAULT_JVM_MTU = 512
@@ -78,6 +78,16 @@ private val HEARTBEAT_DRAIN_DELAY = 200.milliseconds
private val SCAN_TIMEOUT = 5.seconds
private val GATT_CLEANUP_TIMEOUT = 5.seconds
/**
* Delay after onConnect before downgrading BLE connection priority to Balanced.
*
* The initial config drain (fromRadio burst) typically completes within 25 seconds on most devices, but slower radios
* (ESP32 with large node databases, many channels, or dense position history) can take significantly longer. 30 seconds
* provides generous margin while still ensuring we don't sustain the 7.5 ms connection interval indefinitely, which
* significantly increases battery draw on both the phone and the radio.
*/
private val PRIORITY_DOWNGRADE_DELAY = 30.seconds
/**
* A [RadioTransport] implementation for BLE devices using the common BLE abstractions (which are powered by Kable).
*
@@ -361,15 +371,7 @@ class BleRadioTransport(
val maxLen = bleConnection.maximumWriteValueLength(BleWriteType.WITHOUT_RESPONSE)
Logger.i { "[$address] BLE Radio Session Ready. Max write length (WITHOUT_RESPONSE): $maxLen bytes" }
// Ask the platform for a low-latency / high-throughput connection interval
// (~7.5 ms on Android). The Meshtastic firmware happily accepts this and it
// materially speeds up the initial config drain and any bulk fromRadio reads.
if (bleConnection.requestHighConnectionPriority()) {
Logger.d { "[$address] Requested high BLE connection priority" }
// Wait for the connection parameter update to succeed before starting the heavy traffic
// in onConnect(). Otherwise, the Android BLE stack may disconnect with GATT 147.
delay(1.seconds)
}
requestHighPriorityAndScheduleDowngrade()
this@BleRadioTransport.callback.onConnect()
}
@@ -398,6 +400,24 @@ class BleRadioTransport(
}
}
/**
* Requests high BLE connection priority for the initial config burst, then schedules a downgrade to balanced
* priority after [PRIORITY_DOWNGRADE_DELAY] to conserve battery.
*/
private suspend fun CoroutineScope.requestHighPriorityAndScheduleDowngrade() {
if (bleConnection.requestHighConnectionPriority()) {
Logger.d { "[$address] Requested high BLE connection priority" }
// Wait for the connection parameter update before starting heavy traffic.
delay(1.seconds)
}
launch {
delay(PRIORITY_DOWNGRADE_DELAY)
if (bleConnection.requestBalancedConnectionPriority()) {
Logger.d { "[$address] Downgraded to balanced BLE connection priority" }
}
}
}
@Volatile private var radioService: MeshtasticRadioProfile? = null
// --- RadioTransport Implementation ---