Skip to content

Conversation

@compumike
Copy link
Contributor

@garthvh I think this may resolve #1536 (comment) .

I think what's going on is that, per the Core Bluetooth Programming Guide:

In addition, the system wakes up your app when any of the CBCentralManagerDelegate or CBPeripheralDelegate delegate methods are invoked, allowing your app to handle important central role events, such as when a connection is established or torn down, when a peripheral sends updated characteristic values, and when a central manager’s state changes.

[...]

Apps woken up for any Bluetooth-related events should process them and return as quickly as possible so that the app can be suspended again.

[...]

Ideally, it should complete the task as fast as possible and allow itself to be suspended again.

My guess is that wrapping await meshActivity?.update(updatedContent) in Task { ... } 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 BLEConnectionDelegate handlers all going through Task { ... } 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!

@compumike compumike marked this pull request as draft January 8, 2026 00:20
@compumike
Copy link
Contributor Author

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:

  • If we use Task { ... } within any CBPeripheralDelegate or CBCentralManagerDelegate callbacks, we need to use beginBackgroundTask/endBackgroundTask to keep the app from being suspended before the async Task runs.
  • Use .deviceReportedRssi to trigger connectToPreferredDevice()
  • Fix SequentialSteps to always group.cancelAll() to avoid resuming a continuation twice.
  • Add some checks to connection steps to make sure we're actually still connected between steps.

There are a few more edges I'd still like to look at (making background restoration stable; handling wantConfig=false, wantDatabase=false cases with some one-off bidirectional heartbeat during setup steps). But I may wait until #1554 since that's making changes in similar areas! 😄

@compumike compumike changed the title Update Live Activity synchronously, rather than enqueueing a Task, for background/idle/suspended app state updates BLE handling & Live Activity update changes (wrapping Task with beginBackgroundTask) for more reliable background/idle/suspended app behavior Jan 8, 2026
@garthvh
Copy link
Member

garthvh commented Jan 9, 2026

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 garthvh closed this Jan 9, 2026
@compumike
Copy link
Contributor Author

@garthvh I think there may be confusion because BGTaskScheduler sounds similar to beginBackgroundTask, but I'm not talking about BGTaskScheduler at all.


My logic is:

  1. Core Bluetooth, in the background, calls a CBPeripheralDelegate or CBCentralManagerDelegate method. As soon as that delegate method returns, the backgrounded app may be suspended again.
  2. Calling Task { ... } captures the closure and enqueues the task, and then returns immediately, possibly before the Task has started or finished.
  3. Therefore, if CBPeripheralDelegate handler method enqueues a Task, there's no guarantee that the work will be started or finished before the backgrounded app is suspended again.

The typical way that CBPeripheralDelegate handler methods work is to synchronously do the work:

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
	doUpdate(peripheral, characteristic)
}

This is fine because the work is finished by the time the didUpdateValueFor method returns.


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 Task may not have started or finished before the didUpdateValueFor method returns, so the app may be suspended again. (A lot of "may" -- not deterministic, just flaky behavior.)


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 Task completes, using beginBackgroundTask https://developer.apple.com/documentation/uikit/uiapplication/beginbackgroundtask(expirationhandler:) .

If you notice flaky BLE background behavior, this workaround may help! 😄

@garthvh
Copy link
Member

garthvh commented Jan 9, 2026

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.

@garthvh
Copy link
Member

garthvh commented Jan 9, 2026

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

  1. High Frequency & Overhead

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.

  1. The "Background Execution" Fallacy

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.

  1. Race Conditions with Task

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.

  1. Main Thread Dependency

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants