RileyLinkDeviceManager.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. //
  2. // RileyLinkDeviceManager.swift
  3. // RileyLinkBLEKit
  4. //
  5. // Copyright © 2017 Pete Schwamb. All rights reserved.
  6. //
  7. import CoreBluetooth
  8. import os.log
  9. import LoopKit
  10. public class RileyLinkDeviceManager: NSObject {
  11. private let log = OSLog(category: "RileyLinkDeviceManager")
  12. // Isolated to centralQueue
  13. private var central: CBCentralManager!
  14. private let centralQueue = DispatchQueue(label: "com.rileylink.RileyLinkBLEKit.BluetoothManager.centralQueue", qos: .unspecified)
  15. internal let sessionQueue = DispatchQueue(label: "com.rileylink.RileyLinkBLEKit.RileyLinkDeviceManager.sessionQueue", qos: .unspecified)
  16. // Isolated to centralQueue
  17. private var devices: [RileyLinkDevice] = [] {
  18. didSet {
  19. NotificationCenter.default.post(name: .ManagerDevicesDidChange, object: self)
  20. }
  21. }
  22. // Isolated to centralQueue
  23. private var autoConnectIDs: Set<String>
  24. // Isolated to centralQueue
  25. private var isScanningEnabled = false
  26. public init(autoConnectIDs: Set<String>) {
  27. self.autoConnectIDs = autoConnectIDs
  28. super.init()
  29. centralQueue.sync {
  30. central = CBCentralManager(
  31. delegate: self,
  32. queue: centralQueue,
  33. options: [
  34. CBCentralManagerOptionRestoreIdentifierKey: "com.rileylink.CentralManager"
  35. ]
  36. )
  37. }
  38. }
  39. // MARK: - Configuration
  40. public var idleListeningEnabled: Bool {
  41. if case .disabled = idleListeningState {
  42. return false
  43. } else {
  44. return true
  45. }
  46. }
  47. public var idleListeningState: RileyLinkDevice.IdleListeningState {
  48. get {
  49. return lockedIdleListeningState.value
  50. }
  51. set {
  52. lockedIdleListeningState.value = newValue
  53. centralQueue.async {
  54. for device in self.devices {
  55. device.setIdleListeningState(newValue)
  56. }
  57. }
  58. }
  59. }
  60. private let lockedIdleListeningState = Locked(RileyLinkDevice.IdleListeningState.disabled)
  61. public var timerTickEnabled: Bool {
  62. get {
  63. return lockedTimerTickEnabled.value
  64. }
  65. set {
  66. lockedTimerTickEnabled.value = newValue
  67. centralQueue.async {
  68. for device in self.devices {
  69. device.setTimerTickEnabled(newValue)
  70. }
  71. }
  72. }
  73. }
  74. private let lockedTimerTickEnabled = Locked(true)
  75. }
  76. // MARK: - Connecting
  77. extension RileyLinkDeviceManager {
  78. public func getAutoConnectIDs(_ completion: @escaping (_ autoConnectIDs: Set<String>) -> Void) {
  79. centralQueue.async {
  80. completion(self.autoConnectIDs)
  81. }
  82. }
  83. public func connect(_ device: RileyLinkDevice) {
  84. centralQueue.async {
  85. self.autoConnectIDs.insert(device.manager.peripheral.identifier.uuidString)
  86. guard let peripheral = self.reloadPeripheral(for: device) else {
  87. return
  88. }
  89. self.central.connectIfNecessary(peripheral)
  90. }
  91. }
  92. public func disconnect(_ device: RileyLinkDevice) {
  93. centralQueue.async {
  94. self.autoConnectIDs.remove(device.manager.peripheral.identifier.uuidString)
  95. guard let peripheral = self.reloadPeripheral(for: device) else {
  96. return
  97. }
  98. self.central.cancelPeripheralConnectionIfNecessary(peripheral)
  99. }
  100. }
  101. /// Asks the central manager for its peripheral instance for a given device.
  102. /// It seems to be possible that this reference changes across a bluetooth reset, and not updating the reference can result in API MISUSE warnings
  103. ///
  104. /// - Parameter device: The device to reload
  105. /// - Returns: The peripheral instance returned by the central manager
  106. private func reloadPeripheral(for device: RileyLinkDevice) -> CBPeripheral? {
  107. dispatchPrecondition(condition: .onQueue(centralQueue))
  108. guard let peripheral = central.retrievePeripherals(withIdentifiers: [device.manager.peripheral.identifier]).first else {
  109. return nil
  110. }
  111. device.manager.peripheral = peripheral
  112. return peripheral
  113. }
  114. private var hasDiscoveredAllAutoConnectDevices: Bool {
  115. dispatchPrecondition(condition: .onQueue(centralQueue))
  116. return autoConnectIDs.isSubset(of: devices.map { $0.manager.peripheral.identifier.uuidString })
  117. }
  118. private func autoConnectDevices() {
  119. dispatchPrecondition(condition: .onQueue(centralQueue))
  120. for device in devices where autoConnectIDs.contains(device.manager.peripheral.identifier.uuidString) {
  121. log.info("Attempting reconnect to %@", device.manager.peripheral)
  122. connect(device)
  123. }
  124. }
  125. private func addPeripheral(_ peripheral: CBPeripheral) {
  126. dispatchPrecondition(condition: .onQueue(centralQueue))
  127. var device: RileyLinkDevice! = devices.first(where: { $0.manager.peripheral.identifier == peripheral.identifier })
  128. if let device = device {
  129. device.manager.peripheral = peripheral
  130. } else {
  131. device = RileyLinkDevice(peripheralManager: PeripheralManager(peripheral: peripheral, configuration: .rileyLink, centralManager: central, queue: sessionQueue))
  132. device.setTimerTickEnabled(timerTickEnabled)
  133. device.setIdleListeningState(idleListeningState)
  134. devices.append(device)
  135. log.info("Created device for peripheral %@", peripheral)
  136. }
  137. if autoConnectIDs.contains(peripheral.identifier.uuidString) {
  138. central.connectIfNecessary(peripheral)
  139. }
  140. }
  141. }
  142. extension RileyLinkDeviceManager {
  143. public func getDevices(_ completion: @escaping (_ devices: [RileyLinkDevice]) -> Void) {
  144. centralQueue.async {
  145. completion(self.devices)
  146. }
  147. }
  148. public func deprioritize(_ device: RileyLinkDevice, completion: (() -> Void)? = nil) {
  149. centralQueue.async {
  150. self.devices.deprioritize(device)
  151. completion?()
  152. }
  153. }
  154. }
  155. extension Array where Element == RileyLinkDevice {
  156. public var firstConnected: Element? {
  157. return self.first { (device) -> Bool in
  158. return device.manager.peripheral.state == .connected
  159. }
  160. }
  161. mutating func deprioritize(_ element: Element) {
  162. if let index = self.firstIndex(where: { $0 === element }) {
  163. self.swapAt(index, self.count - 1)
  164. }
  165. }
  166. }
  167. // MARK: - Scanning
  168. extension RileyLinkDeviceManager {
  169. public func setScanningEnabled(_ enabled: Bool) {
  170. centralQueue.async {
  171. self.isScanningEnabled = enabled
  172. if case .poweredOn = self.central.state {
  173. if enabled {
  174. self.central.scanForPeripherals()
  175. } else if self.central.isScanning {
  176. self.central.stopScan()
  177. }
  178. }
  179. }
  180. }
  181. public func assertIdleListening(forcingRestart: Bool) {
  182. centralQueue.async {
  183. for device in self.devices {
  184. device.assertIdleListening(forceRestart: forcingRestart)
  185. }
  186. }
  187. }
  188. }
  189. // MARK: - Delegate methods called on `centralQueue`
  190. extension RileyLinkDeviceManager: CBCentralManagerDelegate {
  191. public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
  192. log.default("%@", #function)
  193. guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else {
  194. return
  195. }
  196. for peripheral in peripherals {
  197. addPeripheral(peripheral)
  198. }
  199. }
  200. public func centralManagerDidUpdateState(_ central: CBCentralManager) {
  201. log.default("%@: %@", #function, central.state.description)
  202. if case .poweredOn = central.state {
  203. autoConnectDevices()
  204. if isScanningEnabled || !hasDiscoveredAllAutoConnectDevices {
  205. central.scanForPeripherals()
  206. } else if central.isScanning {
  207. central.stopScan()
  208. }
  209. }
  210. for device in devices {
  211. device.centralManagerDidUpdateState(central)
  212. }
  213. }
  214. public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
  215. log.default("Discovered %@ at %@", peripheral, RSSI)
  216. addPeripheral(peripheral)
  217. // TODO: Should we keep scanning? There's no UI to remove a lost RileyLink, which could result in a battery drain due to indefinite scanning.
  218. if !isScanningEnabled && central.isScanning && hasDiscoveredAllAutoConnectDevices {
  219. log.default("All peripherals discovered")
  220. central.stopScan()
  221. }
  222. }
  223. public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
  224. // Notify the device so it can begin configuration
  225. for device in devices where device.manager.peripheral.identifier == peripheral.identifier {
  226. device.centralManager(central, didConnect: peripheral)
  227. }
  228. }
  229. public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
  230. for device in devices where device.manager.peripheral.identifier == peripheral.identifier {
  231. device.centralManager(central, didDisconnectPeripheral: peripheral, error: error)
  232. }
  233. autoConnectDevices()
  234. }
  235. public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
  236. log.error("%@: %@: %@", #function, peripheral, String(describing: error))
  237. for device in devices where device.manager.peripheral.identifier == peripheral.identifier {
  238. device.centralManager(central, didFailToConnect: peripheral, error: error)
  239. }
  240. autoConnectDevices()
  241. }
  242. }
  243. extension RileyLinkDeviceManager {
  244. public override var debugDescription: String {
  245. var report = [
  246. "## RileyLinkDeviceManager",
  247. "central: \(central!)",
  248. "autoConnectIDs: \(autoConnectIDs)",
  249. "timerTickEnabled: \(timerTickEnabled)",
  250. "idleListeningState: \(idleListeningState)"
  251. ]
  252. for device in devices {
  253. report.append(String(reflecting: device))
  254. report.append("")
  255. }
  256. return report.joined(separator: "\n\n")
  257. }
  258. }
  259. extension Notification.Name {
  260. public static let ManagerDevicesDidChange = Notification.Name("com.rileylink.RileyLinkBLEKit.DevicesDidChange")
  261. }
  262. extension RileyLinkDeviceManager {
  263. public static let autoConnectIDsStateKey = "com.rileylink.RileyLinkBLEKit.AutoConnectIDs"
  264. }