PeripheralManager.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. //
  2. // PeripheralManager.swift
  3. // xDripG5
  4. //
  5. // Copyright © 2017 LoopKit Authors. All rights reserved.
  6. //
  7. import CoreBluetooth
  8. import Foundation
  9. import os.log
  10. class PeripheralManager: NSObject {
  11. private let log = OSLog(category: "PeripheralManager")
  12. ///
  13. /// This is mutable, because CBPeripheral instances can seemingly become invalid, and need to be periodically re-fetched from CBCentralManager
  14. var peripheral: CBPeripheral {
  15. didSet {
  16. guard oldValue !== peripheral else {
  17. return
  18. }
  19. log.error("Replacing peripheral reference %{public}@ -> %{public}@", oldValue, peripheral)
  20. oldValue.delegate = nil
  21. peripheral.delegate = self
  22. queue.sync {
  23. self.needsConfiguration = true
  24. }
  25. }
  26. }
  27. /// The dispatch queue used to serialize operations on the peripheral
  28. let queue = DispatchQueue(label: "com.loopkit.PeripheralManager.queue", qos: .unspecified)
  29. /// The condition used to signal command completion
  30. private let commandLock = NSCondition()
  31. /// The required conditions for the operation to complete
  32. private var commandConditions = [CommandCondition]()
  33. /// Any error surfaced during the active operation
  34. private var commandError: Error?
  35. private(set) weak var central: CBCentralManager?
  36. let configuration: Configuration
  37. // Confined to `queue`
  38. private var needsConfiguration = true
  39. weak var delegate: PeripheralManagerDelegate? {
  40. didSet {
  41. queue.sync {
  42. needsConfiguration = true
  43. }
  44. }
  45. }
  46. init(peripheral: CBPeripheral, configuration: Configuration, centralManager: CBCentralManager) {
  47. self.peripheral = peripheral
  48. self.central = centralManager
  49. self.configuration = configuration
  50. super.init()
  51. peripheral.delegate = self
  52. assertConfiguration()
  53. }
  54. }
  55. // MARK: - Nested types
  56. extension PeripheralManager {
  57. struct Configuration {
  58. var serviceCharacteristics: [CBUUID: [CBUUID]] = [:]
  59. var notifyingCharacteristics: [CBUUID: [CBUUID]] = [:]
  60. var valueUpdateMacros: [CBUUID: (_ manager: PeripheralManager) -> Void] = [:]
  61. }
  62. enum CommandCondition {
  63. case notificationStateUpdate(characteristicUUID: CBUUID, enabled: Bool)
  64. case valueUpdate(characteristic: CBCharacteristic, matching: ((Data?) -> Bool)?)
  65. case write(characteristic: CBCharacteristic)
  66. case discoverServices
  67. case discoverCharacteristicsForService(serviceUUID: CBUUID)
  68. }
  69. }
  70. protocol PeripheralManagerDelegate: AnyObject {
  71. func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic)
  72. func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?)
  73. func peripheralManagerDidUpdateName(_ manager: PeripheralManager)
  74. func completeConfiguration(for manager: PeripheralManager) throws
  75. }
  76. // MARK: - Operation sequence management
  77. extension PeripheralManager {
  78. func configureAndRun(_ block: @escaping (_ manager: PeripheralManager) -> Void) -> (() -> Void) {
  79. return {
  80. if !self.needsConfiguration && self.peripheral.services == nil {
  81. self.log.error("Configured peripheral has no services. Reconfiguring…")
  82. }
  83. if self.needsConfiguration || self.peripheral.services == nil {
  84. do {
  85. try self.applyConfiguration()
  86. self.log.default("Peripheral configuration completed")
  87. } catch let error {
  88. self.log.error("Error applying peripheral configuration: %@", String(describing: error))
  89. // Will retry
  90. }
  91. do {
  92. if let delegate = self.delegate {
  93. try delegate.completeConfiguration(for: self)
  94. self.log.default("Delegate configuration completed")
  95. self.needsConfiguration = false
  96. } else {
  97. self.log.error("No delegate set configured")
  98. }
  99. } catch let error {
  100. self.log.error("Error applying delegate configuration: %@", String(describing: error))
  101. // Will retry
  102. }
  103. }
  104. block(self)
  105. }
  106. }
  107. func perform(_ block: @escaping (_ manager: PeripheralManager) -> Void) {
  108. queue.async(execute: configureAndRun(block))
  109. }
  110. private func assertConfiguration() {
  111. perform { (_) in
  112. // Intentionally empty to trigger configuration if necessary
  113. }
  114. }
  115. private func applyConfiguration(discoveryTimeout: TimeInterval = 2) throws {
  116. try discoverServices(configuration.serviceCharacteristics.keys.map { $0 }, timeout: discoveryTimeout)
  117. for service in peripheral.services ?? [] {
  118. guard let characteristics = configuration.serviceCharacteristics[service.uuid] else {
  119. // Not all services may have characteristics
  120. continue
  121. }
  122. try discoverCharacteristics(characteristics, for: service, timeout: discoveryTimeout)
  123. }
  124. for (serviceUUID, characteristicUUIDs) in configuration.notifyingCharacteristics {
  125. guard let service = peripheral.services?.itemWithUUID(serviceUUID) else {
  126. throw PeripheralManagerError.unknownCharacteristic
  127. }
  128. for characteristicUUID in characteristicUUIDs {
  129. guard let characteristic = service.characteristics?.itemWithUUID(characteristicUUID) else {
  130. throw PeripheralManagerError.unknownCharacteristic
  131. }
  132. guard !characteristic.isNotifying else {
  133. continue
  134. }
  135. try setNotifyValue(true, for: characteristic, timeout: discoveryTimeout)
  136. }
  137. }
  138. }
  139. }
  140. // MARK: - Synchronous Commands
  141. extension PeripheralManager {
  142. /// - Throws: PeripheralManagerError
  143. func runCommand(timeout: TimeInterval, command: () -> Void) throws {
  144. // Prelude
  145. dispatchPrecondition(condition: .onQueue(queue))
  146. guard central?.state == .poweredOn && peripheral.state == .connected else {
  147. throw PeripheralManagerError.notReady
  148. }
  149. commandLock.lock()
  150. defer {
  151. commandLock.unlock()
  152. }
  153. guard commandConditions.isEmpty else {
  154. throw PeripheralManagerError.invalidConfiguration
  155. }
  156. // Run
  157. command()
  158. guard !commandConditions.isEmpty else {
  159. // If the command didn't add any conditions, then finish immediately
  160. return
  161. }
  162. // Postlude
  163. let signaled = commandLock.wait(until: Date(timeIntervalSinceNow: timeout))
  164. defer {
  165. commandError = nil
  166. commandConditions = []
  167. }
  168. guard signaled else {
  169. throw PeripheralManagerError.timeout
  170. }
  171. if let error = commandError {
  172. throw PeripheralManagerError.cbPeripheralError(error)
  173. }
  174. }
  175. /// It's illegal to call this without first acquiring the commandLock
  176. ///
  177. /// - Parameter condition: The condition to add
  178. func addCondition(_ condition: CommandCondition) {
  179. dispatchPrecondition(condition: .onQueue(queue))
  180. commandConditions.append(condition)
  181. }
  182. func discoverServices(_ serviceUUIDs: [CBUUID], timeout: TimeInterval) throws {
  183. let servicesToDiscover = peripheral.servicesToDiscover(from: serviceUUIDs)
  184. guard servicesToDiscover.count > 0 else {
  185. return
  186. }
  187. try runCommand(timeout: timeout) {
  188. addCondition(.discoverServices)
  189. peripheral.discoverServices(serviceUUIDs)
  190. }
  191. }
  192. func discoverCharacteristics(_ characteristicUUIDs: [CBUUID], for service: CBService, timeout: TimeInterval) throws {
  193. let characteristicsToDiscover = peripheral.characteristicsToDiscover(from: characteristicUUIDs, for: service)
  194. guard characteristicsToDiscover.count > 0 else {
  195. return
  196. }
  197. try runCommand(timeout: timeout) {
  198. addCondition(.discoverCharacteristicsForService(serviceUUID: service.uuid))
  199. peripheral.discoverCharacteristics(characteristicsToDiscover, for: service)
  200. }
  201. }
  202. /// - Throws: PeripheralManagerError
  203. func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic, timeout: TimeInterval) throws {
  204. try runCommand(timeout: timeout) {
  205. addCondition(.notificationStateUpdate(characteristicUUID: characteristic.uuid, enabled: enabled))
  206. peripheral.setNotifyValue(enabled, for: characteristic)
  207. }
  208. }
  209. /// - Throws: PeripheralManagerError
  210. func readValue(for characteristic: CBCharacteristic, timeout: TimeInterval) throws -> Data? {
  211. try runCommand(timeout: timeout) {
  212. addCondition(.valueUpdate(characteristic: characteristic, matching: nil))
  213. peripheral.readValue(for: characteristic)
  214. }
  215. return characteristic.value
  216. }
  217. /// - Throws: PeripheralManagerError
  218. func wait(for characteristic: CBCharacteristic, timeout: TimeInterval) throws -> Data {
  219. try runCommand(timeout: timeout) {
  220. addCondition(.valueUpdate(characteristic: characteristic, matching: nil))
  221. }
  222. guard let value = characteristic.value else {
  223. throw PeripheralManagerError.timeout
  224. }
  225. return value
  226. }
  227. /// - Throws: PeripheralManagerError
  228. func writeValue(_ value: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType, timeout: TimeInterval) throws {
  229. try runCommand(timeout: timeout) {
  230. if case .withResponse = type {
  231. addCondition(.write(characteristic: characteristic))
  232. }
  233. peripheral.writeValue(value, for: characteristic, type: type)
  234. }
  235. }
  236. }
  237. // MARK: - Delegate methods executed on the central's queue
  238. extension PeripheralManager: CBPeripheralDelegate {
  239. func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
  240. commandLock.lock()
  241. if let index = commandConditions.firstIndex(where: { (condition) -> Bool in
  242. if case .discoverServices = condition {
  243. return true
  244. } else {
  245. return false
  246. }
  247. }) {
  248. commandConditions.remove(at: index)
  249. commandError = error
  250. if commandConditions.isEmpty {
  251. commandLock.broadcast()
  252. }
  253. }
  254. commandLock.unlock()
  255. }
  256. func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
  257. commandLock.lock()
  258. if let index = commandConditions.firstIndex(where: { (condition) -> Bool in
  259. if case .discoverCharacteristicsForService(serviceUUID: service.uuid) = condition {
  260. return true
  261. } else {
  262. return false
  263. }
  264. }) {
  265. commandConditions.remove(at: index)
  266. commandError = error
  267. if commandConditions.isEmpty {
  268. commandLock.broadcast()
  269. }
  270. }
  271. commandLock.unlock()
  272. }
  273. func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
  274. commandLock.lock()
  275. if let index = commandConditions.firstIndex(where: { (condition) -> Bool in
  276. if case .notificationStateUpdate(characteristicUUID: characteristic.uuid, enabled: characteristic.isNotifying) = condition {
  277. return true
  278. } else {
  279. return false
  280. }
  281. }) {
  282. commandConditions.remove(at: index)
  283. commandError = error
  284. if commandConditions.isEmpty {
  285. commandLock.broadcast()
  286. }
  287. }
  288. commandLock.unlock()
  289. }
  290. func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
  291. commandLock.lock()
  292. if let index = commandConditions.firstIndex(where: { (condition) -> Bool in
  293. if case .write(characteristic: characteristic) = condition {
  294. return true
  295. } else {
  296. return false
  297. }
  298. }) {
  299. commandConditions.remove(at: index)
  300. commandError = error
  301. if commandConditions.isEmpty {
  302. commandLock.broadcast()
  303. }
  304. }
  305. commandLock.unlock()
  306. }
  307. func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
  308. commandLock.lock()
  309. var notifyDelegate = false
  310. if let index = commandConditions.firstIndex(where: { (condition) -> Bool in
  311. if case .valueUpdate(characteristic: characteristic, matching: let matching) = condition {
  312. return matching?(characteristic.value) ?? true
  313. } else {
  314. return false
  315. }
  316. }) {
  317. commandConditions.remove(at: index)
  318. commandError = error
  319. if commandConditions.isEmpty {
  320. commandLock.broadcast()
  321. }
  322. } else if let macro = configuration.valueUpdateMacros[characteristic.uuid] {
  323. macro(self)
  324. } else if commandConditions.isEmpty {
  325. notifyDelegate = true // execute after the unlock
  326. }
  327. commandLock.unlock()
  328. if notifyDelegate {
  329. // If we weren't expecting this notification, pass it along to the delegate
  330. delegate?.peripheralManager(self, didUpdateValueFor: characteristic)
  331. }
  332. }
  333. func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
  334. delegate?.peripheralManager(self, didReadRSSI: RSSI, error: error)
  335. }
  336. func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
  337. delegate?.peripheralManagerDidUpdateName(self)
  338. }
  339. }
  340. extension PeripheralManager: CBCentralManagerDelegate {
  341. func centralManagerDidUpdateState(_ central: CBCentralManager) {
  342. switch central.state {
  343. case .poweredOn:
  344. assertConfiguration()
  345. default:
  346. break
  347. }
  348. }
  349. func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
  350. switch peripheral.state {
  351. case .connected:
  352. assertConfiguration()
  353. default:
  354. break
  355. }
  356. }
  357. }
  358. extension PeripheralManager {
  359. public override var debugDescription: String {
  360. var items = [
  361. "## PeripheralManager",
  362. "peripheral: \(peripheral)",
  363. ]
  364. queue.sync {
  365. items.append("needsConfiguration: \(needsConfiguration)")
  366. }
  367. return items.joined(separator: "\n")
  368. }
  369. }