-
-
Notifications
You must be signed in to change notification settings - Fork 198
BLE handling & Live Activity update changes (wrapping Task with beginBackgroundTask) for more reliable background/idle/suspended app behavior
#1548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…r background/idle app state updates
…o request CBPeripheralDelegate handler Tasks get a chance to run before the app is suspended again
…isconnectError Task from background suspend similarly
…dTask / expirationHandler
…nected after steps finish
|
Hi @garthvh I've had quite stable connections and good reconnect behavior while running the previous 7 commits over the past day! I think there is a bit more to do in this area, but this is a start. I see you've gone a different direction with #1554 but I think these ideas are compatible conceptually. If you end up merging that one, I think these ideas can be refactored to be applied on top. The big ideas in this PR so far are:
There are a few more edges I'd still like to look at (making background restoration stable; handling |
Task with beginBackgroundTask) for more reliable background/idle/suspended app behavior
|
Those are a different type of background task that meshtastic does not use, the beginBackgroundTask/endBackgroundTask stuff is for web connected apps. This issue shows a bunch of similar stuff that is not related to meshtastic #1402 |
|
@garthvh I think there may be confusion because My logic is:
The typical way that func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
doUpdate(peripheral, characteristic)
}This is fine because the work is finished by the time the The way the current code works is to enqueue a Task: func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
Task {
doUpdate(peripheral, characteristic)
}
}This is potentially problematic because the My suggested workaround is: func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
#if canImport(UIKit) && !targetEnvironment(macCatalyst)
let backgroundTaskName = "BLEConnectionDelegate.didUpdateValueFor"
let backgroundTaskId = UIApplication.shared.beginBackgroundTask(withName: backgroundTaskName) {
Logger.transport.error("[BGTask] expiration reached: \(backgroundTaskName)")
}
// Logger.transport.debug("[BGTask] started: \(backgroundTaskName)")
#endif
Task {
#if canImport(UIKit) && !targetEnvironment(macCatalyst)
defer {
DispatchQueue.main.async {
// Logger.transport.debug("[BGTask] finished: \(backgroundTaskName)")
UIApplication.shared.endBackgroundTask(backgroundTaskId)
}
}
#endif
doUpdate(peripheral, characteristic)
}
}This workaround requests a short-lived exception to being suspended, just until the If you notice flaky BLE background behavior, this workaround may help! 😄 |
|
I am not seeing any flaky ble background behavior, we have specific crashes related to concurrency. I have tried both of these approaches and they don't help with the things meshtastic has issues with. |
|
In short: It is generally not a good idea to wrap every didUpdateValueFor call in a beginBackgroundTask. While your intention is to ensure the app doesn't get suspended while processing data, this approach introduces significant overhead and architectural risks. Why this is problematic
didUpdateValueFor can be called dozens of times per second (e.g., if you are streaming sensor data or heart rate). Starting and ending a UIBackgroundTask that frequently is expensive for the OS and can lead to performance bottlenecks or even system-level throttling.
Core Bluetooth has its own built-in mechanisms for background execution. If you have the bluetooth-central background mode enabled: iOS will wake your app automatically when a value is updated. The OS grants you a small slice of execution time (around 10 seconds) specifically to handle that delegate callback. You don't need a UIBackgroundTask to "stay alive" for the immediate processing of that packet; the system has already provided the window.
By wrapping doUpdate in a Task, you are moving the work to a background thread. Since Task is asynchronous, there is no guarantee of execution order. If your BLE device sends "Packet A" then "Packet B," a Task might finish processing "Packet B" before "Packet A" is done, leading to data corruption or UI inconsistencies.
Calling UIApplication.shared.endBackgroundTask inside DispatchQueue.main.async adds further latency. If the main thread is busy, your background task might expire before the "end" call even executes, leading to the "expiration reached" log and potentially a crash if the system thinks you are leaking tasks. |
@garthvh I think this may resolve #1536 (comment) .
I think what's going on is that, per the Core Bluetooth Programming Guide:
My guess is that wrapping
await meshActivity?.update(updatedContent)inTask { ... }enqueues the task, but the app gets suspended before that task runs, so the update never gets pushed.There may be a similar issue with
BLEConnectionDelegatehandlers all going throughTask { ... }but that's a harder one to wrap my head around right now.At least for my testing on iOS 26.2, I've had consistent Live Activity updates for over an hour without opening the app. Curious to see if that works for you as well!