// // PeripheralManager.swift // xDripG5 // // Copyright © 2017 LoopKit Authors. All rights reserved. // import CoreBluetooth import Foundation import os.log class PeripheralManager: NSObject { private let log = OSLog(category: "PeripheralManager") /// /// This is mutable, because CBPeripheral instances can seemingly become invalid, and need to be periodically re-fetched from CBCentralManager var peripheral: CBPeripheral { didSet { guard oldValue !== peripheral else { return } log.error("Replacing peripheral reference %{public}@ -> %{public}@", oldValue, peripheral) oldValue.delegate = nil peripheral.delegate = self queue.sync { self.needsConfiguration = true } } } /// The dispatch queue used to serialize operations on the peripheral let queue: DispatchQueue /// The condition used to signal command completion private let commandLock = NSCondition() /// The required conditions for the operation to complete private var commandConditions = [CommandCondition]() /// Any error surfaced during the active operation private var commandError: Error? private(set) weak var central: CBCentralManager? let configuration: Configuration // Confined to `queue` private var needsConfiguration = true weak var delegate: PeripheralManagerDelegate? { didSet { queue.sync { needsConfiguration = true } } } // Called from RileyLinkDeviceManager.managerQueue init(peripheral: CBPeripheral, configuration: Configuration, centralManager: CBCentralManager, queue: DispatchQueue) { self.peripheral = peripheral self.central = centralManager self.configuration = configuration self.queue = queue super.init() peripheral.delegate = self assertConfiguration() } } // MARK: - Nested types extension PeripheralManager { struct Configuration { var serviceCharacteristics: [CBUUID: [CBUUID]] = [:] var notifyingCharacteristics: [CBUUID: [CBUUID]] = [:] var valueUpdateMacros: [CBUUID: (_ manager: PeripheralManager) -> Void] = [:] } enum CommandCondition { case notificationStateUpdate(characteristic: CBCharacteristic, enabled: Bool) case valueUpdate(characteristic: CBCharacteristic, matching: ((Data?) -> Bool)?) case write(characteristic: CBCharacteristic) case discoverServices case discoverCharacteristicsForService(serviceUUID: CBUUID) } } protocol PeripheralManagerDelegate: AnyObject { func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) func peripheralManager(_ manager: PeripheralManager, didUpdateNotificationStateFor characteristic: CBCharacteristic) func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) func peripheralManagerDidUpdateName(_ manager: PeripheralManager) func completeConfiguration(for manager: PeripheralManager) throws } // MARK: - Operation sequence management extension PeripheralManager { func configureAndRun(_ block: @escaping (_ manager: PeripheralManager) -> Void) -> (() -> Void) { return { if self.needsConfiguration || self.peripheral.services == nil { self.log.default("Configuring peripheral %{public}@, needsConfiguration=%{public}@, has services = %{public}@", self.peripheral, String(describing: self.needsConfiguration), String(describing: self.peripheral.services != nil)) do { try self.applyConfiguration() self.log.default("Peripheral configuration completed: %{public}@", self.peripheral) } catch let error { self.log.error("Error applying peripheral configuration: %{public}@", String(describing: error)) // Will retry } do { if let delegate = self.delegate { try delegate.completeConfiguration(for: self) self.log.default("Delegate configuration completed") self.needsConfiguration = false } else { self.log.error("No delegate set for configuration") } } catch let error { self.log.error("Error applying delegate configuration: %{public}@", String(describing: error)) // Will retry } } block(self) } } func perform(_ block: @escaping (_ manager: PeripheralManager) -> Void) { queue.async(execute: configureAndRun(block)) } private func assertConfiguration() { if peripheral.state == .connected { perform { (_) in // Intentionally empty to trigger configuration if necessary } } } private func applyConfiguration(discoveryTimeout: TimeInterval = 2) throws { try discoverServices(configuration.serviceCharacteristics.keys.map { $0 }, timeout: discoveryTimeout) for service in peripheral.services ?? [] { guard let characteristics = configuration.serviceCharacteristics[service.uuid] else { // Not all services have characteristics continue } try discoverCharacteristics(characteristics, for: service, timeout: discoveryTimeout) } // Subscribe to notifying characteristics for (serviceUUID, characteristicUUIDs) in configuration.notifyingCharacteristics { guard let service = peripheral.services?.itemWithUUID(serviceUUID) else { // Not all RL's have OrangeLink service continue } for characteristicUUID in characteristicUUIDs { guard let characteristic = service.characteristics?.itemWithUUID(characteristicUUID) else { throw PeripheralManagerError.unknownCharacteristic(characteristicUUID) } guard !characteristic.isNotifying else { continue } try setNotifyValue(true, for: characteristic, timeout: discoveryTimeout) } } } } // MARK: - Synchronous Commands extension PeripheralManager { /// - Throws: PeripheralManagerError func runCommand(timeout: TimeInterval, command: () -> Void) throws { // Prelude dispatchPrecondition(condition: .onQueue(queue)) guard central?.state == .poweredOn && peripheral.state == .connected else { throw PeripheralManagerError.notReady } commandLock.lock() defer { commandLock.unlock() } guard commandConditions.isEmpty else { throw PeripheralManagerError.busy } // Run command() guard !commandConditions.isEmpty else { // If the command didn't add any conditions, then finish immediately return } // Postlude let signaled = commandLock.wait(until: Date(timeIntervalSinceNow: timeout)) defer { commandError = nil commandConditions = [] } guard signaled else { throw PeripheralManagerError.timeout(commandConditions) } if let error = commandError { throw PeripheralManagerError.cbPeripheralError(error) } } /// It's illegal to call this without first acquiring the commandLock /// /// - Parameter condition: The condition to add func addCondition(_ condition: CommandCondition) { dispatchPrecondition(condition: .onQueue(queue)) commandConditions.append(condition) } func discoverServices(_ serviceUUIDs: [CBUUID], timeout: TimeInterval) throws { let servicesToDiscover = peripheral.servicesToDiscover(from: serviceUUIDs) guard servicesToDiscover.count > 0 else { return } try runCommand(timeout: timeout) { addCondition(.discoverServices) peripheral.discoverServices(serviceUUIDs) } } func discoverCharacteristics(_ characteristicUUIDs: [CBUUID], for service: CBService, timeout: TimeInterval) throws { let characteristicsToDiscover = peripheral.characteristicsToDiscover(from: characteristicUUIDs, for: service) guard characteristicsToDiscover.count > 0 else { return } try runCommand(timeout: timeout) { addCondition(.discoverCharacteristicsForService(serviceUUID: service.uuid)) peripheral.discoverCharacteristics(characteristicsToDiscover, for: service) } } /// - Throws: PeripheralManagerError func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic, timeout: TimeInterval) throws { try runCommand(timeout: timeout) { addCondition(.notificationStateUpdate(characteristic: characteristic, enabled: enabled)) peripheral.setNotifyValue(enabled, for: characteristic) } } /// - Throws: PeripheralManagerError func readValue(for characteristic: CBCharacteristic, timeout: TimeInterval) throws -> Data? { try runCommand(timeout: timeout) { addCondition(.valueUpdate(characteristic: characteristic, matching: nil)) peripheral.readValue(for: characteristic) } return characteristic.value } /// - Throws: PeripheralManagerError func writeValue(_ value: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType, timeout: TimeInterval) throws { try runCommand(timeout: timeout) { if case .withResponse = type { addCondition(.write(characteristic: characteristic)) } peripheral.writeValue(value, for: characteristic, type: type) } } } // MARK: - Delegate methods executed on the central's queue extension PeripheralManager: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { commandLock.lock() if let index = commandConditions.firstIndex(where: { (condition) -> Bool in if case .discoverServices = condition { return true } else { return false } }) { commandConditions.remove(at: index) commandError = error if commandConditions.isEmpty { commandLock.broadcast() } } commandLock.unlock() } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { commandLock.lock() if let index = commandConditions.firstIndex(where: { (condition) -> Bool in if case .discoverCharacteristicsForService(serviceUUID: service.uuid) = condition { return true } else { return false } }) { commandConditions.remove(at: index) commandError = error if commandConditions.isEmpty { commandLock.broadcast() } } commandLock.unlock() } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { commandLock.lock() if let index = commandConditions.firstIndex(where: { (condition) -> Bool in if case .notificationStateUpdate(characteristic: characteristic, enabled: characteristic.isNotifying) = condition { return true } else { return false } }) { commandConditions.remove(at: index) commandError = error if commandConditions.isEmpty { commandLock.broadcast() } } commandLock.unlock() delegate?.peripheralManager(self, didUpdateNotificationStateFor: characteristic) } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { commandLock.lock() if let index = commandConditions.firstIndex(where: { (condition) -> Bool in if case .write(characteristic: characteristic) = condition { return true } else { return false } }) { commandConditions.remove(at: index) commandError = error if commandConditions.isEmpty { commandLock.broadcast() } } commandLock.unlock() } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { commandLock.lock() var notifyDelegate = false if let index = commandConditions.firstIndex(where: { (condition) -> Bool in if case .valueUpdate(characteristic: characteristic, matching: let matching) = condition { return matching?(characteristic.value) ?? true } else { return false } }) { commandConditions.remove(at: index) commandError = error if commandConditions.isEmpty { commandLock.broadcast() } } else if let macro = configuration.valueUpdateMacros[characteristic.uuid] { macro(self) } else { notifyDelegate = true // execute after the unlock } commandLock.unlock() if notifyDelegate { // If we weren't expecting this notification, pass it along to the delegate delegate?.peripheralManager(self, didUpdateValueFor: characteristic) } } func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { delegate?.peripheralManager(self, didReadRSSI: RSSI, error: error) } func peripheralDidUpdateName(_ peripheral: CBPeripheral) { delegate?.peripheralManagerDidUpdateName(self) } } extension PeripheralManager: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .poweredOn: assertConfiguration() default: break } } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { switch peripheral.state { case .connected: assertConfiguration() default: break } } } extension PeripheralManager { public override var debugDescription: String { var items = [ "## PeripheralManager", "peripheral: \(peripheral)" ] queue.sync { items.append("needsConfiguration: \(needsConfiguration)") } return items.joined(separator: "\n") } }