From bff416f5f104e93d74c55acf8e47fe11217e9193 Mon Sep 17 00:00:00 2001 From: Henryforce Date: Thu, 25 Jul 2024 06:23:15 +0900 Subject: [PATCH] Refactor publishers to Deferred where side effects are present (#49) * Updated the methods with side effects to rely on a Deferred Publisher * Made the CBPeripheral to conform to CBPeripheralWrapper --- README.md | 16 +- .../Central/BLECentralManager.swift | 2 +- .../Central/BLECentralManagerDelegate.swift | 19 +- .../Central/CBCentralManagerWrapper.swift | 8 +- .../Central/StandardBLECentralManager.swift | 140 +++++----- .../Characteristic/BLECharacteristic.swift | 3 +- Sources/BLECombineKit/Data/BLEData.swift | 6 +- Sources/BLECombineKit/Error/BLEError.swift | 22 +- .../BLECombineKit/Main/BLECombineKit.swift | 5 +- .../BLEPeripheralManager.swift | 4 +- .../BLEPeripheralManagerDelegateWrapper.swift | 2 +- .../Peripheral/BLEPeripheral+Async.swift | 9 + .../Peripheral/BLEPeripheral.swift | 82 ++++-- .../Peripheral/BLEPeripheralDelegate.swift | 107 ++++++-- .../Peripheral/BLEPeripheralResult.swift | 17 -- .../Peripheral/CBPeripheralWrapper.swift | 109 ++------ .../Peripheral/StandardBLEPeripheral.swift | 257 +++++++----------- .../BLECombineKit/Service/BLEService.swift | 2 +- .../Utils/BLECombineKit+Combine.swift | 27 ++ .../BLECentralManagerTests.swift | 19 ++ Tests/BLECombineKitTests/BLEDataTests.swift | 2 +- .../BLEPeripheralTests.swift | 86 ++---- .../Mocks/BLEPeripheralMocks.swift | 18 +- 23 files changed, 478 insertions(+), 484 deletions(-) delete mode 100644 Sources/BLECombineKit/Peripheral/BLEPeripheralResult.swift create mode 100644 Sources/BLECombineKit/Utils/BLECombineKit+Combine.swift diff --git a/README.md b/README.md index 5fc4049..b57d8f5 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,16 @@ import BLECombineKit let centralManager = BLECombineKit.buildCentralManager(with: CBCentralManager()) let serviceUUID = CBUUID(string: "0x00FF") -// Connect to the first peripheral that matches the given service UUID and observe all the -// characteristics in that service. +let characteristicUUID = CBUUID(string: "0xFF01") +// Connect to the first peripheral that matches the given service UUID and observe a specific +// characteristic in that service. centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil) .first() .flatMap { $0.peripheral.connect(with: nil) } .flatMap { $0.discoverServices(serviceUUIDs: [serviceUUID]) } .flatMap { $0.discoverCharacteristics(characteristicUUIDs: nil) } - .flatMap { $0.observeValue() } + .filter { $0.value.uuid == characteristicUUID } + .flatMap { $0.observeValueUpdateAndSetNotification() } .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { data in @@ -55,14 +57,16 @@ import BLECombineKit let centralManager = BLECombineKit.buildCentralManager(with: CBCentralManager()) let serviceUUID = CBUUID(string: "0x00FF") -// Connect to the first peripheral that matches the given service UUID and observe all the -// characteristics in that service. +let characteristicUUID = CBUUID(string: "0xFF01") +// Connect to the first peripheral that matches the given service UUID and observe a specific +// characteristic in that service. let stream = centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil) .first() .flatMap { $0.peripheral.connect(with: nil) } .flatMap { $0.discoverServices(serviceUUIDs: [serviceUUID]) } .flatMap { $0.discoverCharacteristics(characteristicUUIDs: nil) } - .flatMap { $0.observeValue() } + .filter { $0.value.uuid == characteristicUUID } + .flatMap { $0.observeValueUpdateAndSetNotification() } .values Task { diff --git a/Sources/BLECombineKit/Central/BLECentralManager.swift b/Sources/BLECombineKit/Central/BLECentralManager.swift index f59eab5..6beb176 100644 --- a/Sources/BLECombineKit/Central/BLECentralManager.swift +++ b/Sources/BLECombineKit/Central/BLECentralManager.swift @@ -50,7 +50,7 @@ public protocol BLECentralManager: AnyObject { func cancelPeripheralConnection(_ peripheral: BLEPeripheral) -> AnyPublisher /// Register for any connection events. - #if !os(macOS) + #if os(iOS) || os(tvOS) || os(watchOS) func registerForConnectionEvents(options: [CBConnectionEventMatchingOption: Any]?) #endif diff --git a/Sources/BLECombineKit/Central/BLECentralManagerDelegate.swift b/Sources/BLECombineKit/Central/BLECentralManagerDelegate.swift index 1e4f4e3..15e72ab 100644 --- a/Sources/BLECombineKit/Central/BLECentralManagerDelegate.swift +++ b/Sources/BLECombineKit/Central/BLECentralManagerDelegate.swift @@ -16,14 +16,15 @@ final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate { let didConnectPeripheral = PassthroughSubject() let didDisconnectPeripheral = PassthroughSubject() let didFailToConnect = PassthroughSubject() - let didDiscoverAdvertisementData = PassthroughSubject() + let didDiscoverAdvertisementData = PassthroughSubject< + DidDiscoverAdvertisementDataResult, BLEError + >() let didUpdateState = PassthroughSubject() let willRestoreState = PassthroughSubject<[String: Any], Never>() let didUpdateANCSAuthorization = PassthroughSubject() public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) - didConnectPeripheral.send(peripheralWrapper) + didConnectPeripheral.send(peripheral) } public func centralManager( @@ -31,8 +32,7 @@ final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate { didDisconnectPeripheral peripheral: CBPeripheral, error: Error? ) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) - didDisconnectPeripheral.send(peripheralWrapper) + didDisconnectPeripheral.send(peripheral) } public func centralManager( @@ -40,8 +40,7 @@ final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate { didFailToConnect peripheral: CBPeripheral, error: Error? ) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) - didFailToConnect.send(peripheralWrapper) + didFailToConnect.send(peripheral) } public func centralManager( @@ -50,8 +49,7 @@ final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate { advertisementData: [String: Any], rssi RSSI: NSNumber ) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) - let result = (peripheral: peripheralWrapper, advertisementData: advertisementData, rssi: RSSI) + let result = (peripheral: peripheral, advertisementData: advertisementData, rssi: RSSI) didDiscoverAdvertisementData.send(result) } @@ -72,8 +70,7 @@ final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate { _ central: CBCentralManager, didUpdateANCSAuthorizationFor peripheral: CBPeripheral ) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) - didUpdateANCSAuthorization.send(peripheralWrapper) + didUpdateANCSAuthorization.send(peripheral) } #endif diff --git a/Sources/BLECombineKit/Central/CBCentralManagerWrapper.swift b/Sources/BLECombineKit/Central/CBCentralManagerWrapper.swift index 006c686..ff9ad48 100644 --- a/Sources/BLECombineKit/Central/CBCentralManagerWrapper.swift +++ b/Sources/BLECombineKit/Central/CBCentralManagerWrapper.swift @@ -46,7 +46,6 @@ final class StandardCBCentralManagerWrapper: CBCentralManagerWrapper { func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheralWrapper] { wrappedManager .retrievePeripherals(withIdentifiers: identifiers) - .map { StandardCBPeripheralWrapper(peripheral: $0) } } func retrieveConnectedPeripherals( @@ -54,7 +53,6 @@ final class StandardCBCentralManagerWrapper: CBCentralManagerWrapper { ) -> [CBPeripheralWrapper] { wrappedManager .retrieveConnectedPeripherals(withServices: serviceUUIDs) - .map { StandardCBPeripheralWrapper(peripheral: $0) } } func scanForPeripherals(withServices serviceUUIDs: [CBUUID]?, options: [String: Any]?) { @@ -66,11 +64,13 @@ final class StandardCBCentralManagerWrapper: CBCentralManagerWrapper { } func connect(_ wrappedPeripheral: CBPeripheralWrapper, options: [String: Any]?) { - wrappedManager.connect(wrappedPeripheral.peripheral, options: options) + guard let manager else { return } + wrappedPeripheral.connect(manager: manager) } func cancelPeripheralConnection(_ wrappedPeripheral: CBPeripheralWrapper) { - wrappedManager.cancelPeripheralConnection(wrappedPeripheral.peripheral) + guard let manager else { return } + wrappedPeripheral.cancelConnection(manager: manager) } #if !os(macOS) diff --git a/Sources/BLECombineKit/Central/StandardBLECentralManager.swift b/Sources/BLECombineKit/Central/StandardBLECentralManager.swift index 903c898..9cf19f7 100644 --- a/Sources/BLECombineKit/Central/StandardBLECentralManager.swift +++ b/Sources/BLECombineKit/Central/StandardBLECentralManager.swift @@ -18,7 +18,7 @@ final class StandardBLECentralManager: BLECentralManager { var stateSubject = CurrentValueSubject(ManagerState.unknown) let delegate: BLECentralManagerDelegate - private var cancellables = [AnyCancellable]() + private var cancellables = Set() var isScanning: Bool { associatedCentralManager.isScanning @@ -49,48 +49,7 @@ final class StandardBLECentralManager: BLECentralManager { self.init(centralManager: centralManagerWrapper, managerDelegate: BLECentralManagerDelegate()) } - func observeUpdateState() { - delegate - .didUpdateState - .sink { self.stateSubject.send($0) } - .store(in: &cancellables) - } - - func observeDidConnectPeripheral() { - delegate - .didConnectPeripheral - .sink { [weak self] result in - guard let self = self else { return } - self.peripheralProvider - .provide(for: result, centralManager: self) - .connectionState.send(true) - }.store(in: &cancellables) - } - - func observeDidFailToConnectPeripheral() { - delegate - .didFailToConnect - .ignoreFailure() - .sink { [weak self] result in - guard let self = self else { return } - self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send( - false - ) - }.store(in: &cancellables) - } - - func observeDidDisconnectPeripheral() { - delegate - .didDisconnectPeripheral - .sink { [weak self] result in - guard let self = self else { return } - self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send( - false - ) - }.store(in: &cancellables) - } - - public func retrievePeripherals( + func retrievePeripherals( withIdentifiers identifiers: [UUID] ) -> AnyPublisher { let retrievedPeripherals = associatedCentralManager.retrievePeripherals( @@ -99,7 +58,7 @@ final class StandardBLECentralManager: BLECentralManager { return observePeripherals(from: retrievedPeripherals) } - public func retrieveConnectedPeripherals( + func retrieveConnectedPeripherals( withServices serviceUUIDs: [CBUUID] ) -> AnyPublisher { let retrievedPeripherals = associatedCentralManager.retrieveConnectedPeripherals( @@ -108,36 +67,42 @@ final class StandardBLECentralManager: BLECentralManager { return observePeripherals(from: retrievedPeripherals) } - public func scanForPeripherals( + func scanForPeripherals( withServices services: [CBUUID]?, options: [String: Any]? ) -> AnyPublisher { - associatedCentralManager.scanForPeripherals(withServices: services, options: options) - - return self.delegate + let stream = delegate .didDiscoverAdvertisementData - .tryMap { [weak self] peripheral, advertisementData, rssi in - guard let self else { throw BLEError.deallocated } + .eraseToAnyPublisher() // Erase needed to silence flatMap(maxPublishers) availability. + .flatMap { [weak self] result -> AnyPublisher in + guard let self else { return Fail(error: BLEError.deallocated).eraseToAnyPublisher() } let peripheral = self.peripheralProvider.provide( - for: peripheral, + for: result.peripheral, centralManager: self ) - - return BLEScanResult( - peripheral: peripheral, - advertisementData: advertisementData, - rssi: rssi - ) + return Just( + BLEScanResult( + peripheral: peripheral, + advertisementData: result.advertisementData, + rssi: result.rssi + ) + ).setFailureType(to: BLEError.self).eraseToAnyPublisher() } - .mapError { $0 as? BLEError ?? BLEError.unknown } .eraseToAnyPublisher() + + return Deferred> { [associatedCentralManager] in + defer { + associatedCentralManager.scanForPeripherals(withServices: services, options: options) + } + return stream + }.eraseToAnyPublisher() } - public func stopScan() { + func stopScan() { associatedCentralManager.stopScan() } - public func connect( + func connect( peripheral: BLEPeripheral, options: [String: Any]? ) -> AnyPublisher { @@ -160,13 +125,14 @@ final class StandardBLECentralManager: BLECentralManager { .eraseToAnyPublisher() } - public func cancelPeripheralConnection( + func cancelPeripheralConnection( _ peripheral: BLEPeripheral ) -> AnyPublisher { let associatedPeripheral = peripheral.associatedPeripheral associatedCentralManager.cancelPeripheralConnection(associatedPeripheral) - return delegate.didDisconnectPeripheral + return delegate + .didDisconnectPeripheral .filter { $0.identifier == associatedPeripheral.identifier } .first() .ignoreOutput() @@ -174,17 +140,17 @@ final class StandardBLECentralManager: BLECentralManager { .eraseToAnyPublisher() } - #if !os(macOS) - public func registerForConnectionEvents(options: [CBConnectionEventMatchingOption: Any]?) { + #if os(iOS) || os(tvOS) || os(watchOS) + func registerForConnectionEvents(options: [CBConnectionEventMatchingOption: Any]?) { associatedCentralManager.registerForConnectionEvents(options: options) } #endif - public func observeWillRestoreState() -> AnyPublisher<[String: Any], Never> { + func observeWillRestoreState() -> AnyPublisher<[String: Any], Never> { delegate.willRestoreState.eraseToAnyPublisher() } - public func observeDidUpdateANCSAuthorization() -> AnyPublisher { + func observeDidUpdateANCSAuthorization() -> AnyPublisher { delegate.didUpdateANCSAuthorization .compactMap { [weak self] peripheral in guard let self = self else { return nil } @@ -201,6 +167,48 @@ final class StandardBLECentralManager: BLECentralManager { observeDidDisconnectPeripheral() } + private func observeUpdateState() { + let stateSubject = self.stateSubject + return delegate + .didUpdateState + .sink { stateSubject.send($0) } + .store(in: &cancellables) + } + + private func observeDidConnectPeripheral() { + delegate + .didConnectPeripheral + .sink { [weak self] result in + guard let self = self else { return } + self.peripheralProvider + .provide(for: result, centralManager: self) + .connectionState.send(true) + }.store(in: &cancellables) + } + + private func observeDidFailToConnectPeripheral() { + delegate + .didFailToConnect + .ignoreFailure() + .sink { [weak self] result in + guard let self = self else { return } + self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send( + false + ) + }.store(in: &cancellables) + } + + private func observeDidDisconnectPeripheral() { + delegate + .didDisconnectPeripheral + .sink { [weak self] result in + guard let self = self else { return } + self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send( + false + ) + }.store(in: &cancellables) + } + private func observePeripherals( from retrievedPeripherals: [CBPeripheralWrapper] ) -> AnyPublisher { diff --git a/Sources/BLECombineKit/Characteristic/BLECharacteristic.swift b/Sources/BLECombineKit/Characteristic/BLECharacteristic.swift index 2156361..561fb40 100644 --- a/Sources/BLECombineKit/Characteristic/BLECharacteristic.swift +++ b/Sources/BLECombineKit/Characteristic/BLECharacteristic.swift @@ -8,9 +8,8 @@ import Combine import CoreBluetooth -import Foundation -public struct BLECharacteristic: BLEPeripheralResult { +public struct BLECharacteristic { public let value: CBCharacteristic private let peripheral: BLEPeripheral diff --git a/Sources/BLECombineKit/Data/BLEData.swift b/Sources/BLECombineKit/Data/BLEData.swift index a0880fe..e2b313f 100644 --- a/Sources/BLECombineKit/Data/BLEData.swift +++ b/Sources/BLECombineKit/Data/BLEData.swift @@ -8,13 +8,11 @@ import Foundation -public struct BLEData: BLEPeripheralResult { +public struct BLEData { public let value: Data - public let peripheral: BLEPeripheral - public init(value: Data, peripheral: BLEPeripheral) { + public init(value: Data) { self.value = value - self.peripheral = peripheral } public var floatValue: Float32? { diff --git a/Sources/BLECombineKit/Error/BLEError.swift b/Sources/BLECombineKit/Error/BLEError.swift index f30d702..6973ea1 100644 --- a/Sources/BLECombineKit/Error/BLEError.swift +++ b/Sources/BLECombineKit/Error/BLEError.swift @@ -86,16 +86,28 @@ public enum BLEError: Error, CustomStringConvertible { case invalid case connectionFailure case disconnectionFailed + case didDiscoverDescriptorsError(CoreBluetoothError) + case didUpdateValueForCharacteristicError(CoreBluetoothError) + case didUpdateValueForDescriptorError(CoreBluetoothError) case servicesFoundError(CoreBluetoothError) case characteristicsFoundError(CoreBluetoothError) + case writeError(CoreBluetoothError) + case readRSSIError(CoreBluetoothError) public var description: String { switch self { - case .invalid: return "Invalid" - case .connectionFailure: return "Connection Failure" - case .disconnectionFailed: return "Disconnection Failed" - case .servicesFoundError(let error): return "Services Found Error: \(error)" - case .characteristicsFoundError(let error): return "Characteristics Found Error: \(error)" + case .invalid: "Invalid" + case .connectionFailure: "Connection Failure" + case .disconnectionFailed: "Disconnection Failed" + case .didDiscoverDescriptorsError(let error): "Did Discover Descriptors Error: \(error)" + case .didUpdateValueForCharacteristicError(let error): + "Did Update Value for Characteristic Error: \(error)" + case .didUpdateValueForDescriptorError(let error): + "Did Update Value for Descriptor Error: \(error)" + case .servicesFoundError(let error): "Services Found Error: \(error)" + case .characteristicsFoundError(let error): "Characteristics Found Error: \(error)" + case .writeError(let error): "Write Error: \(error)" + case .readRSSIError(let error): "Read RSSI Error: \(error)" } } } diff --git a/Sources/BLECombineKit/Main/BLECombineKit.swift b/Sources/BLECombineKit/Main/BLECombineKit.swift index 7c8f3d4..a513a2b 100644 --- a/Sources/BLECombineKit/Main/BLECombineKit.swift +++ b/Sources/BLECombineKit/Main/BLECombineKit.swift @@ -12,9 +12,10 @@ import Foundation public enum BLECombineKit { /// Build a BLECentralManager from which to scan peripherals. /// - /// - parameter centralManager: An optional CBCentralManager object, if available. + /// - Parameters: + /// - centralManager: An optional CBCentralManager object, if available. /// - /// - returns: an initialized BLECentralManager object. + /// - Returns: an initialized BLECentralManager object. static public func buildCentralManager( with centralManager: CBCentralManager? = nil ) -> BLECentralManager { diff --git a/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManager.swift b/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManager.swift index 1a87ee3..c9bd00b 100644 --- a/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManager.swift +++ b/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManager.swift @@ -236,9 +236,9 @@ public class BLEPeripheralManager { /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:didReceiveRead:)` results /// - returns: Observable that emits `next` event whenever didReceiveRead occurs. /// - /// It's **infinite** stream, so `.complete` is never called. + /// It's an **infinite** stream, so `.complete` is never called. /// - /// Observable can ends with following errors: + /// Observable can end with following errors: /// * `BLEError.deallocated` /// * `BLEError.bluetoothUnsupported` /// * `BLEError.bluetoothUnauthorized` diff --git a/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManagerDelegateWrapper.swift b/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManagerDelegateWrapper.swift index 9f10f04..65ee846 100644 --- a/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManagerDelegateWrapper.swift +++ b/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManagerDelegateWrapper.swift @@ -14,7 +14,7 @@ import Foundation /// /// See original source on [GitHub](https://github.com/Polidea/RxBluetoothKit/blob/2a95bce60fb569df57d7bec41d215fe58f56e1d4/Source/CBPeripheralManagerDelegateWrapper.swift). /// -class BLEPeripheralManagerDelegateWrapper: NSObject, CBPeripheralManagerDelegate { +final class BLEPeripheralManagerDelegateWrapper: NSObject, CBPeripheralManagerDelegate { let didUpdateState = PassthroughSubject() let isReady = PassthroughSubject() diff --git a/Sources/BLECombineKit/Peripheral/BLEPeripheral+Async.swift b/Sources/BLECombineKit/Peripheral/BLEPeripheral+Async.swift index ef62ae0..0aeb8c5 100644 --- a/Sources/BLECombineKit/Peripheral/BLEPeripheral+Async.swift +++ b/Sources/BLECombineKit/Peripheral/BLEPeripheral+Async.swift @@ -74,4 +74,13 @@ extension BLEPeripheral { ) -> AsyncThrowingStream { return observeValueUpdateAndSetNotification(for: characteristic).asyncThrowingStream } + + public func writeValueAsync( + _ data: Data, + for characteristic: CBCharacteristic, + type: CBCharacteristicWriteType + ) async throws { + let stream = writeValue(data, for: characteristic, type: type).values + for try await _ in stream { return } + } } diff --git a/Sources/BLECombineKit/Peripheral/BLEPeripheral.swift b/Sources/BLECombineKit/Peripheral/BLEPeripheral.swift index 75f926c..0b7b0cc 100644 --- a/Sources/BLECombineKit/Peripheral/BLEPeripheral.swift +++ b/Sources/BLECombineKit/Peripheral/BLEPeripheral.swift @@ -16,63 +16,101 @@ public protocol BLEPeripheral { var associatedPeripheral: CBPeripheralWrapper { get } /// Observe the connection state of the peripheral. + /// + /// - Returns: a Publisher that emits a boolean indicating a valid connection or not, which never completes. func observeConnectionState() -> AnyPublisher /// Connect to the peripheral with a given set of options. - /// This method will return an event with a successful connection or an error. /// See the CBPeripheral's `connect(with:)`method for more information. + /// + /// - Returns: a Publisher that emits the connected peripheral and then completes or a Fail if an error is found. func connect(with options: [String: Any]?) -> AnyPublisher /// Disconnect from the peripheral. - /// This method will return an event with a successful connection or an error. + /// + /// - Returns: a Publisher that completes event with a successful connection or a Fail if an error is found. @discardableResult func disconnect() -> AnyPublisher /// Observe any changes to the name of the peripheral. - /// An event will be triggered on any change to the peripheral's name, if any. - /// See the `peripheralDidUpdateName` method on the CBPeripheralDelegate. + /// See the CBPeripheralDelegate's `peripheralDidUpdateName(_:)`. + /// + /// - Returns: a Publisher that emits any change to the peripheral's name, which never completes. func observeNameValue() -> AnyPublisher /// Observe any updates to the RSSI value of the peripheral. - /// An event will be triggered on any change to the peripheral's rssi or if an error is triggered. - /// See the `didReadRSSI:` method on the CBPeripheralDelegate. + /// This method wraps up on top of CBPeripheralDelegate's `peripheral(_:didReadRSSI:error:)`. + /// + /// - Returns: a Publisher that emit the latest peripheral's rssi or a Fail if an error is found. func observeRSSIValue() -> AnyPublisher - /// This method wraps up on top of CBPeripheral's `discoverServices`, which will then publish - /// an event for each service discovered. This publisher will complete when all services are - /// published or until an error is triggered. + /// Discover all services given a collection of CBUUIDs. + /// This method wraps up on top of CBPeripheralDelegate's + /// `peripheral(_:didDiscoverServices:error:)`. + /// + /// - Parameters: + /// - serviceUUIDs: an optional collection of service CBUUIDs to discover. + /// + /// - Returns: a Publisher that emits all the services available before completing or a Fail if an error is found. func discoverServices(serviceUUIDs: [CBUUID]?) -> AnyPublisher - /// This method wraps up on top of CBPeripheral's `discoverCharacteristics`, which will then - /// publish an event for characteristic discovered. This publisher will complete when all - /// characteristics are published or until an error is triggered. + /// Discover all characteristics on a service given a collection of CBUUIDs. + /// This method wraps up on top of the CBPeripheralDelegate's `peripheral(_:didDiscoverCharacteristicsFor:error:)`. + /// + /// - Parameters: + /// - characteristicUUIDs: an optional collection of characteristic CBUUIDs to discover. + /// - service: the service to discover characteristics from. + /// + /// - Returns: a Publisher that emits all the characteristics available before completing or a Fail if an error is found. func discoverCharacteristics( characteristicUUIDs: [CBUUID]?, for service: CBService ) -> AnyPublisher /// Read the value of a given characteristic. - /// This method will trigger a single event after the CBPeripheral calls the - /// `didUpdateValueFor:` delegate method (see CBPeripheralDelegate) and it will then complete. + /// This method wraps up on top of the CBPeripheralDelegate's `peripheral(_:didUpdateValueFor:error:)` method. + /// + /// - Parameters: + /// - characteristic: the characteristic to read. + /// + /// - Returns: A Publisher that emits a single value and then completes or a Fail if an error is found. func readValue(for characteristic: CBCharacteristic) -> AnyPublisher /// Start observing for a value updated on a given characteristic. - /// An event is triggered every time the CBPeripheral calls the - /// `didUpdateValueFor:` delegate method (see CBPeripheralDelegate). + /// An event is triggered every time the CBPeripheralDelegate's `peripheral(_:didUpdateValueFor:error:)` method is called. + /// Note that this event does not update the notify/indicate status, if this status is not set then this method might never return any values. If you want to explicitly set the notify status, see `observeValueUpdateAndSetNotification(for:)`. + /// + /// - Parameters: + /// - characteristic: the characteristic to read. + /// + /// - Returns: A Publisher that emits a value or a Fail if an error is found. func observeValue(for characteristic: CBCharacteristic) -> AnyPublisher - /// Start observing for a value updated on a given characteristic and set the notify value on - /// the peripheral (internally calls `setNotifyValue`). - /// An event is triggered every time the CBPeripheral calls the - /// `didUpdateValueFor:` delegate method (see CBPeripheralDelegate). + /// Start observing for a value updated on a given characteristic and set the notify value on the peripheral (internally calls `setNotifyValue`). + /// An event is triggered every time the CBPeripheralDelegate's `peripheral(_:didUpdateValueFor:error:)` method is called. + /// + /// - Parameters: + /// - characteristic: the characteristic to read. + /// + /// - Returns: A Publisher that emits a value or a Fail if an error is found. func observeValueUpdateAndSetNotification( for characteristic: CBCharacteristic ) -> AnyPublisher - /// Set the notify on the peripheral for a given characteristic.. + /// Sets notifications or indications for the value of a specified characteristic. + /// + /// - Parameters: + /// - enabled: A Boolean value that indicates whether to receive notifications or indications whenever the characteristic’s value changes. true if you want to enable notifications or indications for the characteristic’s value. false if you don’t want to receive notifications or indications whenever the characteristic’s value changes. + /// - characteristic: The specified characteristic. func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic) /// Write a value, as Data, to a given characteristic. - /// This method returns an empty event on success or an error. + /// + /// - Parameters: + /// - data: the data to write. + /// - characteristic: the characteristic to write to. + /// - type: the type of write to be performed (with or without response). + /// + /// - Returns: a Publisher that completes on success or a Fail if an error is found. func writeValue( _ data: Data, for characteristic: CBCharacteristic, diff --git a/Sources/BLECombineKit/Peripheral/BLEPeripheralDelegate.swift b/Sources/BLECombineKit/Peripheral/BLEPeripheralDelegate.swift index 74fdfbd..f0d93dd 100644 --- a/Sources/BLECombineKit/Peripheral/BLEPeripheralDelegate.swift +++ b/Sources/BLECombineKit/Peripheral/BLEPeripheralDelegate.swift @@ -11,22 +11,22 @@ import CoreBluetooth import Foundation typealias DidUpdateName = (peripheral: CBPeripheralWrapper, name: String) -typealias DidDiscoverServicesResult = (peripheral: CBPeripheralWrapper, error: Error?) +typealias DidDiscoverServicesResult = (peripheral: CBPeripheralWrapper, error: BLEError?) typealias DidDiscoverCharacteristicsResult = ( - peripheral: CBPeripheralWrapper, service: CBService, error: Error? + peripheral: CBPeripheralWrapper, service: CBService, error: BLEError? ) typealias DidUpdateValueForCharacteristicResult = ( - peripheral: CBPeripheralWrapper, characteristic: CBCharacteristic, error: Error? + peripheral: CBPeripheralWrapper, characteristic: CBCharacteristic, error: BLEError? ) typealias DidDiscoverDescriptorForCharacteristicResult = ( - peripheral: CBPeripheralWrapper, characteristic: CBCharacteristic, error: Error? + peripheral: CBPeripheralWrapper, characteristic: CBCharacteristic, error: BLEError? ) typealias DidUpdateValueForDescriptorResult = ( - peripheral: CBPeripheralWrapper, descriptor: CBDescriptor, error: Error? + peripheral: CBPeripheralWrapper, descriptor: CBDescriptor, error: BLEError? ) -typealias DidReadRSSIResult = (peripheral: CBPeripheralWrapper, rssi: NSNumber, error: Error?) +typealias DidReadRSSIResult = (peripheral: CBPeripheralWrapper, rssi: NSNumber, error: BLEError?) typealias DidWriteValueForCharacteristicResult = ( - peripheral: CBPeripheralWrapper, characteristic: CBCharacteristic, error: Error? + peripheral: CBPeripheralWrapper, characteristic: CBCharacteristic, error: BLEError? ) final class BLEPeripheralDelegate: NSObject { @@ -35,44 +35,45 @@ final class BLEPeripheralDelegate: NSObject { let didUpdateName = PassthroughSubject() /// Subject used for the discover services callback. - let didDiscoverServices = PassthroughSubject() + let didDiscoverServices = PassthroughSubject() /// Subject used for the discover characteristics callback. - let didDiscoverCharacteristics = PassthroughSubject() + let didDiscoverCharacteristics = PassthroughSubject() /// Subject used for the discover descriptors callback. let didDiscoverDescriptors = PassthroughSubject< - DidDiscoverDescriptorForCharacteristicResult, Error + DidDiscoverDescriptorForCharacteristicResult, BLEError >() /// Subject used for the update value of a characteristic callback. let didUpdateValueForCharacteristic = PassthroughSubject< - DidUpdateValueForCharacteristicResult, Error + DidUpdateValueForCharacteristicResult, BLEError >() /// Subject used for the update value of a characteristic descriptor callback. - let didUpdateValueForDescriptor = PassthroughSubject() + let didUpdateValueForDescriptor = PassthroughSubject< + DidUpdateValueForDescriptorResult, BLEError + >() /// Subject used for the update value of the RSSI callback. - let didReadRSSI = PassthroughSubject() + let didReadRSSI = PassthroughSubject() /// Subject used for the didWrite callback. let didWriteValueForCharacteristic = PassthroughSubject< - DidWriteValueForCharacteristicResult, Error + DidWriteValueForCharacteristicResult, BLEError >() } extension BLEPeripheralDelegate: CBPeripheralDelegate { func peripheralDidUpdateName(_ peripheral: CBPeripheral) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) if let name = peripheral.name { - didUpdateName.send((peripheral: peripheralWrapper, name: name)) + didUpdateName.send((peripheral: peripheral, name: name)) } } func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) - didDiscoverServices.send((peripheral: peripheralWrapper, error: error)) + let bleError = didDiscoverServicesError(from: error) + didDiscoverServices.send((peripheral: peripheral, error: bleError)) } func peripheral( @@ -80,8 +81,10 @@ extension BLEPeripheralDelegate: CBPeripheralDelegate { didDiscoverCharacteristicsFor service: CBService, error: Error? ) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) - didDiscoverCharacteristics.send((peripheral: peripheralWrapper, service: service, error: error)) + let bleError = didDiscoverCharacteristicsError(from: error) + didDiscoverCharacteristics.send( + (peripheral: peripheral, service: service, error: bleError) + ) } func peripheral( @@ -89,9 +92,9 @@ extension BLEPeripheralDelegate: CBPeripheralDelegate { didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error? ) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) + let bleError = didDiscoverDescriptorsError(from: error) didDiscoverDescriptors.send( - (peripheral: peripheralWrapper, characteristic: characteristic, error: error) + (peripheral: peripheral, characteristic: characteristic, error: bleError) ) } @@ -100,9 +103,9 @@ extension BLEPeripheralDelegate: CBPeripheralDelegate { didUpdateValueFor characteristic: CBCharacteristic, error: Error? ) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) + let bleError = didUpdateValueForCharacteristicError(from: error) didUpdateValueForCharacteristic.send( - (peripheral: peripheralWrapper, characteristic: characteristic, error: error) + (peripheral: peripheral, characteristic: characteristic, error: bleError) ) } @@ -111,15 +114,15 @@ extension BLEPeripheralDelegate: CBPeripheralDelegate { didUpdateValueFor descriptor: CBDescriptor, error: Error? ) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) + let bleError = didUpdateValueForDescriptorError(from: error) didUpdateValueForDescriptor.send( - (peripheral: peripheralWrapper, descriptor: descriptor, error: error) + (peripheral: peripheral, descriptor: descriptor, error: bleError) ) } func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) - didReadRSSI.send((peripheral: peripheralWrapper, rssi: RSSI, error: error)) + let bleError = readRSSIError(from: error) + didReadRSSI.send((peripheral: peripheral, rssi: RSSI, error: bleError)) } func peripheral( @@ -127,9 +130,53 @@ extension BLEPeripheralDelegate: CBPeripheralDelegate { didWriteValueFor characteristic: CBCharacteristic, error: Error? ) { - let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral) + let bleError = didWriteValueError(from: error) didWriteValueForCharacteristic.send( - (peripheral: peripheralWrapper, characteristic: characteristic, error: error) + (peripheral: peripheral, characteristic: characteristic, error: bleError) ) } + + // MARK - Private. + + private func didDiscoverDescriptorsError(from error: Error?) -> BLEError? { + guard let error else { return nil } + let coreBluetoothError = BLEError.CoreBluetoothError.from(error: error as NSError) + return BLEError.peripheral(.didDiscoverDescriptorsError(coreBluetoothError)) + } + + private func didUpdateValueForCharacteristicError(from error: Error?) -> BLEError? { + guard let error else { return nil } + let coreBluetoothError = BLEError.CoreBluetoothError.from(error: error as NSError) + return BLEError.peripheral(.didUpdateValueForCharacteristicError(coreBluetoothError)) + } + + private func didUpdateValueForDescriptorError(from error: Error?) -> BLEError? { + guard let error else { return nil } + let coreBluetoothError = BLEError.CoreBluetoothError.from(error: error as NSError) + return BLEError.peripheral(.didUpdateValueForDescriptorError(coreBluetoothError)) + } + + private func didDiscoverServicesError(from error: Error?) -> BLEError? { + guard let error else { return nil } + let coreBluetoothError = BLEError.CoreBluetoothError.from(error: error as NSError) + return BLEError.peripheral(.servicesFoundError(coreBluetoothError)) + } + + private func didDiscoverCharacteristicsError(from error: Error?) -> BLEError? { + guard let error else { return nil } + let coreBluetoothError = BLEError.CoreBluetoothError.from(error: error as NSError) + return BLEError.peripheral(.characteristicsFoundError(coreBluetoothError)) + } + + private func readRSSIError(from error: Error?) -> BLEError? { + guard let error else { return nil } + let coreBluetoothError = BLEError.CoreBluetoothError.from(error: error as NSError) + return BLEError.peripheral(.readRSSIError(coreBluetoothError)) + } + + private func didWriteValueError(from error: Error?) -> BLEError? { + guard let error else { return nil } + let coreBluetoothError = BLEError.CoreBluetoothError.from(error: error as NSError) + return BLEError.peripheral(.writeError(coreBluetoothError)) + } } diff --git a/Sources/BLECombineKit/Peripheral/BLEPeripheralResult.swift b/Sources/BLECombineKit/Peripheral/BLEPeripheralResult.swift deleted file mode 100644 index b77b57b..0000000 --- a/Sources/BLECombineKit/Peripheral/BLEPeripheralResult.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// BLEPeripheralResult.swift -// BLECombineKit -// -// Created by Henry Javier Serrano Echeverria on 1/5/20. -// Copyright © 2020 Henry Serrano. All rights reserved. -// - -import Foundation - -protocol BLEPeripheralResult { - associatedtype BLEResultType - - var value: BLEResultType { get } - - init(value: BLEResultType, peripheral: BLEPeripheral) -} diff --git a/Sources/BLECombineKit/Peripheral/CBPeripheralWrapper.swift b/Sources/BLECombineKit/Peripheral/CBPeripheralWrapper.swift index d1d1483..e46ec48 100644 --- a/Sources/BLECombineKit/Peripheral/CBPeripheralWrapper.swift +++ b/Sources/BLECombineKit/Peripheral/CBPeripheralWrapper.swift @@ -9,10 +9,21 @@ import CoreBluetooth import Foundation -public protocol CBPeripheralWrapper { - /// The actual CBPeripheral this objects is wrapping. - var peripheral: CBPeripheral { get } +extension CBPeripheral: CBPeripheralWrapper { + public func setupDelegate(_ delegate: CBPeripheralDelegate) { + self.delegate = delegate + } + + public func connect(manager: CBCentralManager) { + manager.connect(self) + } + + public func cancelConnection(manager: CBCentralManager) { + manager.cancelPeripheralConnection(self) + } +} +public protocol CBPeripheralWrapper { /// The state of the wrapped CBPeripheral. var state: CBPeripheralState { get } @@ -30,6 +41,12 @@ public protocol CBPeripheralWrapper { /// of breaking the peripheral observable events. func setupDelegate(_ delegate: CBPeripheralDelegate) + /// Connect to a CBCentralManager. + func connect(manager: CBCentralManager) + + /// Cancel connection to a CBCentralManager. + func cancelConnection(manager: CBCentralManager) + /// Read the RSSI of the wrapped CBPeripheral. func readRSSI() @@ -70,89 +87,3 @@ public protocol CBPeripheralWrapper { /// Open an L2CAP channel of the wrapped CBPeripheral. func openL2CAPChannel(_ PSM: CBL2CAPPSM) } - -final class StandardCBPeripheralWrapper: CBPeripheralWrapper { - - var peripheral: CBPeripheral { - wrappedPeripheral - } - - var state: CBPeripheralState { - wrappedPeripheral.state - } - - var identifier: UUID { - wrappedPeripheral.identifier - } - - var name: String? { - wrappedPeripheral.name - } - - var services: [CBService]? { - wrappedPeripheral.services - } - - private let wrappedPeripheral: CBPeripheral - - init(peripheral: CBPeripheral) { - self.wrappedPeripheral = peripheral - } - - func setupDelegate(_ delegate: CBPeripheralDelegate) { - wrappedPeripheral.delegate = delegate - } - - func readRSSI() { - wrappedPeripheral.readRSSI() - } - - func discoverServices(_ serviceUUIDs: [CBUUID]?) { - wrappedPeripheral.discoverServices(serviceUUIDs) - } - - func discoverIncludedServices(_ includedServiceUUIDs: [CBUUID]?, for service: CBService) { - wrappedPeripheral.discoverIncludedServices(includedServiceUUIDs, for: service) - } - - func discoverCharacteristics(_ characteristicUUIDs: [CBUUID]?, for service: CBService) { - wrappedPeripheral.discoverCharacteristics(characteristicUUIDs, for: service) - } - - func readValue(for characteristic: CBCharacteristic) { - wrappedPeripheral.readValue(for: characteristic) - } - - func maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int { - wrappedPeripheral.maximumWriteValueLength(for: type) - } - - func writeValue( - _ data: Data, - for characteristic: CBCharacteristic, - type: CBCharacteristicWriteType - ) { - wrappedPeripheral.writeValue(data, for: characteristic, type: type) - } - - func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic) { - wrappedPeripheral.setNotifyValue(enabled, for: characteristic) - } - - func discoverDescriptors(for characteristic: CBCharacteristic) { - wrappedPeripheral.discoverDescriptors(for: characteristic) - } - - func readValue(for descriptor: CBDescriptor) { - wrappedPeripheral.readValue(for: descriptor) - } - - func writeValue(_ data: Data, for descriptor: CBDescriptor) { - wrappedPeripheral.writeValue(data, for: descriptor) - } - - func openL2CAPChannel(_ PSM: CBL2CAPPSM) { - wrappedPeripheral.openL2CAPChannel(PSM) - } - -} diff --git a/Sources/BLECombineKit/Peripheral/StandardBLEPeripheral.swift b/Sources/BLECombineKit/Peripheral/StandardBLEPeripheral.swift index 50618d7..a4add68 100644 --- a/Sources/BLECombineKit/Peripheral/StandardBLEPeripheral.swift +++ b/Sources/BLECombineKit/Peripheral/StandardBLEPeripheral.swift @@ -26,11 +26,7 @@ final class StandardBLEPeripheral: BLETrackedPeripheral { /// Cancellable reference to the connect publisher. private var connectCancellable: AnyCancellable? - /// Cancellable reference to the discoverServices publisher. - private var discoverServicesCancellable: AnyCancellable? - - /// Cancellable reference to the discoverCharacteristics publisher. - private var discoverCharacteristicsCancellable: AnyCancellable? + private typealias CharacteristicValuePreHandler = () -> (Void) init( peripheral: CBPeripheralWrapper, @@ -42,22 +38,11 @@ final class StandardBLEPeripheral: BLETrackedPeripheral { self.delegate = delegate } - public convenience init( - peripheral: CBPeripheralWrapper, - centralManager: BLECentralManager? - ) { - let delegate = BLEPeripheralDelegate() - if let peripheral = peripheral as? StandardCBPeripheralWrapper { - peripheral.setupDelegate(delegate) - } - self.init(peripheral: peripheral, centralManager: centralManager, delegate: delegate) - } - - public func observeConnectionState() -> AnyPublisher { + func observeConnectionState() -> AnyPublisher { return connectionState.eraseToAnyPublisher() } - public func connect( + func connect( with options: [String: Any]? ) -> AnyPublisher { return Future { [weak self] promise in @@ -102,11 +87,9 @@ final class StandardBLEPeripheral: BLETrackedPeripheral { } @discardableResult - public func disconnect() -> AnyPublisher { + func disconnect() -> AnyPublisher { guard let centralManager = centralManager else { - return Just(false) - .tryMap { _ in throw BLEError.peripheral(.disconnectionFailed) } - .mapError { $0 as? BLEError ?? BLEError.unknown } + return Fail(error: BLEError.peripheral(.disconnectionFailed)) .eraseToAnyPublisher() } return @@ -116,28 +99,28 @@ final class StandardBLEPeripheral: BLETrackedPeripheral { .eraseToAnyPublisher() } - public func observeNameValue() -> AnyPublisher { + func observeNameValue() -> AnyPublisher { return delegate .didUpdateName .map { $0.name } .eraseToAnyPublisher() } - public func observeRSSIValue() -> AnyPublisher { - associatedPeripheral.readRSSI() - - return delegate - .didReadRSSI - .map { $0.rssi } - .mapError { $0 as? BLEError ?? BLEError.unknown } - .eraseToAnyPublisher() + func observeRSSIValue() -> AnyPublisher { + Deferred> { [delegate, associatedPeripheral] in + defer { + associatedPeripheral.readRSSI() + } + return delegate + .didReadRSSI + .map { $0.rssi } + .eraseToAnyPublisher() + }.eraseToAnyPublisher() } - public func discoverServices( + func discoverServices( serviceUUIDs: [CBUUID]? ) -> AnyPublisher { - let subject = PassthroughSubject() - if let services = associatedPeripheral.services, services.isNotEmpty { return Publishers.Sequence(sequence: services) .setFailureType(to: BLEError.self) @@ -145,173 +128,135 @@ final class StandardBLEPeripheral: BLETrackedPeripheral { .eraseToAnyPublisher() } - associatedPeripheral.discoverServices(serviceUUIDs) - discoverServicesCancellable?.cancel() - - discoverServicesCancellable = delegate - .didDiscoverServices - .tryFilter { [weak self] in - guard let self = self else { throw BLEError.deallocated } - return $0.peripheral.identifier == self.associatedPeripheral.identifier + return Deferred> { + [weak self, delegate, associatedPeripheral] in + defer { + associatedPeripheral.discoverServices(serviceUUIDs) } - .tryMap { result -> [CBService] in - guard result.error == nil, let services = result.peripheral.services else { - throw BLEError.peripheral( - .servicesFoundError(BLEError.CoreBluetoothError.from(error: result.error! as NSError)) - ) + let identifier = associatedPeripheral.identifier + return delegate + .didDiscoverServices + .filter { $0.peripheral.identifier == identifier } + .first() + .flatMap { result -> AnyPublisher in + let output = result.peripheral.services ?? [] + return Publishers.Sequence(sequence: output) + .eraseToAnyPublisher() } - return services - } - .mapError { $0 as? BLEError ?? BLEError.unknown } - .sink( - receiveCompletion: { completion in - guard case .failure(let error) = completion else { return } - subject.send(completion: .failure(error)) - }, - receiveValue: { [weak self] services in - guard let self = self else { return } - services.forEach { service in - subject.send(BLEService(value: service, peripheral: self)) - } - subject.send(completion: .finished) + .compactMap { [weak self] output -> BLEService? in + guard let self else { return nil } + return BLEService(value: output, peripheral: self) } - ) - - return subject.eraseToAnyPublisher() + .eraseToAnyPublisher() + }.eraseToAnyPublisher() } - public func discoverCharacteristics( + func discoverCharacteristics( characteristicUUIDs: [CBUUID]?, for service: CBService ) -> AnyPublisher { - let subject = PassthroughSubject() - discoverCharacteristicsCancellable?.cancel() - - discoverCharacteristicsCancellable = delegate - .didDiscoverCharacteristics - .handleEvents(receiveSubscription: { [weak self] _ in - self?.associatedPeripheral.discoverCharacteristics(characteristicUUIDs, for: service) - }) - .tryFilter { [weak self] in - guard let self = self else { throw BLEError.deallocated } - return $0.peripheral.identifier == self.associatedPeripheral.identifier + return Deferred> { + [weak self, delegate, associatedPeripheral] in + defer { + associatedPeripheral.discoverCharacteristics(characteristicUUIDs, for: service) } - .tryMap { result -> [CBCharacteristic] in - guard result.error == nil, let characteristics = result.service.characteristics else { - throw BLEError.peripheral( - .characteristicsFoundError( - BLEError.CoreBluetoothError.from(error: result.error! as NSError) - ) - ) + let identifier = associatedPeripheral.identifier + return delegate + .didDiscoverCharacteristics + .filter { $0.peripheral.identifier == identifier } + .first() + .flatMap { result -> AnyPublisher in + let output = result.service.characteristics ?? [] + return Publishers.Sequence(sequence: output) + .eraseToAnyPublisher() } - return characteristics - } - .mapError { $0 as? BLEError ?? BLEError.unknown } - .sink( - receiveCompletion: { completion in - guard case .failure(let error) = completion else { return } - subject.send(completion: .failure(error)) - }, - receiveValue: { [weak self] characteristics in - guard let self = self else { return } - characteristics.forEach { characteristic in - subject.send(BLECharacteristic(value: characteristic, peripheral: self)) - } - subject.send(completion: .finished) + .compactMap { [weak self] output -> BLECharacteristic? in + guard let self else { return nil } + return BLECharacteristic(value: output, peripheral: self) } - ) - - return subject.eraseToAnyPublisher() + .eraseToAnyPublisher() + }.eraseToAnyPublisher() } - public func observeValue( + func observeValue( for characteristic: CBCharacteristic ) -> AnyPublisher { - buildDeferredValuePublisher(for: characteristic) - .handleEvents(receiveRequest: { [weak self] _ in - self?.associatedPeripheral.readValue(for: characteristic) - }).eraseToAnyPublisher() + return buildDeferredValuePublisher(for: characteristic, preHandler: nil) + .eraseToAnyPublisher() } - public func readValue(for characteristic: CBCharacteristic) -> AnyPublisher { - buildDeferredValuePublisher(for: characteristic) + func readValue(for characteristic: CBCharacteristic) -> AnyPublisher { + let preHandler: () -> (Void) = { [weak self] in + self?.associatedPeripheral.readValue(for: characteristic) + } + return buildDeferredValuePublisher(for: characteristic, preHandler: preHandler) .first() - .handleEvents(receiveSubscription: { [weak self] _ in - self?.associatedPeripheral.readValue(for: characteristic) - }).eraseToAnyPublisher() + .eraseToAnyPublisher() } - public func observeValueUpdateAndSetNotification( + func observeValueUpdateAndSetNotification( for characteristic: CBCharacteristic ) -> AnyPublisher { - buildDeferredValuePublisher(for: characteristic) - .handleEvents(receiveRequest: { [weak self] _ in - self?.associatedPeripheral.setNotifyValue(true, for: characteristic) - }).eraseToAnyPublisher() + let preHandler: () -> (Void) = { [weak self] in + self?.associatedPeripheral.setNotifyValue(true, for: characteristic) + } + return buildDeferredValuePublisher(for: characteristic, preHandler: preHandler) + .eraseToAnyPublisher() } - public func setNotifyValue( + func setNotifyValue( _ enabled: Bool, for characteristic: CBCharacteristic ) { associatedPeripheral.setNotifyValue(enabled, for: characteristic) } - public func writeValue( + func writeValue( _ data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType ) -> AnyPublisher { - defer { - associatedPeripheral.writeValue(data, for: characteristic, type: type) - } - - switch type { - case .withResponse: - return self.delegate - .didWriteValueForCharacteristic - .filter({ $0.characteristic == characteristic }) - .tryMap({ result -> CBCharacteristic in - if let error = result.error { - throw error + return Deferred> { [delegate, associatedPeripheral] in + defer { + associatedPeripheral.writeValue(data, for: characteristic, type: type) + } + switch type { + case .withResponse: + return delegate + .didWriteValueForCharacteristic + .filter({ $0.characteristic.uuid == characteristic.uuid }) + .flatMap { result -> AnyPublisher in + BLECombineKit.OutputOrFail(output: true, error: result.error) } - return result.characteristic - }) - .mapError({ - BLEError.writeFailed( - BLEError.CoreBluetoothError.from( - error: - $0 as NSError - ) - ) - }) - .first() - .ignoreOutput() - .eraseToAnyPublisher() - default: - return Empty(completeImmediately: true) - .setFailureType(to: BLEError.self) - .eraseToAnyPublisher() - } + .first() + .ignoreOutput() + .eraseToAnyPublisher() + default: + return Empty(completeImmediately: true) + .setFailureType(to: BLEError.self) + .eraseToAnyPublisher() + } + }.eraseToAnyPublisher() } - // MARK - private + // MARK - Private. private func buildDeferredValuePublisher( - for characteristic: CBCharacteristic + for characteristic: CBCharacteristic, + preHandler: CharacteristicValuePreHandler? ) -> AnyPublisher { - Deferred> { - self.delegate + return Deferred> { [delegate] in + // Run the pre-handler to run the method that will trigger the delegate's publisher. + defer { preHandler?() } + return delegate .didUpdateValueForCharacteristic .filter { $0.characteristic.uuid == characteristic.uuid } - .tryMap { [weak self] filteredPeripheral in - guard let self = self else { throw BLEError.deallocated } + .flatMap { filteredPeripheral -> AnyPublisher in guard let data = filteredPeripheral.characteristic.value else { - throw BLEError.data(.invalid) + return Fail(error: BLEError.data(.invalid)).eraseToAnyPublisher() } - return BLEData(value: data, peripheral: self) + return BLECombineKit.Just(BLEData(value: data)) } - .mapError { $0 as? BLEError ?? BLEError.unknown } .eraseToAnyPublisher() }.eraseToAnyPublisher() } diff --git a/Sources/BLECombineKit/Service/BLEService.swift b/Sources/BLECombineKit/Service/BLEService.swift index ea8d784..c5954a9 100644 --- a/Sources/BLECombineKit/Service/BLEService.swift +++ b/Sources/BLECombineKit/Service/BLEService.swift @@ -10,7 +10,7 @@ import Combine import CoreBluetooth import Foundation -public struct BLEService: BLEPeripheralResult { +public struct BLEService { public let value: CBService private let peripheral: BLEPeripheral diff --git a/Sources/BLECombineKit/Utils/BLECombineKit+Combine.swift b/Sources/BLECombineKit/Utils/BLECombineKit+Combine.swift new file mode 100644 index 0000000..d5461e1 --- /dev/null +++ b/Sources/BLECombineKit/Utils/BLECombineKit+Combine.swift @@ -0,0 +1,27 @@ +// +// BLECombineKit+Combine.swift +// BLECombineKit +// +// Created by Henry Javier Serrano Echeverria on 21/7/24. +// Copyright © 2024 Henry Serrano. All rights reserved. +// + +import Combine + +extension BLECombineKit { + /// A convenient method for creating a Combine's `Just` which defines the failure as `BLEError` + /// and the output type as the given input's type. + /// This method saves the declaration of `setFailureType` followed by `eraseToAnyPublisher`. + static func Just(_ value: T) -> AnyPublisher { + Combine.Just(value) + .setFailureType(to: BLEError.self) + .eraseToAnyPublisher() + } + + static func OutputOrFail(output: T, error: BLEError?) -> AnyPublisher { + if let error = error { + return Fail(error: error).eraseToAnyPublisher() + } + return BLECombineKit.Just(output) + } +} diff --git a/Tests/BLECombineKitTests/BLECentralManagerTests.swift b/Tests/BLECombineKitTests/BLECentralManagerTests.swift index 307ddee..55b5c4a 100644 --- a/Tests/BLECombineKitTests/BLECentralManagerTests.swift +++ b/Tests/BLECombineKitTests/BLECentralManagerTests.swift @@ -253,4 +253,23 @@ final class BLECentralManagerTests: XCTestCase { XCTAssertNotNil(observedPeripheral) } + func testConnect() { + // Given. + let expectation = XCTestExpectation(description: #function) + let peripheral = MockBLEPeripheral() + let peripheralWrapper = MockCBPeripheralWrapper() + + // When. + sut.connect(peripheral: peripheral, options: nil) + .sink { _ in + } receiveValue: { receivedPeripheral in + expectation.fulfill() + }.store(in: &cancellables) + delegate.didConnectPeripheral.send(peripheralWrapper) + + // Then. + wait(for: [expectation], timeout: 0.01) + XCTAssertEqual(centralManagerWrapper.connectWasCalledCount, 1) + } + } diff --git a/Tests/BLECombineKitTests/BLEDataTests.swift b/Tests/BLECombineKitTests/BLEDataTests.swift index b42e903..f0ca400 100644 --- a/Tests/BLECombineKitTests/BLEDataTests.swift +++ b/Tests/BLECombineKitTests/BLEDataTests.swift @@ -35,7 +35,7 @@ final class BLEDataTests: XCTestCase { } ) - let data = BLEData(value: float32Data, peripheral: mockupPeripheral) + let data = BLEData(value: float32Data) if let result = data.to(type: Float32.self) { XCTAssertEqual(float32, result, accuracy: 0.000001) diff --git a/Tests/BLECombineKitTests/BLEPeripheralTests.swift b/Tests/BLECombineKitTests/BLEPeripheralTests.swift index ec4dbda..bb39466 100644 --- a/Tests/BLECombineKitTests/BLEPeripheralTests.swift +++ b/Tests/BLECombineKitTests/BLEPeripheralTests.swift @@ -173,49 +173,12 @@ final class BLEPeripheralTests: XCTestCase { XCTAssertEqual(peripheralMock.discoverCharacteristicsWasCalledStack.count, 1) } - func testDiscoverCharacteristicWithMultipleSubscriptionsCallsDelegateOnlyOnce() throws { - // Given - let expectation = XCTestExpectation(description: "self.debugDescription") - let expectation2 = XCTestExpectation(description: "self.debugDescription 2") - let service = CBMutableService(type: CBUUID(string: "0x0000"), primary: true) - let mutableCharacteristic = commonMutableCharacteristic() - service.characteristics = [mutableCharacteristic] - - // When - let publisher = sut.discoverCharacteristics(characteristicUUIDs: nil, for: service) - - publisher - .sink( - receiveCompletion: { _ in - expectation.fulfill() - }, - receiveValue: { _ in - } - ).store(in: &disposable) - - publisher - .sink( - receiveCompletion: { _ in - expectation2.fulfill() - }, - receiveValue: { _ in - } - ).store(in: &disposable) - delegate.didDiscoverCharacteristics.send( - (peripheral: peripheralMock, service: service, error: nil) - ) - - // Then - wait(for: [expectation, expectation2], timeout: 0.005) - XCTAssertEqual(peripheralMock.discoverCharacteristicsWasCalledStack.count, 1) - } - func testObserveValueReturns() throws { // Given let expectation = XCTestExpectation(description: self.debugDescription) var expectedData: BLEData? let mutableCharacteristic = commonMutableCharacteristic() - let expectedReadStack = [mutableCharacteristic] + let expectedReadStack = [CBCharacteristic]() // When sut.observeValue(for: mutableCharacteristic) @@ -454,21 +417,29 @@ final class BLEPeripheralTests: XCTestCase { XCTAssertTrue(peripheralMock.writeValueForCharacteristicWasCalled) } - func testWriteValueWithResponseReturnsErrorOnDelegateErrorCall() { + func testWriteValueWithResponseReturnsErrorOnDelegateErrorCall() throws { // Given let expectation = XCTestExpectation(description: #function) let mutableCharacteristic = commonMutableCharacteristic() + let baseError = NSError( + domain: CBErrorDomain, + code: CBError.Code.connectionFailed.rawValue, + userInfo: nil + ) + let coreBluetoothError = BLEError.CoreBluetoothError.from(error: baseError) + let bleError = BLEError.peripheral(.writeError(coreBluetoothError)) + var receivedError: BLEError? // When sut.writeValue(Data(), for: mutableCharacteristic, type: .withResponse) .sink( receiveCompletion: { completion in - if case .failure(let error) = completion, case .writeFailed(let subError) = error, - case .base(code: let code, description: _) = subError, - code == CBError.Code.connectionFailed - { - expectation.fulfill() + guard case .failure(let error) = completion else { + XCTFail("Failure was expected") + return } + receivedError = error + expectation.fulfill() }, receiveValue: { _ in } @@ -477,17 +448,23 @@ final class BLEPeripheralTests: XCTestCase { delegate.didWriteValueForCharacteristic.send( ( peripheral: peripheralMock, characteristic: mutableCharacteristic, - error: NSError( - domain: CBErrorDomain, - code: CBError.Code.connectionFailed.rawValue, - userInfo: nil - ) + error: bleError ) ) // Then wait(for: [expectation], timeout: 0.005) XCTAssertTrue(peripheralMock.writeValueForCharacteristicWasCalled) + let validReceivedError = try XCTUnwrap(receivedError) + guard case .peripheral(let peripheralError) = validReceivedError else { + XCTFail("Not a peripheral error") + return + } + guard case .writeError(let writeError) = peripheralError else { + XCTFail("Not a write error") + return + } + XCTAssertEqual(writeError, coreBluetoothError) } func testDisconnectCallsCentralManager() throws { @@ -529,17 +506,6 @@ final class BLEPeripheralTests: XCTestCase { wait(for: [expectation], timeout: 0.005) } - func testConvenienceInit() { - // Given - let peripheralMock = MockCBPeripheralWrapper() - - // When - sut = StandardBLEPeripheral(peripheral: peripheralMock, centralManager: nil) - - // Then - XCTAssertNotNil(sut) - } - // MARK - Private. private func commonMutableCharacteristic( diff --git a/Tests/BLECombineKitTests/Mocks/BLEPeripheralMocks.swift b/Tests/BLECombineKitTests/Mocks/BLEPeripheralMocks.swift index 2939514..fc5dd68 100644 --- a/Tests/BLECombineKitTests/Mocks/BLEPeripheralMocks.swift +++ b/Tests/BLECombineKitTests/Mocks/BLEPeripheralMocks.swift @@ -32,7 +32,7 @@ final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { var connectWasCalled = false func connect(with options: [String: Any]?) -> AnyPublisher { connectWasCalled = true - let blePeripheral = StandardBLEPeripheral(peripheral: associatedPeripheral, centralManager: nil) + let blePeripheral = MockBLEPeripheral() return Just(blePeripheral) .setFailureType(to: BLEError.self) .eraseToAnyPublisher() @@ -74,7 +74,7 @@ final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { var observeValueWasCalled = false func observeValue(for characteristic: CBCharacteristic) -> AnyPublisher { observeValueWasCalled = true - let data = BLEData(value: Data(), peripheral: self) + let data = BLEData(value: Data()) return Just(data) .setFailureType(to: BLEError.self) .eraseToAnyPublisher() @@ -83,7 +83,7 @@ final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { var readValueWasCalledStack = [CBCharacteristic]() func readValue(for characteristic: CBCharacteristic) -> AnyPublisher { readValueWasCalledStack.append(characteristic) - let data = BLEData(value: Data(), peripheral: self) + let data = BLEData(value: Data()) return Just(data) .setFailureType(to: BLEError.self) .eraseToAnyPublisher() @@ -94,7 +94,7 @@ final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { -> AnyPublisher { observeValueUpdateAndSetNotificationWasCalled = true - let data = BLEData(value: Data(), peripheral: self) + let data = BLEData(value: Data()) return Just(data) .setFailureType(to: BLEError.self) .eraseToAnyPublisher() @@ -150,6 +150,16 @@ final class MockCBPeripheralWrapper: CBPeripheralWrapper { return mockedServices } + var connectWasCalledStack = [CBCentralManager]() + func connect(manager: CBCentralManager) { + connectWasCalledStack.append(manager) + } + + var cancelConnectionWasCalledStack = [CBCentralManager]() + func cancelConnection(manager: CBCentralManager) { + cancelConnectionWasCalledStack.append(manager) + } + var setupDelegateWasCalledStack = [CBPeripheralDelegate]() func setupDelegate(_ delegate: CBPeripheralDelegate) { setupDelegateWasCalledStack.append(delegate)