diff --git a/README.md b/README.md index f22340f..6315171 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ let package = Package( /// . . . dependencies: [ // Set the link to the library and choose the version - .package(url: "https://github.com/NordicSemiconductor/IOS-BLE-Library.git", from: "0.1.3"), + .package(url: "https://github.com/NordicSemiconductor/IOS-BLE-Library.git", from: "0.3.1"), ], targets: [ .target( @@ -51,12 +51,12 @@ The library can be installed using CocoaPods. Add the following line to your Podfile: ```ruby -pod 'iOS-BLE-Library', '~> 0.1.3' +pod 'iOS-BLE-Library', '~> 0.3.1' ``` or ```ruby -pod 'iOS-BLE-Library-Mock', '~> 0.1.3' +pod 'iOS-BLE-Library-Mock', '~> 0.3.1' ``` # Documentation & Examples diff --git a/Sources/iOS-BLE-Library-Mock/Alias.swift b/Sources/iOS-BLE-Library-Mock/Alias.swift index 3f8811d..b04c7c5 100644 --- a/Sources/iOS-BLE-Library-Mock/Alias.swift +++ b/Sources/iOS-BLE-Library-Mock/Alias.swift @@ -38,59 +38,50 @@ import CoreBluetoothMock // disabled for Xcode 12.5 beta //typealias CBPeer = CBMPeer //typealias CBAttribute = CBMAttribute -public typealias CBCentralManagerFactory = CBMCentralManagerFactory -public typealias CBUUID = CBMUUID -public typealias CBError = CBMError -public typealias CBATTError = CBMATTError -public typealias CBManagerState = CBMManagerState -public typealias CBPeripheralState = CBMPeripheralState -public typealias CBCentralManager = CBMCentralManager -public typealias CBCentralManagerDelegate = CBMCentralManagerDelegate -public typealias CBPeripheral = CBMPeripheral -public typealias CBPeripheralDelegate = CBMPeripheralDelegate -public typealias CBService = CBMService -public typealias CBCharacteristic = CBMCharacteristic -public typealias CBCharacteristicWriteType = CBMCharacteristicWriteType -public typealias CBCharacteristicProperties = CBMCharacteristicProperties -public typealias CBDescriptor = CBMDescriptor -public typealias CBConnectionEvent = CBMConnectionEvent +public typealias CBCentralManagerFactory = CBMCentralManagerFactory +public typealias CBUUID = CBMUUID +public typealias CBError = CBMError +public typealias CBATTError = CBMATTError +public typealias CBManagerState = CBMManagerState +public typealias CBPeripheralState = CBMPeripheralState +public typealias CBCentralManager = CBMCentralManager +public typealias CBCentralManagerDelegate = CBMCentralManagerDelegate +public typealias CBPeripheral = CBMPeripheral +public typealias CBPeripheralDelegate = CBMPeripheralDelegate +public typealias CBService = CBMService +public typealias CBCharacteristic = CBMCharacteristic +public typealias CBCharacteristicWriteType = CBMCharacteristicWriteType +public typealias CBCharacteristicProperties = CBMCharacteristicProperties +public typealias CBDescriptor = CBMDescriptor +public typealias CBConnectionEvent = CBMConnectionEvent public typealias CBConnectionEventMatchingOption = CBMConnectionEventMatchingOption @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) -public typealias CBL2CAPPSM = CBML2CAPPSM +public typealias CBL2CAPPSM = CBML2CAPPSM @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) -public typealias CBL2CAPChannel = CBML2CAPChannel +public typealias CBL2CAPChannel = CBML2CAPChannel -public let CBCentralManagerScanOptionAllowDuplicatesKey = - CBMCentralManagerScanOptionAllowDuplicatesKey -public let CBCentralManagerOptionShowPowerAlertKey = CBMCentralManagerOptionShowPowerAlertKey -public let CBCentralManagerOptionRestoreIdentifierKey = CBMCentralManagerOptionRestoreIdentifierKey -public let CBCentralManagerScanOptionSolicitedServiceUUIDsKey = - CBMCentralManagerScanOptionSolicitedServiceUUIDsKey -public let CBConnectPeripheralOptionStartDelayKey = CBMConnectPeripheralOptionStartDelayKey +public let CBCentralManagerScanOptionAllowDuplicatesKey = CBMCentralManagerScanOptionAllowDuplicatesKey +public let CBCentralManagerOptionShowPowerAlertKey = CBMCentralManagerOptionShowPowerAlertKey +public let CBCentralManagerOptionRestoreIdentifierKey = CBMCentralManagerOptionRestoreIdentifierKey +public let CBCentralManagerScanOptionSolicitedServiceUUIDsKey = CBMCentralManagerScanOptionSolicitedServiceUUIDsKey +public let CBConnectPeripheralOptionStartDelayKey = CBMConnectPeripheralOptionStartDelayKey #if !os(macOS) - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) - public let CBConnectPeripheralOptionRequiresANCS = CBMConnectPeripheralOptionRequiresANCS +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public let CBConnectPeripheralOptionRequiresANCS = CBMConnectPeripheralOptionRequiresANCS #endif -public let CBCentralManagerRestoredStatePeripheralsKey = - CBMCentralManagerRestoredStatePeripheralsKey -public let CBCentralManagerRestoredStateScanServicesKey = - CBMCentralManagerRestoredStateScanServicesKey -public let CBCentralManagerRestoredStateScanOptionsKey = - CBMCentralManagerRestoredStateScanOptionsKey +public let CBCentralManagerRestoredStatePeripheralsKey = CBMCentralManagerRestoredStatePeripheralsKey +public let CBCentralManagerRestoredStateScanServicesKey = CBMCentralManagerRestoredStateScanServicesKey +public let CBCentralManagerRestoredStateScanOptionsKey = CBMCentralManagerRestoredStateScanOptionsKey -public let CBAdvertisementDataLocalNameKey = CBMAdvertisementDataLocalNameKey -public let CBAdvertisementDataServiceUUIDsKey = CBMAdvertisementDataServiceUUIDsKey -public let CBAdvertisementDataIsConnectable = CBMAdvertisementDataIsConnectable -public let CBAdvertisementDataTxPowerLevelKey = CBMAdvertisementDataTxPowerLevelKey -public let CBAdvertisementDataServiceDataKey = CBMAdvertisementDataServiceDataKey -public let CBAdvertisementDataManufacturerDataKey = CBMAdvertisementDataManufacturerDataKey -public let CBAdvertisementDataOverflowServiceUUIDsKey = CBMAdvertisementDataOverflowServiceUUIDsKey -public let CBAdvertisementDataSolicitedServiceUUIDsKey = - CBMAdvertisementDataSolicitedServiceUUIDsKey +public let CBAdvertisementDataLocalNameKey = CBMAdvertisementDataLocalNameKey +public let CBAdvertisementDataServiceUUIDsKey = CBMAdvertisementDataServiceUUIDsKey +public let CBAdvertisementDataIsConnectable = CBMAdvertisementDataIsConnectable +public let CBAdvertisementDataTxPowerLevelKey = CBMAdvertisementDataTxPowerLevelKey +public let CBAdvertisementDataServiceDataKey = CBMAdvertisementDataServiceDataKey +public let CBAdvertisementDataManufacturerDataKey = CBMAdvertisementDataManufacturerDataKey +public let CBAdvertisementDataOverflowServiceUUIDsKey = CBMAdvertisementDataOverflowServiceUUIDsKey +public let CBAdvertisementDataSolicitedServiceUUIDsKey = CBMAdvertisementDataSolicitedServiceUUIDsKey -public let CBConnectPeripheralOptionNotifyOnConnectionKey = - CBMConnectPeripheralOptionNotifyOnConnectionKey -public let CBConnectPeripheralOptionNotifyOnDisconnectionKey = - CBMConnectPeripheralOptionNotifyOnDisconnectionKey -public let CBConnectPeripheralOptionNotifyOnNotificationKey = - CBMConnectPeripheralOptionNotifyOnNotificationKey +public let CBConnectPeripheralOptionNotifyOnConnectionKey = CBMConnectPeripheralOptionNotifyOnConnectionKey +public let CBConnectPeripheralOptionNotifyOnDisconnectionKey = CBMConnectPeripheralOptionNotifyOnDisconnectionKey +public let CBConnectPeripheralOptionNotifyOnNotificationKey = CBMConnectPeripheralOptionNotifyOnNotificationKey \ No newline at end of file diff --git a/Sources/iOS-BLE-Library-Mock/CentralManager/CentralManager.swift b/Sources/iOS-BLE-Library-Mock/CentralManager/CentralManager.swift index 185f6e4..87dd282 100644 --- a/Sources/iOS-BLE-Library-Mock/CentralManager/CentralManager.swift +++ b/Sources/iOS-BLE-Library-Mock/CentralManager/CentralManager.swift @@ -18,8 +18,7 @@ extension CentralManager { public var localizedDescription: String { switch self { case .wrongManager: - return - "Incorrect manager instance provided. Delegate must be of type ReactiveCentralManagerDelegate." + return "Incorrect manager instance provided. Delegate must be of type ReactiveCentralManagerDelegate." case .badState(let state): return "Bad state: \(state)." case .unknownError: @@ -55,7 +54,7 @@ private class Observer: NSObject { } /// A Custom Central Manager class. -/// +/// /// It wraps the standard CBCentralManager and has similar API. However, instead of using delegate, it uses publishers, thus bringing the reactive programming paradigm to the CoreBluetooth framework. public class CentralManager { private let isScanningSubject = CurrentValueSubject(false) @@ -64,7 +63,7 @@ public class CentralManager { /// The underlying CBCentralManager instance. public let centralManager: CBCentralManager - + /// The reactive delegate for the ``centralManager``. public let centralManagerDelegate: ReactiveCentralManagerDelegate @@ -74,12 +73,10 @@ public class CentralManager { /// - queue: The queue to perform operations on. Default is the main queue. public init( centralManagerDelegate: ReactiveCentralManagerDelegate = - ReactiveCentralManagerDelegate(), queue: DispatchQueue = .main, - options: [String: Any]? = nil + ReactiveCentralManagerDelegate(), queue: DispatchQueue = .main, options: [String : Any]? = nil ) { self.centralManagerDelegate = centralManagerDelegate - self.centralManager = CBMCentralManagerFactory.instance( - delegate: centralManagerDelegate, queue: queue) +self.centralManager = CBMCentralManagerFactory.instance(delegate: centralManagerDelegate, queue: queue) observer.setup() } @@ -112,17 +109,17 @@ extension CentralManager { /// If the peripheral was disconnected successfully, the publisher finishes without error. /// If the connection was unsuccessful or disconnection returns an error (e.g., peripheral disconnected unexpectedly), /// the publisher finishes with an error. - /// - /// Use ``CentralManager/connect(_:options:)`` to connect to a peripheral. - /// The returned publisher will emit the connected peripheral or an error if the connection fails. - /// The publisher will not complete until the peripheral is disconnected. - /// If the connection fails, or the peripheral is unexpectedly disconnected, the publisher will fail with an error. - /// - /// ```swift - /// centralManager.connect(peripheral) - /// .sink { completion in - /// switch completion { - /// case .finished: + /// + /// Use ``CentralManager/connect(_:options:)`` to connect to a peripheral. + /// The returned publisher will emit the connected peripheral or an error if the connection fails. + /// The publisher will not complete until the peripheral is disconnected. + /// If the connection fails, or the peripheral is unexpectedly disconnected, the publisher will fail with an error. + /// + /// ```swift + /// centralManager.connect(peripheral) + /// .sink { completion in + /// switch completion { + /// case .finished: /// print("Peripheral disconnected successfully") /// case .failure(let error): /// print("Error: \(error)") @@ -155,16 +152,15 @@ extension CentralManager { .bluetooth { self.centralManager.connect(peripheral, options: options) } - .autoconnect() - .eraseToAnyPublisher() + .autoconnect() + .eraseToAnyPublisher() } /// Cancels the connection with the specified peripheral. /// - Parameter peripheral: The peripheral to disconnect from. /// - Returns: A publisher that emits the disconnected peripheral. - public func cancelPeripheralConnection(_ peripheral: CBPeripheral) -> AnyPublisher< - CBPeripheral, Error - > { + public func cancelPeripheralConnection(_ peripheral: CBPeripheral) -> AnyPublisher + { return self.disconnectedPeripheralsChannel .tryFilter { r in guard r.0.identifier == peripheral.identifier else { @@ -179,17 +175,17 @@ extension CentralManager { } .map { $0.0 } .first() - .bluetooth { - self.centralManager.cancelPeripheralConnection(peripheral) - } - .autoconnect() - .eraseToAnyPublisher() + .bluetooth { + self.centralManager.cancelPeripheralConnection(peripheral) + } + .autoconnect() + .eraseToAnyPublisher() } } // MARK: Retrieving Lists of Peripherals extension CentralManager { - #warning("check `connect` method") + #warning("check `connect` method") /// Returns a list of the peripherals connected to the system whose /// services match a given set of criteria. /// @@ -222,9 +218,9 @@ extension CentralManager { extension CentralManager { #warning("Question: Should we throw an error if the scan is already running?") /// Initiates a scan for peripherals with the specified services. - /// + /// /// Calling this method stops an ongoing scan if it is already running and finishes the publisher returned by ``scanForPeripherals(withServices:)``. - /// + /// /// - Parameter services: The services to scan for. /// - Returns: A publisher that emits scan results or an error. public func scanForPeripherals(withServices services: [CBUUID]?) @@ -254,8 +250,8 @@ extension CentralManager { .bluetooth { self.centralManager.scanForPeripherals(withServices: services) } - .autoconnect() - .eraseToAnyPublisher() + .autoconnect() + .eraseToAnyPublisher() } /// Stops an ongoing scan for peripherals. diff --git a/Sources/iOS-BLE-Library-Mock/Peripheral/Peripheral+Writer.swift b/Sources/iOS-BLE-Library-Mock/Peripheral/Peripheral+Writer.swift index 0f0b052..52ae97a 100644 --- a/Sources/iOS-BLE-Library-Mock/Peripheral/Peripheral+Writer.swift +++ b/Sources/iOS-BLE-Library-Mock/Peripheral/Peripheral+Writer.swift @@ -43,6 +43,30 @@ extension Peripheral { super.init(peripheral: peripheral) } } + + class DescriptorWriter: OperationQueue { + let writtenEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never> + + init( + writtenEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never>, + peripheral: CBPeripheral + ) { + self.writtenEventsPublisher = writtenEventsPublisher + super.init(peripheral: peripheral) + } + } + + class DescriptorReader: OperationQueue { + let updateEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never> + + init( + updateEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never>, + peripheral: CBPeripheral + ) { + self.updateEventsPublisher = updateEventsPublisher + super.init(peripheral: peripheral) + } + } } extension Peripheral.CharacteristicWriter { @@ -73,6 +97,35 @@ extension Peripheral.CharacteristicReader { } } +extension Peripheral.DescriptorWriter { + func write(_ value: Data, to dsecriptor: CBDescriptor) -> Future { + let operation = WriteDescriptorOperation( + data: value, + writtenEventsPublisher: writtenEventsPublisher, + descriptor: dsecriptor, + peripheral: peripheral + ) + + queue.addOperation(operation) + + return operation.future + } +} + +extension Peripheral.DescriptorReader { + func readValue(from descriptor: CBDescriptor) -> Future { + let operation = ReadDescriptorOperation( + updateEventPublisher: updateEventsPublisher, + descriptor: descriptor, + peripheral: peripheral + ) + + queue.addOperation(operation) + + return operation.future + } +} + private class BasicOperation: Operation { let peripheral: CBPeripheral var cancelable: AnyCancellable? @@ -222,5 +275,105 @@ private class ReadCharacteristicOperation: BasicOperation { state = .executing main() } +} + +private class WriteDescriptorOperation: BasicOperation { + + let writtenEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never> + let descriptor: CBDescriptor + + let data: Data + + init( + data: Data, writtenEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never>, + descriptor: CBDescriptor, peripheral: CBPeripheral + ) { + self.data = data + self.writtenEventsPublisher = writtenEventsPublisher + self.descriptor = descriptor + super.init(peripheral: peripheral) + } + + override func main() { + peripheral.writeValue(data, for: descriptor) + } + + override func start() { + if isCancelled { + state = .finished + return + } + + self.cancelable = writtenEventsPublisher.share() + .filter { $0.0.uuid == self.descriptor.uuid && $0.0.characteristic?.uuid == self.descriptor.characteristic?.uuid } + .first() + .tryMap { v in + if let e = v.1 { + throw e + } else { + return v.0 + } + } + .sink { [unowned self] completion in + switch completion { + case .finished: + self.promise?(.success(())) + case .failure(let e): + self.promise?(.failure(e)) + } + self.state = .finished + } receiveValue: { _ in + + } + + state = .executing + main() + } +} +private class ReadDescriptorOperation: BasicOperation { + let updateEventPublisher: AnyPublisher<(CBDescriptor, Error?), Never> + let descriptor: CBDescriptor + + init( + updateEventPublisher: AnyPublisher<(CBDescriptor, Error?), Never>, + descriptor: CBDescriptor, peripheral: CBPeripheral + ) { + self.updateEventPublisher = updateEventPublisher + self.descriptor = descriptor + super.init(peripheral: peripheral) + } + + override func main() { + peripheral.readValue(for: descriptor) + } + + override func start() { + if isCancelled { + state = .finished + return + } + + self.cancelable = updateEventPublisher.share() + .filter { $0.0.uuid == self.descriptor.uuid && $0.0.characteristic?.uuid == self.descriptor.characteristic?.uuid } + .first() + .tryMap { v in + if let e = v.1 { + throw e + } else { + return v.0.value + } + } + .sink { [unowned self] completion in + if case .failure(let e) = completion { + self.promise?(.failure(e)) + } + self.state = .finished + } receiveValue: { v in + self.promise?(.success(v)) + } + + state = .executing + main() + } } diff --git a/Sources/iOS-BLE-Library-Mock/Peripheral/Peripheral.swift b/Sources/iOS-BLE-Library-Mock/Peripheral/Peripheral.swift index e66390f..600637b 100644 --- a/Sources/iOS-BLE-Library-Mock/Peripheral/Peripheral.swift +++ b/Sources/iOS-BLE-Library-Mock/Peripheral/Peripheral.swift @@ -7,6 +7,7 @@ import Combine import CoreBluetooth + import CoreBluetoothMock import Foundation @@ -19,8 +20,8 @@ private class NativeObserver: Observer { private weak var publisher: CurrentValueSubject! private var observation: NSKeyValueObservation? - - let l = L(category: "peripheral") + + let l = L(category: "peripheral") init( peripheral: CoreBluetooth.CBPeripheral, @@ -34,45 +35,42 @@ private class NativeObserver: Observer { override func setup() { observation = peripheral.observe(\.state, options: [.new]) { [weak self] _, change in - // TODO: Check threads - guard let self else { return } - self.publisher.send(self.peripheral.state) + // TODO: Check threads + guard let self else { return } + self.publisher.send(self.peripheral.state) } } } private class MockObserver: Observer { - @objc private var peripheral: CBMPeripheralMock + @objc private var peripheral: CBMPeripheralMock - private weak var publisher: CurrentValueSubject! - private var observation: NSKeyValueObservation? + private weak var publisher: CurrentValueSubject! + private var observation: NSKeyValueObservation? - init( - peripheral: CBMPeripheralMock, - publisher: CurrentValueSubject - ) { - self.peripheral = peripheral - self.publisher = publisher - super.init() - } + init(peripheral: CBMPeripheralMock, publisher: CurrentValueSubject) { + self.peripheral = peripheral + self.publisher = publisher + super.init() + } - override func setup() { - observation = peripheral.observe(\.state, options: [.new]) { - [weak self] _, change in - #warning("queue can be not only main") - DispatchQueue.main.async { - guard let self else { return } - self.publisher.send(self.peripheral.state) - } - } - } -} - -public class Peripheral { - private var serviceDiscoveryQueue = Queue() + override func setup() { + observation = peripheral.observe(\.state, options: [.new]) { [weak self] _, change in + #warning("queue can be not only main") + DispatchQueue.main.async { + guard let self else { return } + self.publisher.send(self.peripheral.state) + } + } + } + } - let l = L(category: #file) +public class Peripheral { + private var serviceDiscoveryQueue = Queue() + + let l = L(category: #file) + /// I'm Errr from Omicron Persei 8 public enum Err: Error { case badDelegate @@ -81,51 +79,56 @@ public class Peripheral { /// The underlying CBPeripheral instance. public let peripheral: CBPeripheral - // MARK: Identifying a Peripheralin page link - /// The name of the peripheral. - public var name: String? { peripheral.name } - + // MARK: Identifying a Peripheralin page link + /// The name of the peripheral. + public var name: String? { peripheral.name } + /// The delegate for handling peripheral events. public let peripheralDelegate: ReactivePeripheralDelegate private let stateSubject = CurrentValueSubject(.disconnected) private var observer: Observer! - private lazy var writer = CharacteristicWriter( + private lazy var characteristicWriter = CharacteristicWriter( writtenEventsPublisher: self.peripheralDelegate.writtenCharacteristicValuesSubject .eraseToAnyPublisher(), peripheral: self.peripheral ) - private lazy var reader = CharacteristicReader( + private lazy var characteristicReader = CharacteristicReader( updateEventPublisher: self.peripheralDelegate.updatedCharacteristicValuesSubject .eraseToAnyPublisher(), peripheral: peripheral ) + + private lazy var descriptorWriter = DescriptorWriter( + writtenEventsPublisher: self.peripheralDelegate.writtenDescriptorValuesSubject.eraseToAnyPublisher(), + peripheral: peripheral + ) + + private lazy var descriptorReader = DescriptorReader( + updateEventsPublisher: self.peripheralDelegate.updatedDescriptorValuesSubject.eraseToAnyPublisher(), + peripheral: peripheral + ) // TODO: Why don't we use default delegate? /// Initializes a Peripheral instance. - /// - /// - Parameters: - /// - peripheral: The CBPeripheral to manage. - /// - delegate: The delegate for handling peripheral events. - public init( - peripheral: CBPeripheral, - delegate: ReactivePeripheralDelegate = ReactivePeripheralDelegate() - ) { + /// + /// - Parameters: + /// - peripheral: The CBPeripheral to manage. + /// - delegate: The delegate for handling peripheral events. + public init(peripheral: CBPeripheral, delegate: ReactivePeripheralDelegate = ReactivePeripheralDelegate()) { self.peripheral = peripheral self.peripheralDelegate = delegate - assert( - peripheral.delegate == nil, - "CBPeripheral's delegate should be nil, otherwise it can lead to problems") + assert(peripheral.delegate == nil, "CBPeripheral's delegate should be nil, otherwise it can lead to problems") peripheral.delegate = delegate - if let p = peripheral as? CBMPeripheralNative { - observer = NativeObserver(peripheral: p.peripheral, publisher: stateSubject) - observer.setup() - } else if let p = peripheral as? CBMPeripheralMock { - observer = MockObserver(peripheral: p, publisher: stateSubject) - observer.setup() - } +if let p = peripheral as? CBMPeripheralNative { + observer = NativeObserver(peripheral: p.peripheral, publisher: stateSubject) + observer.setup() + } else if let p = peripheral as? CBMPeripheralMock { + observer = MockObserver(peripheral: p, publisher: stateSubject) + observer.setup() + } } } @@ -139,170 +142,168 @@ extension Peripheral { // MARK: - Discovering Servicesin page link extension Peripheral { - /// Discover services for the peripheral. - /// - /// - Parameter serviceUUIDs: An optional array of service UUIDs to filter the discovery results. If nil, all services will be discovered. - /// - Returns: A publisher emitting discovered services or an error. - public func discoverServices(serviceUUIDs: [CBUUID]?) - -> AnyPublisher<[CBService], Error> - { - let id = UUID() - - let allServices = peripheralDelegate.discoveredServicesSubject - .first(where: { $0.id == id }) - .tryCompactMap { result throws -> [CBService]? in - if let e = result.error { - throw e - } else { - return result.value - } - } - .first() - - return allServices.bluetooth { - let operation = IdentifiableOperation(id: id) { - self.peripheral.discoverServices(serviceUUIDs) - self.l.d("\(#function). operation ID: \(id)") - if let serviceUUIDs { - for sid in serviceUUIDs { - self.l.d("Services: \(sid)") - } - } else { - self.l.d("All services") - } - } - - self.peripheralDelegate.discoveredServicesQueue.addOperation(operation) - } - .autoconnect() - .eraseToAnyPublisher() - } - - /// Discovers the specified included services of a previously-discovered service. - public func discoverIncludedServices(_ includedServiceUUIDs: [CBUUID]?, for: CBService) - -> AnyPublisher<[CBService], Error> - { - fatalError() - } - - /// A list of a peripheral’s discovered services. - public var services: [CBService]? { - peripheral.services - } + /// Discover services for the peripheral. + /// + /// - Parameter serviceUUIDs: An optional array of service UUIDs to filter the discovery results. If nil, all services will be discovered. + /// - Returns: A publisher emitting discovered services or an error. + public func discoverServices(serviceUUIDs: [CBUUID]?) + -> AnyPublisher<[CBService], Error> + { + let id = UUID() + + let allServices = peripheralDelegate.discoveredServicesSubject + .first(where: { $0.id == id } ) + .tryCompactMap { result throws -> [CBService]? in + if let e = result.error { + throw e + } else { + return result.value + } + } + .first() + + return allServices.bluetooth { + let operation = IdentifiableOperation(id: id) { + self.peripheral.discoverServices(serviceUUIDs) + self.l.d("\(#function). operation ID: \(id)") + if let serviceUUIDs { + for sid in serviceUUIDs { + self.l.d("Services: \(sid)") + } + } else { + self.l.d("All services") + } + } + + self.peripheralDelegate.discoveredServicesQueue.addOperation(operation) + } + .autoconnect() + .eraseToAnyPublisher() + } + + /// Discovers the specified included services of a previously-discovered service. + public func discoverIncludedServices(_ includedServiceUUIDs: [CBUUID]?, for: CBService) -> AnyPublisher<[CBService], Error> { + fatalError() + } + + /// A list of a peripheral’s discovered services. + public var services: [CBService]? { + peripheral.services + } } //MARK: - Discovering Characteristics and Descriptorsin page link extension Peripheral { /// Discover characteristics for a given service. - /// - /// - Parameters: - /// - characteristicUUIDs: An optional array of characteristic UUIDs to filter the discovery results. If nil, all characteristics will be discovered. - /// - service: The service for which to discover characteristics. - /// - Returns: A publisher emitting discovered characteristics or an error. + /// + /// - Parameters: + /// - characteristicUUIDs: An optional array of characteristic UUIDs to filter the discovery results. If nil, all characteristics will be discovered. + /// - service: The service for which to discover characteristics. + /// - Returns: A publisher emitting discovered characteristics or an error. public func discoverCharacteristics( _ characteristicUUIDs: [CBUUID]?, for service: CBService ) -> AnyPublisher<[CBCharacteristic], Error> { - let id = UUID() - + let id = UUID() + let allCharacteristics = peripheralDelegate.discoveredCharacteristicsSubject - .filter { - $0.value.0.uuid == service.uuid - } - .first(where: { $0.id == id }) + .filter { + $0.value.0.uuid == service.uuid + } + .first(where: { $0.id == id } ) .tryCompactMap { result throws -> [CBCharacteristic]? in - if let e = result.error { + if let e = result.error { throw e } else { - return result.value.1 + return result.value.1 } } - .first() + .first() return allCharacteristics.bluetooth { - self.peripheralDelegate.discoveredCharacteristicsQueue.enqueue(id) + self.peripheralDelegate.discoveredCharacteristicsQueue.enqueue(id) self.peripheral.discoverCharacteristics(characteristicUUIDs, for: service) } - .autoconnect() - .eraseToAnyPublisher() + .autoconnect() + .eraseToAnyPublisher() } /// Discover descriptors for a given characteristic. - /// - /// - Parameter characteristic: The characteristic for which to discover descriptors. - /// - Returns: A publisher emitting discovered descriptors or an error. + /// + /// - Parameter characteristic: The characteristic for which to discover descriptors. + /// - Returns: A publisher emitting discovered descriptors or an error. public func discoverDescriptors(for characteristic: CBCharacteristic) -> AnyPublisher<[CBDescriptor], Error> { - let id = UUID() - + let id = UUID() + return peripheralDelegate.discoveredDescriptorsSubject .filter { - $0.value.0.uuid == characteristic.uuid + $0.value.0.uuid == characteristic.uuid } - .first(where: { $0.id == id }) + .first(where: { $0.id == id }) .tryCompactMap { result throws -> [CBDescriptor]? in - if let e = result.error { + if let e = result.error { throw e } else { - return result.value.1 + return result.value.1 } } - .first() + .first() .bluetooth { - self.peripheralDelegate.discoveredDescriptorsQueue.enqueue(id) + self.peripheralDelegate.discoveredDescriptorsQueue.enqueue(id) self.peripheral.discoverDescriptors(for: characteristic) } - .autoconnect() - .eraseToAnyPublisher() + .autoconnect() + .eraseToAnyPublisher() } } // MARK: - Reading Characteristic and Descriptor Values extension Peripheral { - /// Read the value of a characteristic. - /// - /// - Parameter characteristic: The characteristic to read from. - /// - Returns: A future emitting the read data or an error. - public func readValue(for characteristic: CBCharacteristic) -> Future { - return reader.readValue(from: characteristic) - } - - /// Listen for updates to the value of a characteristic. - /// - /// - Parameter characteristic: The characteristic to monitor for updates. - /// - Returns: A publisher emitting characteristic values or an error. - public func listenValues(for characteristic: CBCharacteristic) -> AnyPublisher - { - return peripheralDelegate.updatedCharacteristicValuesSubject - .filter { $0.0.uuid == characteristic.uuid } - .tryCompactMap { (ch, err) in - if let err { - throw err - } - - return ch.value - } - .eraseToAnyPublisher() - } - - /// Read the value of a descriptor. - /// - /// - Parameter descriptor: The descriptor to read from. - /// - Returns: A future emitting the read data or an error. - public func readValue(for descriptor: CBDescriptor) -> Future { - fatalError() - } + /// Read the value of a characteristic. + /// + /// - Parameter characteristic: The characteristic to read from. + /// - Returns: A future emitting the read data or an error. + public func readValue(for characteristic: CBCharacteristic) -> Future { + return characteristicReader.readValue(from: characteristic) + } + + /// Listen for updates to the value of a characteristic. + /// + /// - Parameter characteristic: The characteristic to monitor for updates. + /// - Returns: A publisher emitting characteristic values or an error. + public func listenValues(for characteristic: CBCharacteristic) -> AnyPublisher + { + return peripheralDelegate.updatedCharacteristicValuesSubject + .filter { $0.0.uuid == characteristic.uuid } + .tryCompactMap { (ch, err) in + if let err { + throw err + } + + return ch.value + } + .eraseToAnyPublisher() + } + + /// Read the value of a descriptor. + /// + /// - Parameter descriptor: The descriptor to read from. + /// - Returns: A future emitting the read data or an error. + public func readValue(for descriptor: CBDescriptor) -> Future { + return descriptorReader.readValue(from: descriptor) + } } // MARK: - Writing Characteristic and Descriptor Values extension Peripheral { /// Write data to a characteristic and wait for a response. - /// - /// - Parameters: - /// - data: The data to write. - /// - characteristic: The characteristic to write to. - /// - Returns: A publisher indicating success or an error. + /// + /// - Parameters: + /// - data: The data to write. + /// - characteristic: The characteristic to write to. + /// - Returns: A publisher indicating success or an error. public func writeValueWithResponse(_ data: Data, for characteristic: CBCharacteristic) -> AnyPublisher { @@ -319,44 +320,44 @@ extension Peripheral { self.peripheral.writeValue( data, for: characteristic, type: .withResponse) } - .autoconnect() - .eraseToAnyPublisher() + .autoconnect() + .eraseToAnyPublisher() } /// Write data to a characteristic without waiting for a response. - /// - /// - Parameters: - /// - data: The data to write. - /// - characteristic: The characteristic to write to. + /// + /// - Parameters: + /// - data: The data to write. + /// - characteristic: The characteristic to write to. public func writeValueWithoutResponse(_ data: Data, for characteristic: CBCharacteristic) { peripheral.writeValue(data, for: characteristic, type: .withoutResponse) } /// Write data to a descriptor. - /// - /// - Parameters: - /// - data: The data to write. - /// - descriptor: The descriptor to write to. - public func writeValue(_ data: Data, for descriptor: CBDescriptor) { - fatalError() + /// + /// - Parameters: + /// - data: The data to write. + /// - descriptor: The descriptor to write to. + public func writeValue(_ data: Data, for descriptor: CBDescriptor) -> Future { + return descriptorWriter.write(data, to: descriptor) } } // MARK: - Setting Notifications for a Characteristic’s Value extension Peripheral { /// Set notification state for a characteristic. - /// - /// - Parameters: - /// - isEnabled: Whether notifications should be enabled or disabled. - /// - characteristic: The characteristic for which to set the notification state. - /// - Returns: A publisher indicating success or an error. + /// + /// - Parameters: + /// - isEnabled: Whether notifications should be enabled or disabled. + /// - characteristic: The characteristic for which to set the notification state. + /// - Returns: A publisher indicating success or an error. public func setNotifyValue(_ isEnabled: Bool, for characteristic: CBCharacteristic) -> AnyPublisher { if characteristic.isNotifying == isEnabled { return Just(isEnabled) .setFailureType(to: Error.self) - .eraseToAnyPublisher() + .eraseToAnyPublisher() } return peripheralDelegate.notificationStateSubject @@ -370,28 +371,132 @@ extension Peripheral { .bluetooth { self.peripheral.setNotifyValue(isEnabled, for: characteristic) } - .autoconnect() - .eraseToAnyPublisher() + .autoconnect() + .eraseToAnyPublisher() } } // MARK: - Accessing a Peripheral’s Signal Strengthin page link extension Peripheral { - /// Retrieves the current RSSI value for the peripheral while connected to the central manager. - public func readRSSI() -> AnyPublisher { - peripheralDelegate.readRSSISubject - .tryMap { rssi in - if let error = rssi.1 { - throw error - } else { - return rssi.0 - } - } - .first() - .bluetooth { - self.peripheral.readRSSI() - } - .autoconnect() - .eraseToAnyPublisher() - } + /// Retrieves the current RSSI value for the peripheral while connected to the central manager. + public func readRSSI() -> AnyPublisher { + peripheralDelegate.readRSSISubject + .tryMap { rssi in + if let error = rssi.1 { + throw error + } else { + return rssi.0 + } + } + .first() + .bluetooth { + self.peripheral.readRSSI() + } + .autoconnect() + .eraseToAnyPublisher() + } +} + +// MARK: - Channels +extension Peripheral { + /// A publisher that emits the discovered services of the peripheral. + public var discoveredServicesChannel: AnyPublisher<[CBService]?, Error> { + peripheralDelegate.discoveredServicesSubject + .tryMap { result in + if let e = result.error { + throw e + } else { + return result.value + } + } + .eraseToAnyPublisher() + } + + /// A publisher that emits the discovered characteristics of a service. + public var discoveredCharacteristicsChannel: AnyPublisher<(CBService, [CBCharacteristic]?)?, Error> { + peripheralDelegate.discoveredCharacteristicsSubject + .tryMap { result in + if let e = result.error { + throw e + } else { + return result.value + } + } + .eraseToAnyPublisher() + } + + /// A publisher that emits the discovered descriptors of a characteristic. + public var discoveredDescriptorsChannel: AnyPublisher<(CBCharacteristic, [CBDescriptor]?)?, Error> { + peripheralDelegate.discoveredDescriptorsSubject + .tryMap { result in + if let e = result.error { + throw e + } else { + return result.value + } + } + .eraseToAnyPublisher() + } + + /// A publisher that emits the updated value of a characteristic. + public var updatedCharacteristicValuesChannel: AnyPublisher<(CBCharacteristic, Error?), Never> { + peripheralDelegate.updatedCharacteristicValuesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the updated value of a descriptor. + public var updatedDescriptorValuesChannel: AnyPublisher<(CBDescriptor, Error?), Never> { + peripheralDelegate.updatedDescriptorValuesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the written value of a characteristic. + public var writtenCharacteristicValuesChannel: AnyPublisher<(CBCharacteristic, Error?), Never> { + peripheralDelegate.writtenCharacteristicValuesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the written value of a descriptor. + public var writtenDescriptorValuesChannel: AnyPublisher<(CBDescriptor, Error?), Never> { + peripheralDelegate.writtenDescriptorValuesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the notification state of a characteristic. + public var notificationStateChannel: AnyPublisher<(CBCharacteristic, Error?), Never> { + peripheralDelegate.notificationStateSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the update name of a peripheral. + public var updateNameChannel: AnyPublisher { + peripheralDelegate.updateNameSubject + .eraseToAnyPublisher() + } + + public var modifyServices: AnyPublisher<[CBService], Never> { + peripheralDelegate.modifyServicesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the read RSSI value of a peripheral. + public var readRSSIChannel: AnyPublisher { + peripheralDelegate.readRSSISubject + .tryMap { rssi in + if let error = rssi.1 { + throw error + } else { + return rssi.0 + } + } + .eraseToAnyPublisher() + } + + /// A publisher that emits the isReadyToSendWriteWithoutResponse value of a peripheral. + public var isReadyToSendWriteWithoutResponseChannel: AnyPublisher { + peripheralDelegate.isReadyToSendWriteWithoutResponseSubject + .first() + .eraseToAnyPublisher() + } + } diff --git a/Sources/iOS-BLE-Library-Mock/Peripheral/ReactivePeripheralDelegate.swift b/Sources/iOS-BLE-Library-Mock/Peripheral/ReactivePeripheralDelegate.swift index 1e4283f..bae33bd 100644 --- a/Sources/iOS-BLE-Library-Mock/Peripheral/ReactivePeripheralDelegate.swift +++ b/Sources/iOS-BLE-Library-Mock/Peripheral/ReactivePeripheralDelegate.swift @@ -10,84 +10,84 @@ import CoreBluetoothMock import Foundation struct BluetoothOperationResult { - let value: T - let error: Error? - let id: UUID + let value: T + let error: Error? + let id: UUID } struct IdentifiableOperation { - let id: UUID - let block: () -> Void + let id: UUID + let block: () -> Void } class SingleTaskQueue { - private var queue = Queue() - let l = L(category: "SingleTaskQueue") - private let accessQueue = DispatchQueue(label: "com.ble-library.SingleTaskQueue") - - func addOperation(_ task: IdentifiableOperation) { - accessQueue.sync { - l.i("add operation \(task.id)") - if queue.isEmpty { - l.i("queue is empty") - queue.enqueue(task) - task.block() - } else { - l.i("some tasks") - queue.enqueue(task) - } - } - } - - func dequeue() -> IdentifiableOperation? { - var task: IdentifiableOperation? - accessQueue.sync { - task = queue.dequeue() - } - l.i("dequeue: \(task?.id.uuidString ?? "no task")") - return task - } - - func runNext() { - accessQueue.sync { - let task = queue.peek() - l.i("run next: \(task?.id.uuidString ?? "no task")") - task?.block() - } - } + private var queue = Queue() + let l = L(category: "SingleTaskQueue") + private let accessQueue = DispatchQueue(label: "com.ble-library.SingleTaskQueue") + + func addOperation(_ task: IdentifiableOperation) { + accessQueue.sync { + l.i("add operation \(task.id)") + if queue.isEmpty { + l.i("queue is empty") + queue.enqueue(task) + task.block() + } else { + l.i("some tasks") + queue.enqueue(task) + } + } + } + + func dequeue() -> IdentifiableOperation? { + var task: IdentifiableOperation? + accessQueue.sync { + task = queue.dequeue() + } + l.i("dequeue: \(task?.id.uuidString ?? "no task")") + return task + } + + func runNext() { + accessQueue.sync { + let task = queue.peek() + l.i("run next: \(task?.id.uuidString ?? "no task")") + task?.block() + } + } } open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { let l = L(category: #file) - - typealias NonFailureSubject = PassthroughSubject - - struct TaskID { - let id: UUID - let task: () -> Void - } - - var discoveredServicesQueue = SingleTaskQueue() - var discoveredCharacteristicsQueue = Queue() - var discoveredDescriptorsQueue = Queue() - - // MARK: Discovering Services + + typealias NonFailureSubject = PassthroughSubject + + struct TaskID { + let id: UUID + let task: () -> () + } + + var discoveredServicesQueue = SingleTaskQueue() + var discoveredCharacteristicsQueue = Queue() + var discoveredDescriptorsQueue = Queue() + + // MARK: Discovering Services let discoveredServicesSubject = NonFailureSubject< - BluetoothOperationResult<[CBService]?> - >() - - /* + BluetoothOperationResult<[CBService]?> + >() + + /* let discoveredIncludedServicesSubject = PassthroughSubject< BluetoothOperationResult<(CBService, [CBService]?)>, Never >() */ - - // MARK: Discovering Characteristics and their Descriptors + + // MARK: Discovering Characteristics and their Descriptors let discoveredCharacteristicsSubject = NonFailureSubject< - BluetoothOperationResult<(CBService, [CBCharacteristic]?)> + BluetoothOperationResult<(CBService, [CBCharacteristic]?)> >() let discoveredDescriptorsSubject = NonFailureSubject< - BluetoothOperationResult<(CBCharacteristic, [CBDescriptor]?)> + BluetoothOperationResult<(CBCharacteristic, [CBDescriptor]?)> >() // MARK: Retrieving Characteristic and Descriptor Values @@ -97,6 +97,8 @@ open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { let updatedDescriptorValuesSubject = PassthroughSubject< (CBDescriptor, Error?), Never >() + + let isReadyToSendWriteWithoutResponseSubject = PassthroughSubject() let writtenCharacteristicValuesSubject = PassthroughSubject< (CBCharacteristic, Error?), Never @@ -112,25 +114,25 @@ open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { // MARK: Monitoring Changes to a Peripheral’s Name or Services let updateNameSubject = PassthroughSubject() - let modifyServicesSubject = PassthroughSubject<[CBService], Never>() - - let readRSSISubject = PassthroughSubject<(NSNumber, Error?), Never>() - - // MARK: - Channels - + let modifyServicesSubject = PassthroughSubject<[CBService], Never>() + + let readRSSISubject = PassthroughSubject<(NSNumber, Error?), Never>() + + // MARK: - Channels + + // MARK: Discovering Services open func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - let operation = discoveredServicesQueue.dequeue()! - - let result = BluetoothOperationResult<[CBService]?>( - value: peripheral.services, error: error, id: operation.id) - - discoveredServicesSubject.send(result) - discoveredServicesQueue.runNext() + let operation = discoveredServicesQueue.dequeue()! + + let result = BluetoothOperationResult<[CBService]?>(value: peripheral.services, error: error, id: operation.id) + + discoveredServicesSubject.send(result) + discoveredServicesQueue.runNext() } - /* + /* public func peripheral( _ peripheral: CBPeripheral, didDiscoverIncludedServicesFor service: CBService, error: Error? @@ -144,11 +146,10 @@ open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { open func peripheral( _ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error? - ) { - let operationId = discoveredCharacteristicsQueue.dequeue()! - let result = BluetoothOperationResult<(CBService, [CBCharacteristic]?)>( - value: (service, service.characteristics), error: error, id: operationId) - + ) { + let operationId = discoveredCharacteristicsQueue.dequeue()! + let result = BluetoothOperationResult<(CBService, [CBCharacteristic]?)>(value: (service, service.characteristics), error: error, id: operationId) + discoveredCharacteristicsSubject.send(result) } @@ -156,11 +157,9 @@ open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { _ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error? ) { - let operationId = discoveredDescriptorsQueue.dequeue()! - let result = BluetoothOperationResult<(CBCharacteristic, [CBDescriptor]?)>( - value: (characteristic, characteristic.descriptors), error: error, - id: operationId) - + let operationId = discoveredDescriptorsQueue.dequeue()! + let result = BluetoothOperationResult<(CBCharacteristic, [CBDescriptor]?)>(value: (characteristic, characteristic.descriptors), error: error, id: operationId) + discoveredDescriptorsSubject.send(result) } @@ -196,9 +195,8 @@ open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { } open func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { - l.i(#function) - fatalError() - } + isReadyToSendWriteWithoutResponseSubject.send(()) + } // MARK: Managing Notifications for a Characteristic’s Value @@ -214,7 +212,7 @@ open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { open func peripheral( _ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error? ) { - readRSSISubject.send((RSSI, error)) + readRSSISubject.send((RSSI, error)) } // MARK: Monitoring Changes to a Peripheral’s Name or Services @@ -226,11 +224,11 @@ open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { open func peripheral( _ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService] ) { - modifyServicesSubject.send(invalidatedServices) + modifyServicesSubject.send(invalidatedServices) } // MARK: Monitoring L2CAP Channels - /* +/* public func peripheral( _ peripheral: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error? ) { diff --git a/Sources/iOS-BLE-Library-Mock/Utilities/Logger.swift b/Sources/iOS-BLE-Library-Mock/Utilities/Logger.swift index f1c7036..bf397ec 100644 --- a/Sources/iOS-BLE-Library-Mock/Utilities/Logger.swift +++ b/Sources/iOS-BLE-Library-Mock/Utilities/Logger.swift @@ -10,76 +10,76 @@ import os @available(iOS 14.0, macOS 11, *) private struct Loggers { - static var loggers: [UUID: Logger] = [:] + static var loggers: [UUID : Logger] = [:] } struct L { - @inline(__always) - static let enabled: Bool = false - - let subsystem: String - let category: String - - private let shouldLog: Bool - - private let id = UUID() - - init( - subsystem: String = "com.nordicsemi.ios_ble_library", category: String, - enabled: Bool = Self.enabled - ) { - self.subsystem = subsystem - self.category = category - self.shouldLog = enabled - - if #available(iOS 14, macOS 11, *) { - Loggers.loggers[self.id] = Logger(subsystem: subsystem, category: category) - } - } - - func i(_ msg: String) { - #if DEBUG - if !shouldLog { return } - - if #available(iOS 14, macOS 11, *) { - Loggers.loggers[id]?.info("\(msg)") - } else { - os_log("%@", type: .info, msg) - } - - #endif - } - - func d(_ msg: String) { - #if DEBUG - if !shouldLog { return } - if #available(iOS 14, macOS 11, *) { - Loggers.loggers[id]?.debug("\(msg)") - } else { - os_log("%@", type: .debug, msg) - } - #endif - } - - func e(_ msg: String) { - #if DEBUG - if !shouldLog { return } - if #available(iOS 14, macOS 11, *) { - Loggers.loggers[id]?.error("\(msg)") - } else { - os_log("%@", type: .error, msg) - } - #endif - } - - func f(_ msg: String) { - #if DEBUG - if !shouldLog { return } - if #available(iOS 14, macOS 11, *) { - Loggers.loggers[id]?.fault("\(msg)") - } else { - os_log("%@", type: .fault, msg) - } - #endif - } + @inline(__always) + static let enabled: Bool = false + + let subsystem: String + let category: String + + private let shouldLog: Bool + + private let id = UUID() + + init( + subsystem: String = "com.nordicsemi.ios_ble_library", category: String, + enabled: Bool = Self.enabled + ) { + self.subsystem = subsystem + self.category = category + self.shouldLog = enabled + + if #available(iOS 14, macOS 11, *) { + Loggers.loggers[self.id] = Logger(subsystem: subsystem, category: category) + } + } + + func i(_ msg: String) { +#if DEBUG + if !shouldLog { return } + + if #available(iOS 14, macOS 11, *) { + Loggers.loggers[id]?.info("\(msg)") + } else { + os_log("%@", type: .info, msg) + } + +#endif + } + + func d(_ msg: String) { +#if DEBUG + if !shouldLog { return } + if #available(iOS 14, macOS 11, *) { + Loggers.loggers[id]?.debug("\(msg)") + } else { + os_log("%@", type: .debug, msg) + } +#endif + } + + func e(_ msg: String) { +#if DEBUG + if !shouldLog { return } + if #available(iOS 14, macOS 11, *) { + Loggers.loggers[id]?.error("\(msg)") + } else { + os_log("%@", type: .error, msg) + } +#endif + } + + func f(_ msg: String) { +#if DEBUG + if !shouldLog { return } + if #available(iOS 14, macOS 11, *) { + Loggers.loggers[id]?.fault("\(msg)") + } else { + os_log("%@", type: .fault, msg) + } +#endif + } } diff --git a/Sources/iOS-BLE-Library-Mock/Utilities/Publishers/Publishers+Bluetooth.swift b/Sources/iOS-BLE-Library-Mock/Utilities/Publishers/Publishers+Bluetooth.swift index 6c86d86..bb41d71 100644 --- a/Sources/iOS-BLE-Library-Mock/Utilities/Publishers/Publishers+Bluetooth.swift +++ b/Sources/iOS-BLE-Library-Mock/Utilities/Publishers/Publishers+Bluetooth.swift @@ -17,16 +17,16 @@ extension Publisher { } extension Publishers { - - /** + + /** A publisher that is used for most of the Bluetooth operations. - + # Overview This publisher conforms to the `ConnectablePublisher` protocol because most of the Bluetooth operations have to be set up before they can be used. - + It means that the publisher will not emit any values until it is connected. The connection is established by calling the `connect()` or `autoconnect()` methods. To learn more about the `ConnectablePublisher` protocol, see [Apple's documentation](https://developer.apple.com/documentation/combine/connectablepublisher). - + ```swift let publisher = centralManager.scanForPeripherals(withServices: nil) .autoconnect() diff --git a/Sources/iOS-BLE-Library-Mock/Utilities/Queue.swift b/Sources/iOS-BLE-Library-Mock/Utilities/Queue.swift index e514773..c9c5f74 100644 --- a/Sources/iOS-BLE-Library-Mock/Utilities/Queue.swift +++ b/Sources/iOS-BLE-Library-Mock/Utilities/Queue.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Nick Kibysh on 03/11/2023. // @@ -8,58 +8,58 @@ import Foundation class Node { - var value: T - var next: Node? + var value: T + var next: Node? - init(value: T) { - self.value = value - } + init(value: T) { + self.value = value + } } class Queue { - private var front: Node? - private var rear: Node? - private let accessQueue = DispatchQueue(label: "com.ble-library.threadSafeQueue") + private var front: Node? + private var rear: Node? + private let accessQueue = DispatchQueue(label: "com.ble-library.threadSafeQueue") - var isEmpty: Bool { - return front == nil - } + var isEmpty: Bool { + return front == nil + } - // Enqueue operation to add an element to the rear of the queue - func enqueue(_ value: T) { - accessQueue.sync { - let newNode = Node(value: value) - if isEmpty { - front = newNode - rear = newNode - } else { - rear?.next = newNode - rear = newNode - } - } - } + // Enqueue operation to add an element to the rear of the queue + func enqueue(_ value: T) { + accessQueue.sync { + let newNode = Node(value: value) + if isEmpty { + front = newNode + rear = newNode + } else { + rear?.next = newNode + rear = newNode + } + } + } - // Dequeue operation to remove and return the element from the front of the queue - func dequeue() -> T? { - var element: T? - accessQueue.sync { - if let currentFront = front { - front = currentFront.next - if front == nil { - rear = nil - } - element = currentFront.value - } else { - element = nil - } - } - return element - } + // Dequeue operation to remove and return the element from the front of the queue + func dequeue() -> T? { + var element: T? + accessQueue.sync { + if let currentFront = front { + front = currentFront.next + if front == nil { + rear = nil + } + element = currentFront.value + } else { + element = nil + } + } + return element + } - // Peek operation to get the value at the front of the queue without removing it - func peek() -> T? { - return front?.value - } + // Peek operation to get the value at the front of the queue without removing it + func peek() -> T? { + return front?.value + } } /* struct Queue { @@ -81,7 +81,7 @@ struct Queue { } return element } - + var head: T? { var element: T? accessQueue.sync { diff --git a/Sources/iOS-BLE-Library/Peripheral/Peripheral+Writer.swift b/Sources/iOS-BLE-Library/Peripheral/Peripheral+Writer.swift index 9d1a6de..9270e17 100644 --- a/Sources/iOS-BLE-Library/Peripheral/Peripheral+Writer.swift +++ b/Sources/iOS-BLE-Library/Peripheral/Peripheral+Writer.swift @@ -49,6 +49,30 @@ extension Peripheral { super.init(peripheral: peripheral) } } + + class DescriptorWriter: OperationQueue { + let writtenEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never> + + init( + writtenEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never>, + peripheral: CBPeripheral + ) { + self.writtenEventsPublisher = writtenEventsPublisher + super.init(peripheral: peripheral) + } + } + + class DescriptorReader: OperationQueue { + let updateEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never> + + init( + updateEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never>, + peripheral: CBPeripheral + ) { + self.updateEventsPublisher = updateEventsPublisher + super.init(peripheral: peripheral) + } + } } extension Peripheral.CharacteristicWriter { @@ -79,6 +103,35 @@ extension Peripheral.CharacteristicReader { } } +extension Peripheral.DescriptorWriter { + func write(_ value: Data, to dsecriptor: CBDescriptor) -> Future { + let operation = WriteDescriptorOperation( + data: value, + writtenEventsPublisher: writtenEventsPublisher, + descriptor: dsecriptor, + peripheral: peripheral + ) + + queue.addOperation(operation) + + return operation.future + } +} + +extension Peripheral.DescriptorReader { + func readValue(from descriptor: CBDescriptor) -> Future { + let operation = ReadDescriptorOperation( + updateEventPublisher: updateEventsPublisher, + descriptor: descriptor, + peripheral: peripheral + ) + + queue.addOperation(operation) + + return operation.future + } +} + private class BasicOperation: Operation { let peripheral: CBPeripheral var cancelable: AnyCancellable? @@ -228,5 +281,105 @@ private class ReadCharacteristicOperation: BasicOperation { state = .executing main() } +} + +private class WriteDescriptorOperation: BasicOperation { + + let writtenEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never> + let descriptor: CBDescriptor + + let data: Data + + init( + data: Data, writtenEventsPublisher: AnyPublisher<(CBDescriptor, Error?), Never>, + descriptor: CBDescriptor, peripheral: CBPeripheral + ) { + self.data = data + self.writtenEventsPublisher = writtenEventsPublisher + self.descriptor = descriptor + super.init(peripheral: peripheral) + } + + override func main() { + peripheral.writeValue(data, for: descriptor) + } + + override func start() { + if isCancelled { + state = .finished + return + } + + self.cancelable = writtenEventsPublisher.share() + .filter { $0.0.uuid == self.descriptor.uuid && $0.0.characteristic?.uuid == self.descriptor.characteristic?.uuid } + .first() + .tryMap { v in + if let e = v.1 { + throw e + } else { + return v.0 + } + } + .sink { [unowned self] completion in + switch completion { + case .finished: + self.promise?(.success(())) + case .failure(let e): + self.promise?(.failure(e)) + } + self.state = .finished + } receiveValue: { _ in + + } + + state = .executing + main() + } +} +private class ReadDescriptorOperation: BasicOperation { + let updateEventPublisher: AnyPublisher<(CBDescriptor, Error?), Never> + let descriptor: CBDescriptor + + init( + updateEventPublisher: AnyPublisher<(CBDescriptor, Error?), Never>, + descriptor: CBDescriptor, peripheral: CBPeripheral + ) { + self.updateEventPublisher = updateEventPublisher + self.descriptor = descriptor + super.init(peripheral: peripheral) + } + + override func main() { + peripheral.readValue(for: descriptor) + } + + override func start() { + if isCancelled { + state = .finished + return + } + + self.cancelable = updateEventPublisher.share() + .filter { $0.0.uuid == self.descriptor.uuid && $0.0.characteristic?.uuid == self.descriptor.characteristic?.uuid } + .first() + .tryMap { v in + if let e = v.1 { + throw e + } else { + return v.0.value + } + } + .sink { [unowned self] completion in + if case .failure(let e) = completion { + self.promise?(.failure(e)) + } + self.state = .finished + } receiveValue: { v in + self.promise?(.success(v)) + } + + state = .executing + main() + } } diff --git a/Sources/iOS-BLE-Library/Peripheral/Peripheral.swift b/Sources/iOS-BLE-Library/Peripheral/Peripheral.swift index 51c57e5..2772791 100644 --- a/Sources/iOS-BLE-Library/Peripheral/Peripheral.swift +++ b/Sources/iOS-BLE-Library/Peripheral/Peripheral.swift @@ -100,17 +100,27 @@ public class Peripheral { private let stateSubject = CurrentValueSubject(.disconnected) private var observer: Observer! - private lazy var writer = CharacteristicWriter( + private lazy var characteristicWriter = CharacteristicWriter( writtenEventsPublisher: self.peripheralDelegate.writtenCharacteristicValuesSubject .eraseToAnyPublisher(), peripheral: self.peripheral ) - private lazy var reader = CharacteristicReader( + private lazy var characteristicReader = CharacteristicReader( updateEventPublisher: self.peripheralDelegate.updatedCharacteristicValuesSubject .eraseToAnyPublisher(), peripheral: peripheral ) + + private lazy var descriptorWriter = DescriptorWriter( + writtenEventsPublisher: self.peripheralDelegate.writtenDescriptorValuesSubject.eraseToAnyPublisher(), + peripheral: peripheral + ) + + private lazy var descriptorReader = DescriptorReader( + updateEventsPublisher: self.peripheralDelegate.updatedDescriptorValuesSubject.eraseToAnyPublisher(), + peripheral: peripheral + ) // TODO: Why don't we use default delegate? /// Initializes a Peripheral instance. @@ -275,7 +285,7 @@ extension Peripheral { /// - Parameter characteristic: The characteristic to read from. /// - Returns: A future emitting the read data or an error. public func readValue(for characteristic: CBCharacteristic) -> Future { - return reader.readValue(from: characteristic) + return characteristicReader.readValue(from: characteristic) } /// Listen for updates to the value of a characteristic. @@ -300,8 +310,8 @@ extension Peripheral { /// /// - Parameter descriptor: The descriptor to read from. /// - Returns: A future emitting the read data or an error. - public func readValue(for descriptor: CBDescriptor) -> Future { - fatalError() + public func readValue(for descriptor: CBDescriptor) -> Future { + return descriptorReader.readValue(from: descriptor) } } @@ -347,8 +357,8 @@ extension Peripheral { /// - Parameters: /// - data: The data to write. /// - descriptor: The descriptor to write to. - public func writeValue(_ data: Data, for descriptor: CBDescriptor) { - fatalError() + public func writeValue(_ data: Data, for descriptor: CBDescriptor) -> Future { + return descriptorWriter.write(data, to: descriptor) } } @@ -405,3 +415,107 @@ extension Peripheral { .eraseToAnyPublisher() } } + +// MARK: - Channels +extension Peripheral { + /// A publisher that emits the discovered services of the peripheral. + public var discoveredServicesChannel: AnyPublisher<[CBService]?, Error> { + peripheralDelegate.discoveredServicesSubject + .tryMap { result in + if let e = result.error { + throw e + } else { + return result.value + } + } + .eraseToAnyPublisher() + } + + /// A publisher that emits the discovered characteristics of a service. + public var discoveredCharacteristicsChannel: AnyPublisher<(CBService, [CBCharacteristic]?)?, Error> { + peripheralDelegate.discoveredCharacteristicsSubject + .tryMap { result in + if let e = result.error { + throw e + } else { + return result.value + } + } + .eraseToAnyPublisher() + } + + /// A publisher that emits the discovered descriptors of a characteristic. + public var discoveredDescriptorsChannel: AnyPublisher<(CBCharacteristic, [CBDescriptor]?)?, Error> { + peripheralDelegate.discoveredDescriptorsSubject + .tryMap { result in + if let e = result.error { + throw e + } else { + return result.value + } + } + .eraseToAnyPublisher() + } + + /// A publisher that emits the updated value of a characteristic. + public var updatedCharacteristicValuesChannel: AnyPublisher<(CBCharacteristic, Error?), Never> { + peripheralDelegate.updatedCharacteristicValuesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the updated value of a descriptor. + public var updatedDescriptorValuesChannel: AnyPublisher<(CBDescriptor, Error?), Never> { + peripheralDelegate.updatedDescriptorValuesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the written value of a characteristic. + public var writtenCharacteristicValuesChannel: AnyPublisher<(CBCharacteristic, Error?), Never> { + peripheralDelegate.writtenCharacteristicValuesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the written value of a descriptor. + public var writtenDescriptorValuesChannel: AnyPublisher<(CBDescriptor, Error?), Never> { + peripheralDelegate.writtenDescriptorValuesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the notification state of a characteristic. + public var notificationStateChannel: AnyPublisher<(CBCharacteristic, Error?), Never> { + peripheralDelegate.notificationStateSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the update name of a peripheral. + public var updateNameChannel: AnyPublisher { + peripheralDelegate.updateNameSubject + .eraseToAnyPublisher() + } + + public var modifyServices: AnyPublisher<[CBService], Never> { + peripheralDelegate.modifyServicesSubject + .eraseToAnyPublisher() + } + + /// A publisher that emits the read RSSI value of a peripheral. + public var readRSSIChannel: AnyPublisher { + peripheralDelegate.readRSSISubject + .tryMap { rssi in + if let error = rssi.1 { + throw error + } else { + return rssi.0 + } + } + .eraseToAnyPublisher() + } + + /// A publisher that emits the isReadyToSendWriteWithoutResponse value of a peripheral. + public var isReadyToSendWriteWithoutResponseChannel: AnyPublisher { + peripheralDelegate.isReadyToSendWriteWithoutResponseSubject + .first() + .eraseToAnyPublisher() + } + +} diff --git a/Sources/iOS-BLE-Library/Peripheral/ReactivePeripheralDelegate.swift b/Sources/iOS-BLE-Library/Peripheral/ReactivePeripheralDelegate.swift index 74b9929..9026ba5 100644 --- a/Sources/iOS-BLE-Library/Peripheral/ReactivePeripheralDelegate.swift +++ b/Sources/iOS-BLE-Library/Peripheral/ReactivePeripheralDelegate.swift @@ -103,6 +103,8 @@ open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { let updatedDescriptorValuesSubject = PassthroughSubject< (CBDescriptor, Error?), Never >() + + let isReadyToSendWriteWithoutResponseSubject = PassthroughSubject() let writtenCharacteristicValuesSubject = PassthroughSubject< (CBCharacteristic, Error?), Never @@ -199,9 +201,8 @@ open class ReactivePeripheralDelegate: NSObject, CBPeripheralDelegate { } open func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { - l.i(#function) - fatalError() - } + isReadyToSendWriteWithoutResponseSubject.send(()) + } // MARK: Managing Notifications for a Characteristic’s Value diff --git a/Tests/iOS-BLE-LibraryTests/CentralManagerTests.swift b/Tests/iOS-BLE-LibraryTests/CentralManagerTests.swift index 437601a..ebfa312 100644 --- a/Tests/iOS-BLE-LibraryTests/CentralManagerTests.swift +++ b/Tests/iOS-BLE-LibraryTests/CentralManagerTests.swift @@ -32,13 +32,13 @@ final class CentralManagerTests: XCTestCase { } override func tearDownWithError() throws { - try super.tearDownWithError() - cancelables.removeAll() cancelables = nil central = nil rs = nil CBMCentralManagerMock.tearDownSimulation() + + try super.tearDownWithError() } func testCentralManagerCreation() throws { @@ -144,7 +144,7 @@ final class CentralManagerTests: XCTestCase { func testConnect() async throws { let connectionPeripheral = try await central.scanForPeripherals(withServices: nil) - .value + .firstValue .peripheral let connectionExpectation = XCTestExpectation(description: "Connection expectation") @@ -180,7 +180,7 @@ final class CentralManagerTests: XCTestCase { func testDisconnectFromPeripheral() async throws { let connectionPeripheral = try await central.scanForPeripherals(withServices: nil) - .value + .firstValue .peripheral let connectionExpectation = XCTestExpectation(description: "Connection expectation") diff --git a/Tests/iOS-BLE-LibraryTests/PeripheralMultitaskingTests.swift b/Tests/iOS-BLE-LibraryTests/PeripheralMultitaskingTests.swift index b5c97e1..748459b 100644 --- a/Tests/iOS-BLE-LibraryTests/PeripheralMultitaskingTests.swift +++ b/Tests/iOS-BLE-LibraryTests/PeripheralMultitaskingTests.swift @@ -17,10 +17,18 @@ private extension CBMUUID { static let heartRateMonitorService = CBUUID(string: "180D") static let deviceInformationService = CBUUID(string: "180A") - static var all: [CBMUUID] = [.batteryService, .runningSpeedCadenceService, .heartRateMonitorService, .deviceInformationService] + static var allServices: [CBMUUID] = [ + .batteryService, + .runningSpeedCadenceService, + .heartRateMonitorService, + .deviceInformationService + ] + + static let batteryLevel = CBUUID(string: "2A19") + static let presentationFormat = CBUUID(string: "2904") } -class MockPeripheral: CBMPeripheralSpecDelegate { +private class MockPeripheral: CBMPeripheralSpecDelegate { public private (set) lazy var peripheral = CBMPeripheralSpec .simulatePeripheral(proximity: .far) .advertising( @@ -35,7 +43,18 @@ class MockPeripheral: CBMPeripheralSpecDelegate { ) .connectable( name: "Running Sensor", - services: CBMUUID.all.map { CBMServiceMock(type: $0, primary: true) }, + services: CBMUUID.allServices.map { + if $0 == .batteryService { + return CBMServiceMock( + type: $0, + primary: true, + characteristics: [ + CBMCharacteristicMock(type: .batteryLevel, properties: .read, descriptors: CBMDescriptorMock(type: .presentationFormat)) + ]) + } else { + return CBMServiceMock(type: $0, primary: true) + } + }, delegate: self ) .build() @@ -43,12 +62,16 @@ class MockPeripheral: CBMPeripheralSpecDelegate { func peripheral(_ peripheral: CBMPeripheralSpec, didReceiveServiceDiscoveryRequest serviceUUIDs: [CBMUUID]?) -> Result { return .success(()) } + + func peripheral(_ peripheral: CBMPeripheralSpec, didReceiveCharacteristicsDiscoveryRequest characteristicUUIDs: [CBMUUID]?, for service: CBMServiceMock) -> Result { + return .success(()) + } } final class PeripheralMultitaskingTests: XCTestCase { var cancelables: Set! var central: CentralManager! - var rs: MockPeripheral! + private var rs: MockPeripheral! override func setUpWithError() throws { try super.setUpWithError() @@ -65,11 +88,16 @@ final class PeripheralMultitaskingTests: XCTestCase { cancelables = Set() } + override func tearDown() async throws { + CBMCentralManagerMock.tearDownSimulation() + try await super.tearDown() + } + func testDiscoverServices() async throws { let p = try await central.scanForPeripherals(withServices: nil) .flatMap { self.central.connect($0.peripheral) } .map { Peripheral(peripheral: $0, delegate: ReactivePeripheralDelegate()) } - .value + .firstValue let batteryExp = expectation(description: "Battery Service Expectation") let hrExp = expectation(description: "Heart Rate Service Expectation") @@ -86,7 +114,7 @@ final class PeripheralMultitaskingTests: XCTestCase { } .store(in: &cancelables) - p.discoverServices(serviceUUIDs: [.deviceInformationService]) + p.discoverServices(serviceUUIDs: [.batteryService]) .sink { completion in if case .failure = completion { XCTFail("Should not fail") @@ -100,4 +128,29 @@ final class PeripheralMultitaskingTests: XCTestCase { await fulfillment(of: [batteryExp, hrExp], timeout: 4) } + + func testDiscoverCharacteristics() async throws { + let p = try await central.scanForPeripherals(withServices: nil) + .flatMap { self.central.connect($0.peripheral) } + .map { Peripheral(peripheral: $0, delegate: ReactivePeripheralDelegate()) } + .firstValue + + let batteryService = try await p.discoverServices(serviceUUIDs: [.batteryService ]).firstValue.first! + + let characteristicExp = expectation(description: "Characteristic expectation") + let descriptorExp = expectation(description: "Descriptor expectation") + + Task { + let ch = try await p.discoverCharacteristics([.batteryLevel], for: batteryService).firstValue.first! + characteristicExp.fulfill() + + let desc = try await p.discoverDescriptors(for: ch).firstValue.first! + descriptorExp.fulfill() + + XCTAssert(ch.uuid == CBUUID.batteryLevel) + XCTAssert(desc.uuid == CBUUID.presentationFormat) + } + + await fulfillment(of: [characteristicExp, descriptorExp], timeout: 4) + } } diff --git a/Tests/iOS-BLE-LibraryTests/PeripheralReadWriteDescriptorTests.swift b/Tests/iOS-BLE-LibraryTests/PeripheralReadWriteDescriptorTests.swift new file mode 100644 index 0000000..d8a3290 --- /dev/null +++ b/Tests/iOS-BLE-LibraryTests/PeripheralReadWriteDescriptorTests.swift @@ -0,0 +1,328 @@ +// +// PeripheralReadWriteDescriptorTests.swift +// +// +// Created by Nick Kibysh on 23/02/2024. +// + +import Combine +import CoreBluetoothMock +import XCTest + +@testable import iOS_BLE_Library_Mock + +private extension CBMUUID { + static let runningSpeedCadenceService = CBUUID(string: "1814") + static let batteryService = CBUUID(string: "180F") + static let heartRateMonitorService = CBUUID(string: "180D") + static let deviceInformationService = CBUUID(string: "180A") + + static var allServices: [CBMUUID] = [ + .batteryService, + .runningSpeedCadenceService, + .heartRateMonitorService, + .deviceInformationService + ] + + static let batteryLevel = CBUUID(string: "2A19") + static let presentationFormat = CBUUID(string: "2904") +} + +private class MockPeripheral: CBMPeripheralSpecDelegate { + + private var batteryLevel: UInt8 = 0 + private (set) var lastWroteCommandValue: Data? + + public private (set) lazy var peripheral = CBMPeripheralSpec + .simulatePeripheral(proximity: .far) + .advertising( + advertisementData: [ + CBAdvertisementDataIsConnectable : true as NSNumber, + CBAdvertisementDataLocalNameKey : "Running Speed and Cadence sensor", + CBAdvertisementDataServiceUUIDsKey : [CBMUUID.runningSpeedCadenceService] + ], + withInterval: 2.0, + delay: 5.0, + alsoWhenConnected: false + ) + .connectable( + name: "Running Sensor", + services: CBMUUID.allServices.map { + if $0 == .batteryService { + return CBMServiceMock( + type: $0, + primary: true, + characteristics: [ + CBMCharacteristicMock(type: .batteryLevel, properties: .indicate, descriptors: CBMDescriptorMock(type: .presentationFormat)) + ]) + } else { + return CBMServiceMock(type: $0, primary: true) + } + }, + delegate: self + ) + .build() + + func peripheral(_ peripheral: CBMPeripheralSpec, didReceiveServiceDiscoveryRequest serviceUUIDs: [CBMUUID]?) -> Result { + return .success(()) + } + + func peripheral(_ peripheral: CBMPeripheralSpec, didReceiveCharacteristicsDiscoveryRequest characteristicUUIDs: [CBMUUID]?, for service: CBMServiceMock) -> Result { + return .success(()) + } + + func peripheral(_ peripheral: CBMPeripheralSpec, didReceiveReadRequestFor characteristic: CBMCharacteristicMock) -> Result { + defer { batteryLevel += 1 } + + if batteryLevel % 2 == 0 { + return .failure(NSError(domain: "com.ble.characteristic", code: 1, userInfo: ["value":batteryLevel])) + } else { + return .success(Data([batteryLevel])) + } + } + + func peripheral(_ peripheral: CBMPeripheralSpec, didReceiveWriteCommandFor characteristic: CBMCharacteristicMock, data: Data) { + lastWroteCommandValue = data + } + + func peripheral(_ peripheral: CBMPeripheralSpec, didReceiveWriteRequestFor characteristic: CBMCharacteristicMock, data: Data) -> Result { + defer { lastWroteCommandValue = data } + + if data[0] % 2 == 0 { + return .failure(NSError(domain: "com.ble.characteristic", code: 2, userInfo: ["value": data])) + } else { + peripheral.simulateValueUpdate(data, for: characteristic) + return .success(()) + } + } + + func peripheral(_ peripheral: CBMPeripheralSpec, didReceiveReadRequestFor descriptor: CBMDescriptorMock) -> Result { + defer { batteryLevel += 1 } + + if batteryLevel % 2 == 0 { + let e = NSError(domain: "com.ble.characteristic", code: 3, userInfo: ["value": batteryLevel]) + return .failure(e) + } else { + return .success(Data([batteryLevel])) + } + } + + func peripheral(_ peripheral: CBMPeripheralSpec, didReceiveWriteRequestFor descriptor: CBMDescriptorMock, data: Data) -> Result { + if data[0] % 2 == 0 { + let e = NSError(domain: "com.ble.characteristic", code: 3, userInfo: ["value": data]) + return .failure(e) + } else { + lastWroteCommandValue = data + return .success(()) + } + } +} + +final class PeripheralReadWriteDescriptorTests: XCTestCase { + var cancelables: Set! + var central: CentralManager! + private var rs: MockPeripheral! + + override func setUpWithError() throws { + try super.setUpWithError() + + self.rs = MockPeripheral() + + CBMCentralManagerMock.simulateInitialState(.poweredOn) + CBMCentralManagerMock.simulatePeripherals([rs.peripheral]) + + let cmd = ReactiveCentralManagerDelegate() + let cm = CBCentralManagerFactory.instance(delegate: cmd, queue: .main, forceMock: true) + self.central = try CentralManager(centralManager: cm) + + cancelables = Set() + } + + override func tearDown() async throws { + CBMCentralManagerMock.tearDownSimulation() + try await super.tearDown() + } + + func testReadCharacteristic() async throws { + let p = try await central.scanForPeripherals(withServices: nil) + .flatMap { self.central.connect($0.peripheral) } + .map { Peripheral(peripheral: $0, delegate: ReactivePeripheralDelegate()) } + .firstValue + + let batteryService = try await p.discoverServices(serviceUUIDs: [.batteryService ]).firstValue.first! + let batteryLevelCharacteristic = try await p.discoverCharacteristics([.batteryLevel], for: batteryService).firstValue.first! + + do { + _ = try await p.readValue(for: batteryLevelCharacteristic).firstValue + XCTFail("Error should be thrown") + } catch let e as NSError { + let level = try XCTUnwrap(e.userInfo["value"] as? UInt8) + XCTAssertEqual(level, 0) + XCTAssertEqual(e.code, 1) + } catch { + XCTFail("Unexpected Error: \(error.localizedDescription)") + } + + do { + let data = try await p.readValue(for: batteryLevelCharacteristic).firstValue + let level = try XCTUnwrap(data?[0] as? UInt8) + XCTAssertEqual(level, 1) + } catch { + XCTFail("Unexpected Error: \(error.localizedDescription)") + } + } + + func testWriteWithoutResponse() async throws { + let delegate = ReactivePeripheralDelegate() + + let p = try await central.scanForPeripherals(withServices: nil) + .flatMap { self.central.connect($0.peripheral) } + .map { Peripheral(peripheral: $0, delegate: delegate) } + .firstValue + + let batteryService = try await p.discoverServices(serviceUUIDs: [.batteryService ]).firstValue.first! + let batteryLevelCharacteristic = try await p.discoverCharacteristics([.batteryLevel], for: batteryService).firstValue.first! + + let isReadyExp = expectation(description: "Peripheral is ready to write value without response") + p.isReadyToSendWriteWithoutResponseChannel + .sink { _ in + isReadyExp.fulfill() + } receiveValue: { _ in + } + .store(in: &cancelables) + + + let data = Data([1, 2, 3]) + p.writeValueWithoutResponse(data, for: batteryLevelCharacteristic) + XCTAssertEqual(data, rs.lastWroteCommandValue) + + await fulfillment(of: [isReadyExp], timeout: 3) + } + + func testWriteWithResponse() async throws { + let delegate = ReactivePeripheralDelegate() + + let p = try await central.scanForPeripherals(withServices: nil) + .flatMap { self.central.connect($0.peripheral) } + .map { Peripheral(peripheral: $0, delegate: delegate) } + .firstValue + + let batteryService = try await p.discoverServices(serviceUUIDs: [.batteryService ]).firstValue.first! + let batteryLevelCharacteristic = try await p.discoverCharacteristics([.batteryLevel], for: batteryService).firstValue.first! + + let sendData = Data([0]) + do { + _ = try await p.writeValueWithResponse(sendData, for: batteryLevelCharacteristic).firstValue + XCTFail("Error should be thrown") + } catch let e as NSError { + let level = try XCTUnwrap(e.userInfo["value"] as? Data) + XCTAssertEqual(level, sendData) + XCTAssertEqual(e.code, 2) + } catch { + XCTFail("Unexpected Error: \(error.localizedDescription)") + } + + do { + _ = try await p.writeValueWithResponse(Data([1]), for: batteryLevelCharacteristic).firstValue + } catch { + XCTFail("Unexpected Error: \(error.localizedDescription)") + } + } + + func testNotifyCharacteristic() async throws { + let delegate = ReactivePeripheralDelegate() + + let p = try await central.scanForPeripherals(withServices: nil) + .flatMap { self.central.connect($0.peripheral) } + .map { Peripheral(peripheral: $0, delegate: delegate) } + .firstValue + + let batteryService = try await p.discoverServices(serviceUUIDs: [.batteryService ]).firstValue.first! + let batteryLevelCharacteristic = try await p.discoverCharacteristics([.batteryLevel], for: batteryService).firstValue.first! + + let exp = expectation(description: "write response expectation") + + p.listenValues(for: batteryLevelCharacteristic) + .sink { _ in + + } receiveValue: { data in + XCTAssertEqual(data, Data([1])) + exp.fulfill() + } + .store(in: &cancelables) + + do { + _ = try await p.setNotifyValue(true, for: batteryLevelCharacteristic).firstValue + _ = try await p.writeValueWithResponse(Data([1]), for: batteryLevelCharacteristic).firstValue + } catch { + XCTFail("Unexpected Error: \(error.localizedDescription)") + } + + await fulfillment(of: [exp], timeout: 3) + } + + func testReadDescriptor() async throws { + let delegate = ReactivePeripheralDelegate() + + let p = try await central.scanForPeripherals(withServices: nil) + .flatMap { self.central.connect($0.peripheral) } + .map { Peripheral(peripheral: $0, delegate: delegate) } + .firstValue + + let batteryService = try await p.discoverServices(serviceUUIDs: [.batteryService ]).firstValue.first! + let batteryLevelCharacteristic = try await p.discoverCharacteristics([.batteryLevel], for: batteryService).firstValue.first! + let presentationFormat = try await p.discoverDescriptors(for: batteryLevelCharacteristic).firstValue.first! + + do { + let v = try await p.readValue(for: presentationFormat).firstValue + XCTFail("Error should be thrown") + } catch let e as NSError { + XCTAssertEqual(e.code, 3) + + let value = try XCTUnwrap(e.userInfo["value"] as? UInt8) + XCTAssertEqual(value, 0) + } catch { + XCTFail("Unexpected Error: \(error.localizedDescription)") + } + + do { + let v = try await p.readValue(for: presentationFormat).firstValue + let bl = try XCTUnwrap(v as? Data) + XCTAssertEqual(1, bl[0]) + } catch { + XCTFail("Unexpected Error: \(error.localizedDescription)") + } + } + + func testWriteDescriptor() async throws { + let delegate = ReactivePeripheralDelegate() + + let p = try await central.scanForPeripherals(withServices: nil) + .flatMap { self.central.connect($0.peripheral) } + .map { Peripheral(peripheral: $0, delegate: delegate) } + .firstValue + + let batteryService = try await p.discoverServices(serviceUUIDs: [.batteryService ]).firstValue.first! + let batteryLevelCharacteristic = try await p.discoverCharacteristics([.batteryLevel], for: batteryService).firstValue.first! + let presentationFormat = try await p.discoverDescriptors(for: batteryLevelCharacteristic).firstValue.first! + + let writeData1 = Data([0]) + do { + try await p.writeValue(writeData1, for: presentationFormat).firstValue + XCTFail("Error should be thrown") + } catch let e as NSError { + XCTAssertEqual(e.code, 3) + let data = try XCTUnwrap(e.userInfo["value"] as? Data) + XCTAssertEqual(data, writeData1) + } catch { + XCTFail("Unexpected Error: \(error.localizedDescription)") + } + + let writeData2 = Data([1]) + do { + try await p.writeValue(writeData2, for: presentationFormat).firstValue + } catch { + XCTFail("Unexpected Error: \(error.localizedDescription)") + } + } +}