Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ extension AccessoryManager {
Logger.transport.info("🔗👟[Connect] Step 1: connection to \(device.id, privacy: .public)")
do {
let connection: Connection
if let providedConnection = withConnection {
if let providedConnection = withConnection, await providedConnection.isConnected {
connection = providedConnection
} else {
connection = try await transport.connect(to: device)
Expand Down Expand Up @@ -213,6 +213,14 @@ extension AccessoryManager {
do {
try await connectionStepper?.run()
Logger.transport.debug("🔗 [Connect] ConnectionStepper completed.")

// Ensure we're still connected (catches disconnects that happen in the middle of connection steps)
if !(await self.activeConnection?.connection.isConnected ?? false) {
Logger.transport.error("🔗 [Connect] Disconnected during connection steps. Resetting to discovering.")
try await self.closeConnection()
updateState(.discovering)
self.lastConnectionError = AccessoryError.connectionFailed("Disconnected during connection steps")
}
} catch {
Logger.transport.error("🔗 [Connect] Error returned by connectionStepper: \(error)")
try await self.closeConnection()
Expand Down Expand Up @@ -373,6 +381,11 @@ actor SequentialSteps {
func executeWithTimeout<ReturnType>(stepNumber: Int, timeout: Duration, operation: @escaping @Sendable () async throws -> ReturnType) -> Task<ReturnType, Error> {
return Task {
try await withThrowingTaskGroup(of: ReturnType.self) { group -> ReturnType in
defer {
// Always cancel, even if we hit a timeout or an error.
group.cancelAll()
}

group.addTask(operation: operation)
group.addTask {
try await _Concurrency.Task.sleep(for: timeout)
Expand All @@ -381,7 +394,6 @@ actor SequentialSteps {
guard let success = try await group.next() else {
throw SequentialStepError.timeout(stepNumber: stepNumber, afterWaiting: timeout)
}
group.cancelAll()
return success
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,7 @@ extension AccessoryManager {
existing.rssi = newDevice.rssi
self.devices[index] = existing
} else {
// This is a new device, add it to our list if we are in the foreground
if !(self.isInBackground) {
self.devices.append(newDevice)
} else {
Logger.transport.debug("🔎 [Discovery] Found a new device but not in the foreground, not adding to our list: peripheral \(newDevice.name)")
}
self.devices.append(newDevice)
}

if self.shouldAutomaticallyConnectToPreferredPeripheral,
Expand All @@ -75,6 +70,14 @@ extension AccessoryManager {

case .deviceReportedRssi(let deviceId, let newRssi):
updateDevice(deviceId: deviceId, key: \.rssi, value: newRssi)

// If we see RSSI for the preferred device while idle, attempt auto-connect
if self.shouldAutomaticallyConnectToPreferredPeripheral,
UserDefaults.autoconnectOnDiscovery,
!self.isConnected, !self.isConnecting,
deviceId.uuidString == UserDefaults.preferredPeripheralId {
self.connectToPreferredDevice()
}
}
} catch {
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import Foundation
@preconcurrency import CoreBluetooth
#if canImport(UIKit)
import UIKit
#endif
import OSLog
import MeshtasticProtobufs

Expand Down Expand Up @@ -483,24 +486,125 @@ class BLEConnectionDelegate: NSObject, CBPeripheralDelegate {
self.connection = connection
}

// See "Core Bluetooth Background Processing for iOS Apps": https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html
// "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."
// If the CBPeripheralDelegate peripheral(...) call returns, the app may be suspended again.
// Therefore, if we enqueue any work with Task { ... } (so that we can use an async actor), we have to make sure it runs even though the call returns.
// To do this, we use a pattern of UIApplication.shared.beginBackgroundTask and endBackgroundTask to request that the app not be suspended yet until the newly enqueued Task gets a chance to run!

// MARK: CBPeripheralDelegate
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
Task { await connection?.didDiscoverServices(error: error) }
#if canImport(UIKit) && !targetEnvironment(macCatalyst)
let backgroundTaskName = "BLEConnectionDelegate.didDiscoverServices"
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

await connection?.didDiscoverServices(error: error)
}
}

func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
Task { await connection?.didDiscoverCharacteristicsFor(service: service, error: error) }
#if canImport(UIKit) && !targetEnvironment(macCatalyst)
let backgroundTaskName = "BLEConnectionDelegate.didDiscoverCharacteristicsFor"
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

await connection?.didDiscoverCharacteristicsFor(service: service, error: error)
}
}

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
Task { await connection?.didUpdateValueFor(characteristic: characteristic, 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

await connection?.didUpdateValueFor(characteristic: characteristic, error: error)
}
}

func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
Task { await connection?.didWriteValueFor(characteristic: characteristic, error: error) }
#if canImport(UIKit) && !targetEnvironment(macCatalyst)
let backgroundTaskName = "BLEConnectionDelegate.didWriteValueFor"
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

await connection?.didWriteValueFor(characteristic: characteristic, error: error)
}
}

func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
Task { await connection?.didReadRSSI(RSSI: RSSI, error: error) }
#if canImport(UIKit) && !targetEnvironment(macCatalyst)
let backgroundTaskName = "BLEConnectionDelegate.didReadRSSI"
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

await connection?.didReadRSSI(RSSI: RSSI, error: error)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import Foundation
@preconcurrency import CoreBluetooth
#if canImport(UIKit)
import UIKit
#endif
import SwiftUI
import OSLog

Expand Down Expand Up @@ -223,7 +226,26 @@ class BLETransport: Transport {
if let connection = self.activeConnection {
discoveredPeripherals.removeValue(forKey: peripheral.identifier)
discoveredDeviceContinuation?.yield(.deviceLost(peripheral.identifier))

// Use beginBackgroundTask/endBackgroundTask to keep the app from being suspended before the async Task runs. (See BLEConnection for more details.)
#if canImport(UIKit) && !targetEnvironment(macCatalyst)
let backgroundTaskName = "BLETransport.handlePeripheralDisconnect"
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

if await connection.peripheral.identifier == peripheral.identifier {
try await connection.disconnect(withError: AccessoryError.disconnected("BLE connection lost"), shouldReconnect: true)
self.connectionDidDisconnect(fromPeripheral: peripheral)
Expand Down Expand Up @@ -262,7 +284,26 @@ class BLETransport: Transport {
} else if let activeConnection = self.activeConnection {
// Inform the active connection that there was an error and it should disconnect
Logger.transport.debug("🛜 [BLETransport] Error while connecting. Disconnecting the active connection.")

// Use beginBackgroundTask/endBackgroundTask to keep the app from being suspended before the async Task runs. (See BLEConnection for more details.)
#if canImport(UIKit) && !targetEnvironment(macCatalyst)
let backgroundTaskName = "BLETransport.handlePeripheralDisconnectError"
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

try? await activeConnection.disconnect(withError: error, shouldReconnect: shouldReconnect)
self.connectionDidDisconnect(fromPeripheral: peripheral)
}
Expand Down
9 changes: 4 additions & 5 deletions Meshtastic/Helpers/MeshPackets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -880,11 +880,10 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage

let meshActivity = Activity<MeshActivityAttributes>.activities.first(where: { $0.attributes.nodeNum == connectedNode })
if meshActivity != nil {
Task {
// await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration)
await meshActivity?.update(updatedContent)
Logger.services.debug("Updated live activity.")
}
// Synchronously update the Live Activity content, because scheduling in a Task { } may allow the backgrounded app to go back to idle.
// await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration)
await meshActivity?.update(updatedContent)
Logger.services.debug("Updated live activity.")
}
#endif
#endif
Expand Down