From d78e104cdd9379247a85d24c84c67cf0e0a48331 Mon Sep 17 00:00:00 2001 From: Adam Hamel Date: Thu, 16 May 2024 11:05:10 -0400 Subject: [PATCH 1/6] Expose raw BLE adv data and rssi outside of the package It is important to expose the adv data and RSSI outside of the library for the discovered peripherals. This information contains critical data in the adv data that will be needed for implementors of this library --- .../CentralManager/CentralManager+async.swift | 6 +++--- .../CentralManager/CentralManager+callback.swift | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift index d9b8c21..0ea1aff 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift @@ -44,13 +44,13 @@ public extension CentralManager { // This method doesn't need to be marked async, but it prevents a signature collision @available(iOS 13, macOS 10.15, watchOS 6.0, tvOS 13.0, *) - func scanForPeripherals(withServices services: [CBUUID]? = nil, timeout: TimeInterval? = nil, options: [String: Any]? = nil) async -> AsyncStream { + func scanForPeripherals(withServices services: [CBUUID]? = nil, timeout: TimeInterval? = nil, options: [String: Any]? = nil) async -> AsyncStream { .init { cont in var timer: Timer? let subscription = eventSubscriptions.queue { event, done in switch event { - case .discovered(let peripheral, _, _): - cont.yield(peripheral) + case .discovered(let peripheral, let advData, let rssi): + cont.yield(ScanResult(peripheral: peripheral, advertisementData: advData, rssi: rssi)) case .stopScan: done() cont.finish() diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift index 7fd8c0a..581df9a 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift @@ -1,6 +1,12 @@ import Foundation import CoreBluetooth +public struct ScanResult { + let peripheral: Peripheral + let advertisementData: [String: Any] + let rssi: NSNumber +} + public extension CentralManager { func waitUntilReady(completionHandler: @escaping (Result) -> Void) { eventQueue.async { [self] in @@ -78,13 +84,13 @@ public extension CentralManager { } } - func scanForPeripherals(withServices services: [CBUUID]? = nil, timeout: TimeInterval? = nil, options: [String: Any]? = nil, onPeripheralFound: @escaping (Peripheral) -> Void) -> CancellableTask { + func scanForPeripherals(withServices services: [CBUUID]? = nil, timeout: TimeInterval? = nil, options: [String: Any]? = nil, onPeripheralFound: @escaping (ScanResult) -> Void) -> CancellableTask { eventQueue.sync { var timer: Timer? let subscription = eventSubscriptions.queue { event, done in switch event { - case .discovered(let peripheral, _, _): - onPeripheralFound(peripheral) + case .discovered(let peripheral, let advData, let rssi): + onPeripheralFound(ScanResult(peripheral: peripheral, advertisementData: advData, rssi: rssi)) case .stopScan: done() default: From ca482ce4cd13b276cc18bfad17db276754b6b998 Mon Sep 17 00:00:00 2001 From: Adam Hamel Date: Thu, 16 May 2024 11:20:04 -0400 Subject: [PATCH 2/6] Use more specific name for the scan result to avoid collisions with implementors of this library use PeripheralScanResult --- .../CentralManager/CentralManager+async.swift | 4 ++-- .../CentralManager/CentralManager+callback.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift index 0ea1aff..6b1308d 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift @@ -44,13 +44,13 @@ public extension CentralManager { // This method doesn't need to be marked async, but it prevents a signature collision @available(iOS 13, macOS 10.15, watchOS 6.0, tvOS 13.0, *) - func scanForPeripherals(withServices services: [CBUUID]? = nil, timeout: TimeInterval? = nil, options: [String: Any]? = nil) async -> AsyncStream { + func scanForPeripherals(withServices services: [CBUUID]? = nil, timeout: TimeInterval? = nil, options: [String: Any]? = nil) async -> AsyncStream { .init { cont in var timer: Timer? let subscription = eventSubscriptions.queue { event, done in switch event { case .discovered(let peripheral, let advData, let rssi): - cont.yield(ScanResult(peripheral: peripheral, advertisementData: advData, rssi: rssi)) + cont.yield(PeripheralScanResult(peripheral: peripheral, advertisementData: advData, rssi: rssi)) case .stopScan: done() cont.finish() diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift index 581df9a..6d0679e 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift @@ -1,7 +1,7 @@ import Foundation import CoreBluetooth -public struct ScanResult { +public struct PeripheralScanResult { let peripheral: Peripheral let advertisementData: [String: Any] let rssi: NSNumber @@ -84,13 +84,13 @@ public extension CentralManager { } } - func scanForPeripherals(withServices services: [CBUUID]? = nil, timeout: TimeInterval? = nil, options: [String: Any]? = nil, onPeripheralFound: @escaping (ScanResult) -> Void) -> CancellableTask { + func scanForPeripherals(withServices services: [CBUUID]? = nil, timeout: TimeInterval? = nil, options: [String: Any]? = nil, onPeripheralFound: @escaping (PeripheralScanResult) -> Void) -> CancellableTask { eventQueue.sync { var timer: Timer? let subscription = eventSubscriptions.queue { event, done in switch event { case .discovered(let peripheral, let advData, let rssi): - onPeripheralFound(ScanResult(peripheral: peripheral, advertisementData: advData, rssi: rssi)) + onPeripheralFound(PeripheralScanResult(peripheral: peripheral, advertisementData: advData, rssi: rssi)) case .stopScan: done() default: From 6be115de851f236b2b1606324125661fc227755b Mon Sep 17 00:00:00 2001 From: Adam Hamel Date: Thu, 16 May 2024 11:28:57 -0400 Subject: [PATCH 3/6] Make variables public --- .../CentralManager/CentralManager+callback.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift index 6d0679e..3957277 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift @@ -2,9 +2,9 @@ import Foundation import CoreBluetooth public struct PeripheralScanResult { - let peripheral: Peripheral - let advertisementData: [String: Any] - let rssi: NSNumber + public let peripheral: Peripheral + public let advertisementData: [String: Any] + public let rssi: NSNumber } public extension CentralManager { From 637a1963a6c10224a0031cb384e9a4c26828a6bc Mon Sep 17 00:00:00 2001 From: Adam Hamel Date: Tue, 18 Jun 2024 13:36:17 -0400 Subject: [PATCH 4/6] Allow waitUntilReady to pass a timeout --- .../CentralManager/CentralManager+async.swift | 4 ++-- .../CentralManager+callback.swift | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift index 6b1308d..982901a 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift @@ -3,9 +3,9 @@ import CoreBluetooth public extension CentralManager { @available(iOS 13, macOS 10.15, watchOS 6.0, tvOS 13.0, *) - func waitUntilReady() async throws { + func waitUntilReady(timeout: TimeInterval) async throws { try await withCheckedThrowingContinuation { cont in - self.waitUntilReady { result in + self.waitUntilReady(timeout: timeout) { result in cont.resume(with: result) } } diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift index 3957277..df84d5e 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift @@ -8,7 +8,7 @@ public struct PeripheralScanResult { } public extension CentralManager { - func waitUntilReady(completionHandler: @escaping (Result) -> Void) { + func waitUntilReady(timeout: TimeInterval, completionHandler: @escaping (Result) -> Void) { eventQueue.async { [self] in guard state != .poweredOn else { completionHandler(.success(Void())) @@ -24,8 +24,9 @@ public extension CentralManager { completionHandler(.failure(CentralError.unavailable)) return } - - eventSubscriptions.queue { event, done in + + var timer: Timer? + let task = eventSubscriptions.queue { event, done in guard case .stateUpdated(let state) = event else { return } switch state { @@ -38,9 +39,19 @@ public extension CentralManager { default: return } - + + timer?.invalidate() done() } + + if timeout != .infinity { + let timeoutTimer = Timer(fire: Date() + timeout, interval: 0, repeats: false) { _ in + task.cancel() + completionHandler(.failure(CBError(.connectionTimeout))) + } + timer = timeoutTimer + RunLoop.main.add(timeoutTimer, forMode: .default) + } } } From 80e0031dd3cd7bfd4eb7e863ec69fdc1d44be265 Mon Sep 17 00:00:00 2001 From: Adam Hamel Date: Tue, 18 Jun 2024 13:39:03 -0400 Subject: [PATCH 5/6] Set default value to be .infinity for wait timeout --- .../SwiftBluetooth/CentralManager/CentralManager+async.swift | 2 +- .../SwiftBluetooth/CentralManager/CentralManager+callback.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift index 982901a..6a7adea 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+async.swift @@ -3,7 +3,7 @@ import CoreBluetooth public extension CentralManager { @available(iOS 13, macOS 10.15, watchOS 6.0, tvOS 13.0, *) - func waitUntilReady(timeout: TimeInterval) async throws { + func waitUntilReady(timeout: TimeInterval = Double.infinity) async throws { try await withCheckedThrowingContinuation { cont in self.waitUntilReady(timeout: timeout) { result in cont.resume(with: result) diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift index df84d5e..9bcc70d 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift @@ -8,7 +8,7 @@ public struct PeripheralScanResult { } public extension CentralManager { - func waitUntilReady(timeout: TimeInterval, completionHandler: @escaping (Result) -> Void) { + func waitUntilReady(timeout: TimeInterval = Double.infinity, completionHandler: @escaping (Result) -> Void) { eventQueue.async { [self] in guard state != .poweredOn else { completionHandler(.success(Void())) From 7b6bf4a07460d89228dce8470398f86cb8a58893 Mon Sep 17 00:00:00 2001 From: Adam Hamel Date: Tue, 18 Jun 2024 13:44:33 -0400 Subject: [PATCH 6/6] Throw correct CentralError.poweredOff error --- .../SwiftBluetooth/CentralManager/CentralManager+callback.swift | 2 +- Sources/SwiftBluetooth/CentralManager/CentralManagerError.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift index 9bcc70d..6b6796b 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManager+callback.swift @@ -47,7 +47,7 @@ public extension CentralManager { if timeout != .infinity { let timeoutTimer = Timer(fire: Date() + timeout, interval: 0, repeats: false) { _ in task.cancel() - completionHandler(.failure(CBError(.connectionTimeout))) + completionHandler(.failure(CentralError.poweredOff)) } timer = timeoutTimer RunLoop.main.add(timeoutTimer, forMode: .default) diff --git a/Sources/SwiftBluetooth/CentralManager/CentralManagerError.swift b/Sources/SwiftBluetooth/CentralManager/CentralManagerError.swift index 6caff7f..ce795e1 100644 --- a/Sources/SwiftBluetooth/CentralManager/CentralManagerError.swift +++ b/Sources/SwiftBluetooth/CentralManager/CentralManagerError.swift @@ -4,4 +4,5 @@ public enum CentralError: Error { case unknown case unauthorized case unavailable + case poweredOff }