RileyLinkDeviceManager.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  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. if device.peripheralState == .connected {
  70. device.setTimerTickEnabled(newValue)
  71. }
  72. }
  73. }
  74. }
  75. }
  76. private let lockedTimerTickEnabled = Locked(true)
  77. }
  78. // MARK: - Connecting
  79. extension RileyLinkDeviceManager {
  80. public func getAutoConnectIDs(_ completion: @escaping (_ autoConnectIDs: Set<String>) -> Void) {
  81. centralQueue.async {
  82. completion(self.autoConnectIDs)
  83. }
  84. }
  85. public func connect(_ device: RileyLinkDevice) {
  86. centralQueue.async {
  87. self.autoConnectIDs.insert(device.manager.peripheral.identifier.uuidString)
  88. guard let peripheral = self.reloadPeripheral(for: device) else {
  89. return
  90. }
  91. self.central.connectIfNecessary(peripheral)
  92. }
  93. }
  94. public func disconnect(_ device: RileyLinkDevice) {
  95. centralQueue.async {
  96. self.autoConnectIDs.remove(device.manager.peripheral.identifier.uuidString)
  97. guard let peripheral = self.reloadPeripheral(for: device) else {
  98. return
  99. }
  100. self.central.cancelPeripheralConnectionIfNecessary(peripheral)
  101. }
  102. }
  103. /// Asks the central manager for its peripheral instance for a given device.
  104. /// It seems to be possible that this reference changes across a bluetooth reset, and not updating the reference can result in API MISUSE warnings
  105. ///
  106. /// - Parameter device: The device to reload
  107. /// - Returns: The peripheral instance returned by the central manager
  108. private func reloadPeripheral(for device: RileyLinkDevice) -> CBPeripheral? {
  109. dispatchPrecondition(condition: .onQueue(centralQueue))
  110. guard let peripheral = central.retrievePeripherals(withIdentifiers: [device.manager.peripheral.identifier]).first else {
  111. return nil
  112. }
  113. device.manager.peripheral = peripheral
  114. return peripheral
  115. }
  116. private var hasDiscoveredAllAutoConnectDevices: Bool {
  117. dispatchPrecondition(condition: .onQueue(centralQueue))
  118. return autoConnectIDs.isSubset(of: devices.map { $0.manager.peripheral.identifier.uuidString })
  119. }
  120. private func autoConnectDevices() {
  121. dispatchPrecondition(condition: .onQueue(centralQueue))
  122. for device in devices where autoConnectIDs.contains(device.manager.peripheral.identifier.uuidString) {
  123. log.info("Attempting reconnect to %@", device.manager.peripheral)
  124. connect(device)
  125. }
  126. }
  127. private func addPeripheral(_ peripheral: CBPeripheral, rssi: Int? = nil) {
  128. dispatchPrecondition(condition: .onQueue(centralQueue))
  129. var device: RileyLinkDevice! = devices.first(where: { $0.manager.peripheral.identifier == peripheral.identifier })
  130. if let device = device {
  131. device.manager.peripheral = peripheral
  132. } else {
  133. device = RileyLinkDevice(peripheralManager: PeripheralManager(peripheral: peripheral, configuration: .rileyLink, centralManager: central, queue: sessionQueue), rssi: rssi)
  134. if peripheral.state == .connected {
  135. device.setTimerTickEnabled(timerTickEnabled)
  136. device.setIdleListeningState(idleListeningState)
  137. }
  138. devices.append(device)
  139. log.info("Created device for peripheral %@", peripheral)
  140. }
  141. if autoConnectIDs.contains(peripheral.identifier.uuidString) {
  142. central.connectIfNecessary(peripheral)
  143. }
  144. }
  145. }
  146. extension RileyLinkDeviceManager {
  147. public func getDevices(_ completion: @escaping (_ devices: [RileyLinkDevice]) -> Void) {
  148. centralQueue.async {
  149. completion(self.devices)
  150. }
  151. }
  152. public func deprioritize(_ device: RileyLinkDevice, completion: (() -> Void)? = nil) {
  153. centralQueue.async {
  154. self.devices.deprioritize(device)
  155. completion?()
  156. }
  157. }
  158. }
  159. extension Array where Element == RileyLinkDevice {
  160. public var firstConnected: Element? {
  161. return self.first { (device) -> Bool in
  162. return device.manager.peripheral.state == .connected
  163. }
  164. }
  165. mutating func deprioritize(_ element: Element) {
  166. if let index = self.firstIndex(where: { $0 === element }) {
  167. self.swapAt(index, self.count - 1)
  168. }
  169. }
  170. }
  171. // MARK: - Scanning
  172. extension RileyLinkDeviceManager {
  173. public func setScanningEnabled(_ enabled: Bool) {
  174. centralQueue.async {
  175. self.isScanningEnabled = enabled
  176. if case .poweredOn = self.central.state {
  177. if enabled {
  178. self.central.scanForPeripherals()
  179. } else if self.central.isScanning {
  180. self.central.stopScan()
  181. }
  182. }
  183. }
  184. }
  185. public func assertIdleListening(forcingRestart: Bool) {
  186. centralQueue.async {
  187. for device in self.devices {
  188. device.assertIdleListening(forceRestart: forcingRestart)
  189. }
  190. }
  191. }
  192. }
  193. // MARK: - Delegate methods called on `centralQueue`
  194. extension RileyLinkDeviceManager: CBCentralManagerDelegate {
  195. public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
  196. log.default("%@", #function)
  197. guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else {
  198. return
  199. }
  200. for peripheral in peripherals {
  201. addPeripheral(peripheral)
  202. }
  203. }
  204. public func centralManagerDidUpdateState(_ central: CBCentralManager) {
  205. log.default("%@: %@", #function, central.state.description)
  206. if case .poweredOn = central.state {
  207. autoConnectDevices()
  208. if isScanningEnabled || !hasDiscoveredAllAutoConnectDevices {
  209. central.scanForPeripherals()
  210. } else if central.isScanning {
  211. central.stopScan()
  212. }
  213. }
  214. for device in devices {
  215. device.centralManagerDidUpdateState(central)
  216. }
  217. }
  218. public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
  219. log.default("Discovered %@ at %@", peripheral, RSSI)
  220. addPeripheral(peripheral, rssi: Int(truncating: RSSI))
  221. // 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.
  222. if !isScanningEnabled && central.isScanning && hasDiscoveredAllAutoConnectDevices {
  223. log.default("All peripherals discovered")
  224. central.stopScan()
  225. }
  226. }
  227. public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
  228. // Notify the device so it can begin configuration
  229. for device in devices where device.manager.peripheral.identifier == peripheral.identifier {
  230. device.centralManager(central, didConnect: peripheral)
  231. }
  232. }
  233. public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
  234. for device in devices where device.manager.peripheral.identifier == peripheral.identifier {
  235. device.centralManager(central, didDisconnectPeripheral: peripheral, error: error)
  236. }
  237. autoConnectDevices()
  238. }
  239. public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
  240. log.error("%@: %@: %@", #function, peripheral, String(describing: error))
  241. for device in devices where device.manager.peripheral.identifier == peripheral.identifier {
  242. device.centralManager(central, didFailToConnect: peripheral, error: error)
  243. }
  244. autoConnectDevices()
  245. }
  246. }
  247. extension RileyLinkDeviceManager {
  248. public override var debugDescription: String {
  249. var report = [
  250. "## RileyLinkDeviceManager",
  251. "central: \(central!)",
  252. "autoConnectIDs: \(autoConnectIDs)",
  253. "timerTickEnabled: \(timerTickEnabled)",
  254. "idleListeningState: \(idleListeningState)"
  255. ]
  256. for device in devices {
  257. report.append(String(reflecting: device))
  258. report.append("")
  259. }
  260. return report.joined(separator: "\n\n")
  261. }
  262. }
  263. extension Notification.Name {
  264. public static let ManagerDevicesDidChange = Notification.Name("com.rileylink.RileyLinkBLEKit.DevicesDidChange")
  265. }
  266. extension RileyLinkDeviceManager {
  267. public static let autoConnectIDsStateKey = "com.rileylink.RileyLinkBLEKit.AutoConnectIDs"
  268. }