RileyLinkBluetoothDeviceProvider.swift 11 KB

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