G7BluetoothManager.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. //
  2. // G7BluetoothManager.swift
  3. // CGMBLEKit
  4. //
  5. // Created by Pete Schwamb on 11/11/22.
  6. // Copyright © 2022 LoopKit Authors. All rights reserved.
  7. //
  8. import CoreBluetooth
  9. import Foundation
  10. import os.log
  11. enum PeripheralConnectionCommand {
  12. case connect
  13. case makeActive
  14. case ignore
  15. }
  16. protocol G7BluetoothManagerDelegate: AnyObject {
  17. /**
  18. Tells the delegate that the bluetooth manager has finished connecting to and discovering all required services of its peripheral
  19. - parameter manager: The bluetooth manager
  20. - parameter peripheralManager: The peripheral manager
  21. - parameter error: An error describing why bluetooth setup failed
  22. - returns: True if scanning should stop
  23. */
  24. func bluetoothManager(_ manager: G7BluetoothManager, readied peripheralManager: G7PeripheralManager) -> Bool
  25. /**
  26. Tells the delegate that the bluetooth manager encountered an error while connecting to and discovering required services of a peripheral
  27. - parameter manager: The bluetooth manager
  28. - parameter peripheralManager: The peripheral manager
  29. - parameter error: An error describing why bluetooth setup failed
  30. */
  31. func bluetoothManager(_ manager: G7BluetoothManager, readyingFailed peripheralManager: G7PeripheralManager, with error: Error)
  32. /**
  33. Asks the delegate if the discovered or restored peripheral is active or should be connected to
  34. - parameter manager: The bluetooth manager
  35. - parameter peripheral: The found peripheral
  36. - returns: PeripheralConnectionCommand indicating what should be done with this peripheral
  37. */
  38. func bluetoothManager(_ manager: G7BluetoothManager, shouldConnectPeripheral peripheral: CBPeripheral) -> PeripheralConnectionCommand
  39. /// Informs the delegate that the bluetooth manager received new data in the control characteristic
  40. ///
  41. /// - Parameters:
  42. /// - manager: The bluetooth manager
  43. /// - peripheralManager: The peripheral manager
  44. /// - response: The data received on the control characteristic
  45. func bluetoothManager(_ manager: G7BluetoothManager, peripheralManager: G7PeripheralManager, didReceiveControlResponse response: Data)
  46. /// Informs the delegate that the bluetooth manager received new data in the backfill characteristic
  47. ///
  48. /// - Parameters:
  49. /// - manager: The bluetooth manager
  50. /// - response: The data received on the backfill characteristic
  51. func bluetoothManager(_ manager: G7BluetoothManager, didReceiveBackfillResponse response: Data)
  52. /// Informs the delegate that the bluetooth manager received new data in the authentication characteristic
  53. ///
  54. /// - Parameters:
  55. /// - manager: The bluetooth manager
  56. /// - peripheralManager: The peripheral manager
  57. /// - response: The data received on the authentication characteristic
  58. func bluetoothManager(_ manager: G7BluetoothManager, peripheralManager: G7PeripheralManager, didReceiveAuthenticationResponse response: Data)
  59. /// Informs the delegate that the bluetooth manager started or stopped scanning
  60. ///
  61. /// - Parameters:
  62. /// - manager: The bluetooth manager
  63. func bluetoothManagerScanningStatusDidChange(_ manager: G7BluetoothManager)
  64. /// Informs the delegate that a peripheral disconnected
  65. ///
  66. /// - Parameters:
  67. /// - manager: The bluetooth manager
  68. func peripheralDidDisconnect(_ manager: G7BluetoothManager, peripheralManager: G7PeripheralManager, wasRemoteDisconnect: Bool)
  69. }
  70. class G7BluetoothManager: NSObject {
  71. weak var delegate: G7BluetoothManagerDelegate?
  72. private let log = OSLog(category: "G7BluetoothManager")
  73. /// Isolated to `managerQueue`
  74. private var centralManager: CBCentralManager! = nil
  75. /// Isolated to `managerQueue`
  76. private var activePeripheral: CBPeripheral? {
  77. get {
  78. return activePeripheralManager?.peripheral
  79. }
  80. }
  81. /// Isolated to `managerQueue`
  82. private var managedPeripherals: [UUID:G7PeripheralManager] = [:]
  83. var activePeripheralIdentifier: UUID? {
  84. get {
  85. return lockedPeripheralIdentifier.value
  86. }
  87. }
  88. private let lockedPeripheralIdentifier: Locked<UUID?> = Locked(nil)
  89. /// Isolated to `managerQueue`
  90. private var activePeripheralManager: G7PeripheralManager? {
  91. didSet {
  92. oldValue?.delegate = nil
  93. lockedPeripheralIdentifier.value = activePeripheralManager?.peripheral.identifier
  94. }
  95. }
  96. // MARK: - Synchronization
  97. private let managerQueue = DispatchQueue(label: "com.loudnate.CGMBLEKit.bluetoothManagerQueue", qos: .unspecified)
  98. override init() {
  99. super.init()
  100. managerQueue.sync {
  101. self.centralManager = CBCentralManager(delegate: self, queue: managerQueue, options: [CBCentralManagerOptionRestoreIdentifierKey: "com.loudnate.CGMBLEKit"])
  102. }
  103. }
  104. // MARK: - Actions
  105. func scanForPeripheral() {
  106. dispatchPrecondition(condition: .notOnQueue(managerQueue))
  107. managerQueue.sync {
  108. self.managerQueue_scanForPeripheral()
  109. }
  110. }
  111. func forgetPeripheral() {
  112. managerQueue.sync {
  113. self.activePeripheralManager = nil
  114. }
  115. }
  116. func stopScanning() {
  117. managerQueue.sync {
  118. managerQueue_stopScanning()
  119. }
  120. }
  121. private func managerQueue_stopScanning() {
  122. if centralManager.isScanning {
  123. log.debug("Stopping scan")
  124. centralManager.stopScan()
  125. delegate?.bluetoothManagerScanningStatusDidChange(self)
  126. }
  127. }
  128. func disconnect() {
  129. dispatchPrecondition(condition: .notOnQueue(managerQueue))
  130. managerQueue.sync {
  131. if centralManager.isScanning {
  132. log.debug("Stopping scan on disconnect")
  133. centralManager.stopScan()
  134. delegate?.bluetoothManagerScanningStatusDidChange(self)
  135. }
  136. if let peripheral = activePeripheral {
  137. centralManager.cancelPeripheralConnection(peripheral)
  138. }
  139. }
  140. }
  141. private func managerQueue_scanForPeripheral() {
  142. dispatchPrecondition(condition: .onQueue(managerQueue))
  143. guard centralManager.state == .poweredOn else {
  144. return
  145. }
  146. let currentState = activePeripheral?.state ?? .disconnected
  147. guard currentState != .connected else {
  148. return
  149. }
  150. if let peripheralID = activePeripheralIdentifier, let peripheral = centralManager.retrievePeripherals(withIdentifiers: [peripheralID]).first {
  151. log.debug("Retrieved peripheral %{public}@", peripheral.identifier.uuidString)
  152. handleDiscoveredPeripheral(peripheral)
  153. } else {
  154. for peripheral in centralManager.retrieveConnectedPeripherals(withServices: [
  155. SensorServiceUUID.advertisement.cbUUID,
  156. SensorServiceUUID.cgmService.cbUUID
  157. ]) {
  158. handleDiscoveredPeripheral(peripheral)
  159. }
  160. }
  161. if activePeripheral == nil {
  162. log.debug("Scanning for peripherals")
  163. centralManager.scanForPeripherals(withServices: [
  164. SensorServiceUUID.advertisement.cbUUID
  165. ],
  166. options: nil
  167. )
  168. delegate?.bluetoothManagerScanningStatusDidChange(self)
  169. }
  170. }
  171. /**
  172. Persistent connections don't seem to work with the transmitter shutoff: The OS won't re-wake the
  173. app unless it's scanning.
  174. The sleep gives the transmitter time to shut down, but keeps the app running.
  175. */
  176. fileprivate func scanAfterDelay() {
  177. DispatchQueue.global(qos: .utility).async {
  178. Thread.sleep(forTimeInterval: 2)
  179. self.scanForPeripheral()
  180. }
  181. }
  182. // MARK: - Accessors
  183. var isScanning: Bool {
  184. dispatchPrecondition(condition: .notOnQueue(managerQueue))
  185. var isScanning = false
  186. managerQueue.sync {
  187. isScanning = centralManager.isScanning
  188. }
  189. return isScanning
  190. }
  191. var isConnected: Bool {
  192. dispatchPrecondition(condition: .notOnQueue(managerQueue))
  193. var isConnected = false
  194. managerQueue.sync {
  195. isConnected = activePeripheral?.state == .connected
  196. }
  197. return isConnected
  198. }
  199. private func handleDiscoveredPeripheral(_ peripheral: CBPeripheral) {
  200. dispatchPrecondition(condition: .onQueue(managerQueue))
  201. if let delegate = delegate {
  202. switch delegate.bluetoothManager(self, shouldConnectPeripheral: peripheral) {
  203. case .makeActive:
  204. log.debug("Making peripheral active: %{public}@", peripheral.identifier.uuidString)
  205. if let peripheralManager = activePeripheralManager {
  206. peripheralManager.peripheral = peripheral
  207. } else {
  208. activePeripheralManager = G7PeripheralManager(
  209. peripheral: peripheral,
  210. configuration: .dexcomG7,
  211. centralManager: centralManager
  212. )
  213. activePeripheralManager?.delegate = self
  214. }
  215. self.managedPeripherals[peripheral.identifier] = activePeripheralManager
  216. self.centralManager.connect(peripheral)
  217. case .connect:
  218. log.debug("Connecting to peripheral: %{public}@", peripheral.identifier.uuidString)
  219. self.centralManager.connect(peripheral)
  220. let peripheralManager = G7PeripheralManager(
  221. peripheral: peripheral,
  222. configuration: .dexcomG7,
  223. centralManager: centralManager
  224. )
  225. peripheralManager.delegate = self
  226. self.managedPeripherals[peripheral.identifier] = peripheralManager
  227. case .ignore:
  228. break
  229. }
  230. }
  231. }
  232. override var debugDescription: String {
  233. return [
  234. "## BluetoothManager",
  235. activePeripheralManager.map(String.init(reflecting:)) ?? "No peripheral",
  236. ].joined(separator: "\n")
  237. }
  238. }
  239. extension G7BluetoothManager: CBCentralManagerDelegate {
  240. func centralManagerDidUpdateState(_ central: CBCentralManager) {
  241. dispatchPrecondition(condition: .onQueue(managerQueue))
  242. activePeripheralManager?.centralManagerDidUpdateState(central)
  243. log.default("%{public}@: %{public}@", #function, String(describing: central.state.rawValue))
  244. switch central.state {
  245. case .poweredOn:
  246. managerQueue_scanForPeripheral()
  247. case .resetting, .poweredOff, .unauthorized, .unknown, .unsupported:
  248. fallthrough
  249. @unknown default:
  250. if central.isScanning {
  251. log.debug("Stopping scan on central not powered on")
  252. central.stopScan()
  253. delegate?.bluetoothManagerScanningStatusDidChange(self)
  254. }
  255. }
  256. }
  257. func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
  258. dispatchPrecondition(condition: .onQueue(managerQueue))
  259. if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
  260. for peripheral in peripherals {
  261. log.default("Restoring peripheral from state: %{public}@", peripheral.identifier.uuidString)
  262. handleDiscoveredPeripheral(peripheral)
  263. }
  264. }
  265. }
  266. func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
  267. dispatchPrecondition(condition: .onQueue(managerQueue))
  268. log.info("%{public}@: %{public}@, data = %{public}@", #function, peripheral, String(describing: advertisementData))
  269. managerQueue.async {
  270. self.handleDiscoveredPeripheral(peripheral)
  271. }
  272. }
  273. func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
  274. dispatchPrecondition(condition: .onQueue(managerQueue))
  275. log.default("%{public}@: %{public}@", #function, peripheral)
  276. if let peripheralManager = managedPeripherals[peripheral.identifier] {
  277. peripheralManager.centralManager(central, didConnect: peripheral)
  278. if let delegate = delegate, case .poweredOn = centralManager.state, case .connected = peripheral.state {
  279. if delegate.bluetoothManager(self, readied: peripheralManager) {
  280. managerQueue_stopScanning()
  281. }
  282. }
  283. }
  284. }
  285. func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
  286. dispatchPrecondition(condition: .onQueue(managerQueue))
  287. log.default("%{public}@: %{public}@", #function, peripheral)
  288. // Ignore errors indicating the peripheral disconnected remotely, as that's expected behavior
  289. if let error = error as NSError?, CBError(_nsError: error).code != .peripheralDisconnected {
  290. log.error("%{public}@: %{public}@", #function, error)
  291. if let peripheralManager = activePeripheralManager {
  292. self.delegate?.bluetoothManager(self, readyingFailed: peripheralManager, with: error)
  293. }
  294. }
  295. if let peripheralManager = managedPeripherals[peripheral.identifier] {
  296. let remoteDisconnect: Bool
  297. if let error = error as NSError?, CBError(_nsError: error).code == .peripheralDisconnected {
  298. remoteDisconnect = true
  299. } else {
  300. remoteDisconnect = false
  301. }
  302. self.delegate?.peripheralDidDisconnect(self, peripheralManager: peripheralManager, wasRemoteDisconnect: remoteDisconnect)
  303. }
  304. if peripheral != activePeripheral {
  305. managedPeripherals.removeValue(forKey: peripheral.identifier)
  306. }
  307. scanAfterDelay()
  308. }
  309. func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
  310. dispatchPrecondition(condition: .onQueue(managerQueue))
  311. log.error("%{public}@: %{public}@", #function, String(describing: error))
  312. if let error = error, let peripheralManager = activePeripheralManager {
  313. self.delegate?.bluetoothManager(self, readyingFailed: peripheralManager, with: error)
  314. }
  315. if peripheral != activePeripheral {
  316. managedPeripherals.removeValue(forKey: peripheral.identifier)
  317. }
  318. scanAfterDelay()
  319. }
  320. }
  321. extension G7BluetoothManager: G7PeripheralManagerDelegate {
  322. func peripheralManager(_ manager: G7PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) {
  323. }
  324. func peripheralManagerDidUpdateName(_ manager: G7PeripheralManager) {
  325. }
  326. func peripheralManagerDidConnect(_ manager: G7PeripheralManager) {
  327. }
  328. func completeConfiguration(for manager: G7PeripheralManager) throws {
  329. }
  330. func peripheralManager(_ manager: G7PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) {
  331. guard let value = characteristic.value else {
  332. return
  333. }
  334. switch CGMServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString.uppercased()) {
  335. case .none, .communication?:
  336. return
  337. case .control?:
  338. self.delegate?.bluetoothManager(self, peripheralManager: manager, didReceiveControlResponse: value)
  339. case .backfill?:
  340. self.delegate?.bluetoothManager(self, didReceiveBackfillResponse: value)
  341. case .authentication?:
  342. self.delegate?.bluetoothManager(self, peripheralManager: manager, didReceiveAuthenticationResponse: value)
  343. }
  344. }
  345. }