BluetoothTransmitter.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import CoreBluetooth
  2. import Foundation
  3. import os
  4. /// Generic bluetoothtransmitter class that handles scanning, connect, discover services, discover characteristics, subscribe to receive characteristic, reconnect.
  5. ///
  6. /// - the connection will be set up and a subscribe to a characteristic will be done
  7. /// - a heartbeat function is called each time there's a disconnect (needed for Dexcom) or if there's data received on the receive characteristic
  8. /// - the class does nothing with the data itself
  9. class BluetoothTransmitter: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
  10. // MARK: - private properties
  11. /// the address of the transmitter.
  12. private let deviceAddress: String
  13. /// services to be discovered
  14. private let servicesCBUUIDs: [CBUUID]
  15. /// receive characteristic to which we should subcribe in order to awake the app when the tarnsmitter sends data
  16. private let CBUUID_ReceiveCharacteristic: String
  17. /// centralManager
  18. private var centralManager: CBCentralManager?
  19. /// the receive Characteristic
  20. private var receiveCharacteristic: CBCharacteristic?
  21. /// peripheral, gets value during connect
  22. private(set) var peripheral: CBPeripheral?
  23. /// to be called when data is received or if there's a disconnect, this is the actual heartbeat.
  24. private let heartbeat: () -> Void
  25. // MARK: - Initialization
  26. /// - parameters:
  27. /// - deviceAddress : the bluetooth Mac address
  28. /// - one serviceCBUUID: as string, this is the service to be discovered
  29. /// - CBUUID_Receive: receive characteristic uuid as string, to which subscribe should be done
  30. /// - heartbeat : function to call when data is received on the receive characteristic or when there's a disconnect
  31. init(deviceAddress: String, servicesCBUUID: String, CBUUID_Receive: String, heartbeat: @escaping () -> Void) {
  32. servicesCBUUIDs = [CBUUID(string: servicesCBUUID)]
  33. CBUUID_ReceiveCharacteristic = CBUUID_Receive
  34. self.deviceAddress = deviceAddress
  35. self.heartbeat = heartbeat
  36. let cBCentralManagerOptionRestoreIdentifierKeyToUse = "Loop-" + deviceAddress
  37. super.init()
  38. debug(.deviceManager, "in initialize, creating centralManager for peripheral with address \(deviceAddress)")
  39. centralManager = CBCentralManager(
  40. delegate: self,
  41. queue: nil,
  42. options: [
  43. CBCentralManagerOptionShowPowerAlertKey: true,
  44. CBCentralManagerOptionRestoreIdentifierKey: cBCentralManagerOptionRestoreIdentifierKeyToUse
  45. ]
  46. )
  47. // connect to the device
  48. connect()
  49. }
  50. // MARK: - De-initialization
  51. deinit {
  52. debug(.deviceManager, "deinit called")
  53. // disconnect the device
  54. disconnect()
  55. }
  56. // MARK: - public functions
  57. /// will try to connect to the device, first by calling retrievePeripherals, if peripheral not known, then by calling startScanning
  58. func connect() {
  59. if !retrievePeripherals(centralManager!) {
  60. startScanning()
  61. }
  62. }
  63. /// disconnect the device
  64. func disconnect() {
  65. if let peripheral = peripheral {
  66. var name = "unknown"
  67. if let peripheralName = peripheral.name {
  68. name = peripheralName
  69. }
  70. debug(.deviceManager, "disconnecting from peripheral with name \(name)")
  71. centralManager!.cancelPeripheralConnection(peripheral)
  72. }
  73. }
  74. /// stops scanning
  75. func stopScanning() {
  76. debug(.deviceManager, "in stopScanning")
  77. centralManager!.stopScan()
  78. }
  79. /// calls setNotifyValue for characteristic with value enabled
  80. func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic) {
  81. if let peripheral = peripheral {
  82. debug(
  83. .deviceManager,
  84. "setNotifyValue, for peripheral with name \(peripheral.name ?? "'unknown'"), setting notify for characteristic \(characteristic.uuid.uuidString), to \(enabled.description)"
  85. )
  86. peripheral.setNotifyValue(enabled, for: characteristic)
  87. } else {
  88. debug(
  89. .deviceManager,
  90. "setNotifyValue, for peripheral with name \(peripheral?.name ?? "'unknown'"), failed to set notify for characteristic \(characteristic.uuid.uuidString), to \(enabled.description)"
  91. )
  92. }
  93. }
  94. // MARK: - fileprivate functions
  95. /// start bluetooth scanning for device
  96. fileprivate func startScanning() {
  97. if centralManager!.state == .poweredOn {
  98. debug(.deviceManager, "in startScanning")
  99. centralManager!.scanForPeripherals(withServices: nil, options: nil)
  100. } else {
  101. debug(.deviceManager, "in startScanning. Not started, state is not poweredOn")
  102. }
  103. }
  104. /// stops scanning and connect. To be called after diddiscover
  105. fileprivate func stopScanAndconnect(to peripheral: CBPeripheral) {
  106. centralManager!.stopScan()
  107. self.peripheral = peripheral
  108. peripheral.delegate = self
  109. if peripheral.state == .disconnected {
  110. debug(.deviceManager, " trying to connect")
  111. centralManager!.connect(peripheral, options: nil)
  112. } else {
  113. debug(.deviceManager, " calling centralManager(newCentralManager, didConnect: peripheral")
  114. centralManager(centralManager!, didConnect: peripheral)
  115. }
  116. }
  117. /// try to connect to peripheral to which connection was successfully done previously, and that has a uuid that matches the stored deviceAddress. If such peripheral exists, then try to connect, it's not necessary to start scanning. iOS will connect as soon as the peripheral comes in range, or bluetooth status is switched on, whatever is necessary
  118. ///
  119. /// the result of the attempt to try to find such device, is returned
  120. fileprivate func retrievePeripherals(_ central: CBCentralManager) -> Bool {
  121. debug(.deviceManager, "in retrievePeripherals, deviceaddress is \(deviceAddress)")
  122. if let uuid = UUID(uuidString: deviceAddress) {
  123. debug(.deviceManager, " uuid is not nil")
  124. let peripheralArr = central.retrievePeripherals(withIdentifiers: [uuid])
  125. if !peripheralArr.isEmpty {
  126. peripheral = peripheralArr[0]
  127. if let peripheral = peripheral {
  128. debug(.deviceManager, " trying to connect")
  129. peripheral.delegate = self
  130. central.connect(peripheral, options: nil)
  131. return true
  132. } else {
  133. debug(.deviceManager, " peripheral is nil")
  134. }
  135. } else {
  136. debug(.deviceManager, " uuid is not nil, but central.retrievePeripherals returns 0 peripherals")
  137. }
  138. } else {
  139. debug(.deviceManager, " uuid is nil")
  140. }
  141. return false
  142. }
  143. // MARK: - methods from protocols CBCentralManagerDelegate, CBPeripheralDelegate
  144. func centralManager(
  145. _: CBCentralManager,
  146. didDiscover peripheral: CBPeripheral,
  147. advertisementData _: [String: Any],
  148. rssi _: NSNumber
  149. ) {
  150. // devicename needed unwrapped for logging
  151. var deviceName = "unknown"
  152. if let temp = peripheral.name {
  153. deviceName = temp
  154. }
  155. debug(.deviceManager, "Did discover peripheral with name: \(deviceName)")
  156. // check if stored address not nil, in which case we already connected before and we expect a full match with the already known device name
  157. if peripheral.identifier.uuidString == deviceAddress {
  158. debug(.deviceManager, " stored address matches peripheral address, will try to connect")
  159. stopScanAndconnect(to: peripheral)
  160. } else {
  161. debug(.deviceManager, " stored address does not match peripheral address, ignoring this device")
  162. }
  163. }
  164. func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
  165. debug(.deviceManager, "connected to peripheral with name \(peripheral.name ?? "'unknown'")")
  166. peripheral.discoverServices(servicesCBUUIDs)
  167. }
  168. func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
  169. if let error = error {
  170. debug(
  171. .deviceManager,
  172. "failed to connect, for peripheral with name \(peripheral.name ?? "'unknown'"), with error: \(error.localizedDescription), will try again"
  173. )
  174. } else {
  175. debug(.deviceManager, "failed to connect, for peripheral with name \(peripheral.name ?? "'unknown'"), will try again")
  176. }
  177. centralManager!.connect(peripheral, options: nil)
  178. }
  179. func centralManagerDidUpdateState(_ central: CBCentralManager) {
  180. debug(
  181. .deviceManager,
  182. "in centralManagerDidUpdateState, for peripheral with name \(peripheral?.name ?? "'unknown'"), new state is \(central.state.rawValue)"
  183. )
  184. /// in case status changed to powered on and if device address known then try to retrieveperipherals
  185. if central.state == .poweredOn {
  186. /// try to connect to device to which connection was successfully done previously, this attempt is done by callling retrievePeripherals(central)
  187. _ = retrievePeripherals(central)
  188. }
  189. }
  190. func centralManager(_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
  191. debug(.deviceManager, " didDisconnect peripheral with name \(peripheral.name ?? "'unknown'")")
  192. // call heartbeat, useful for Dexcom transmitters, after a disconnect, then there's probably a new reading available
  193. heartbeat()
  194. if let error = error {
  195. debug(.deviceManager, " error: \(error.localizedDescription)")
  196. }
  197. // if self.peripheral == nil, then a manual disconnect or something like that has occured, no need to reconnect
  198. // otherwise disconnect occurred because of other (like out of range), so let's try to reconnect
  199. if let ownPeripheral = self.peripheral {
  200. debug(.deviceManager, " Will try to reconnect")
  201. centralManager!.connect(ownPeripheral, options: nil)
  202. } else {
  203. debug(.deviceManager, " peripheral is nil, will not try to reconnect")
  204. }
  205. }
  206. func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
  207. debug(.deviceManager, "didDiscoverServices for peripheral with name \(peripheral.name ?? "'unknown'")")
  208. if let error = error {
  209. debug(.deviceManager, " didDiscoverServices error: \(error.localizedDescription)")
  210. }
  211. if let services = peripheral.services {
  212. for service in services {
  213. debug(
  214. .deviceManager,
  215. " Call discovercharacteristics for service with uuid \(String(describing: service.uuid))"
  216. )
  217. peripheral.discoverCharacteristics(nil, for: service)
  218. }
  219. } else {
  220. disconnect()
  221. }
  222. }
  223. func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
  224. debug(
  225. .deviceManager,
  226. "didDiscoverCharacteristicsFor for peripheral with name \(peripheral.name ?? "'unknown'"), for service with uuid \(String(describing: service.uuid))"
  227. )
  228. if let error = error {
  229. debug(.deviceManager, " didDiscoverCharacteristicsFor error: \(error.localizedDescription)")
  230. }
  231. if let characteristics = service.characteristics {
  232. for characteristic in characteristics {
  233. debug(.deviceManager, " characteristic: \(String(describing: characteristic.uuid))")
  234. if characteristic.uuid == CBUUID(string: CBUUID_ReceiveCharacteristic) {
  235. debug(.deviceManager, " found receiveCharacteristic")
  236. receiveCharacteristic = characteristic
  237. peripheral.setNotifyValue(true, for: characteristic)
  238. }
  239. }
  240. } else {
  241. debug(.deviceManager, " Did discover characteristics, but no characteristics listed. There must be some error.")
  242. }
  243. }
  244. func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
  245. if let error = error {
  246. debug(
  247. .deviceManager,
  248. "didUpdateNotificationStateFor for peripheral with name \(peripheral.name ?? "'unkonwn'"), characteristic \(String(describing: characteristic.uuid)), error = \(error.localizedDescription)"
  249. )
  250. }
  251. }
  252. func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor _: CBCharacteristic, error _: Error?) {
  253. debug(.deviceManager, "didUpdateValueFor for peripheral with name \(peripheral.name ?? "'unknown'")")
  254. // call heartbeat
  255. heartbeat()
  256. }
  257. func centralManager(
  258. _: CBCentralManager,
  259. willRestoreState _: [String: Any]
  260. ) {
  261. // willRestoreState must be defined, otherwise the app would crash (because the centralManager was created with a CBCentralManagerOptionRestoreIdentifierKey)
  262. // even if it's an empty function
  263. // trace is called here because it allows us to see in the issue reports if there was a restart after app crash or removed from memory - in all other cases (force closed by user) this function is not called
  264. debug(.deviceManager, "in willRestoreState")
  265. }
  266. }
  267. // MARK: - UserDefaults
  268. extension UserDefaults {
  269. public enum BTKey: String {
  270. /// used as local copy of cgmTransmitterDeviceAddress, will be compared regularly against value in shared UserDefaults
  271. ///
  272. /// this is the local stored (ie not shared with xDrip4iOS) copy of the cgm (bluetooth) device address
  273. case cgmTransmitterDeviceAddress = "com.loopkit.Loop.cgmTransmitterDeviceAddress"
  274. }
  275. /// used as local copy of cgmTransmitterDeviceAddress, will be compared regularly against value in shared UserDefaults
  276. var cgmTransmitterDeviceAddress: String? {
  277. get {
  278. string(forKey: BTKey.cgmTransmitterDeviceAddress.rawValue)
  279. }
  280. set {
  281. set(newValue, forKey: BTKey.cgmTransmitterDeviceAddress.rawValue)
  282. }
  283. }
  284. }