mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-01 22:19:18 +02:00
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:
@@ -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 ~30–50 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 ~30–50 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
|
||||
|
||||
+29
-9
@@ -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 2–5 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 ---
|
||||
|
||||
+10
-11
@@ -38,8 +38,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -48,6 +46,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
@@ -136,17 +135,17 @@ fun ConnectionsScreen(
|
||||
onDenied = { scanModel.stopNetworkScan() },
|
||||
)
|
||||
|
||||
// Auto-start BLE scan on screen entry when the user has previously opted in. Stop on screen exit to save battery.
|
||||
// We use `Unit` as the key so the effect is stable across recompositions — the toggle button manages scan
|
||||
// start/stop directly and must not be interrupted by this effect re-firing when `bleAutoScan` changes.
|
||||
// A LaunchedEffect watches bleAutoScan separately to handle the DataStore delivering `true` after the initial
|
||||
// `false` (disk read latency), without triggering a dispose/restart cycle that kills an in-progress scan.
|
||||
LaunchedEffect(bleAutoScan) { if (bleAutoScan && !scanModel.isBleScanning.value) scanModel.startBleScan() }
|
||||
DisposableEffect(Unit) { onDispose { scanModel.stopBleScan() } }
|
||||
// Auto-start BLE scan when the screen is visible (lifecycle ≥ STARTED) and the user has previously opted in.
|
||||
// LifecycleStartEffect stops scanning on ON_STOP (app backgrounded) and restarts on ON_START — preventing
|
||||
// continuous background BLE radio usage that drains the battery.
|
||||
LifecycleStartEffect(bleAutoScan) {
|
||||
if (bleAutoScan && !scanModel.isBleScanning.value) scanModel.startBleScan()
|
||||
onStopOrDispose { scanModel.stopBleScan() }
|
||||
}
|
||||
|
||||
DisposableEffect(networkAutoScan, localNetworkPermissionGranted) {
|
||||
LifecycleStartEffect(networkAutoScan, localNetworkPermissionGranted) {
|
||||
if (networkAutoScan && localNetworkPermissionGranted) scanModel.startNetworkScan()
|
||||
onDispose { scanModel.stopNetworkScan() }
|
||||
onStopOrDispose { scanModel.stopNetworkScan() }
|
||||
}
|
||||
|
||||
/* Animate waiting for the configurations */
|
||||
|
||||
Reference in New Issue
Block a user