Central.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import CoreBluetooth
  2. typealias RSSI = Int
  3. typealias PeripheralID = UUID
  4. typealias ServiceID = CBUUID
  5. typealias CharacteristicID = CBUUID
  6. typealias ServiceData = [ServiceID: Data]
  7. typealias AdvertisementData = [String: Any]
  8. final class Central {
  9. typealias StateChangeHandler = (Central, CBManagerState) -> Void
  10. typealias DiscoveryHandler = (Central, CBPeripheral, AdvertisementData, RSSI) -> Void
  11. typealias ConnectionChangeHandler = (Central, CBPeripheral, ConnectionChange) -> Void
  12. typealias ServicesWithCharacteristicsDiscoveryHandler = (Central, CBPeripheral, [Error]) -> Void
  13. typealias CharacteristicNotifyCompletionHandler = (Central, Error?) -> Void
  14. typealias CharacteristicValueUpdateHandler = (Central, QualifiedCharacteristic, Data?, Error?) -> Void
  15. typealias CharacteristicWriteCompletionHandler = (Central, QualifiedCharacteristic, Error?) -> Void
  16. private let onServicesWithCharacteristicsInitialDiscovery: ServicesWithCharacteristicsDiscoveryHandler
  17. private var peripheralDelegate: PeripheralDelegate!
  18. private var centralManagerDelegate: CentralManagerDelegate!
  19. private var centralManager: CBCentralManager!
  20. private(set) var isScanning = false
  21. private(set) var activePeripherals = [PeripheralID: CBPeripheral]()
  22. private(set) var connectRegistry = PeripheralTaskRegistry<ConnectTaskController>()
  23. private let servicesWithCharacteristicsDiscoveryRegistry = PeripheralTaskRegistry<ServicesWithCharacteristicsDiscoveryTaskController>()
  24. private let characteristicNotifyRegistry = PeripheralTaskRegistry<CharacteristicNotifyTaskController>()
  25. private let characteristicWriteRegistry = PeripheralTaskRegistry<CharacteristicWriteTaskController>()
  26. init(
  27. onStateChange: @escaping StateChangeHandler,
  28. onDiscovery: @escaping DiscoveryHandler,
  29. onConnectionChange: @escaping ConnectionChangeHandler,
  30. onServicesWithCharacteristicsInitialDiscovery: @escaping ServicesWithCharacteristicsDiscoveryHandler,
  31. onCharacteristicValueUpdate: @escaping CharacteristicValueUpdateHandler
  32. ) {
  33. self.onServicesWithCharacteristicsInitialDiscovery = onServicesWithCharacteristicsInitialDiscovery
  34. self.centralManagerDelegate = CentralManagerDelegate(
  35. onStateChange: papply(weak: self) { central, state in
  36. if state != .poweredOn {
  37. central.activePeripherals.forEach { _, peripheral in
  38. let error = Failure.notPoweredOn(actualState: state)
  39. central.eject(peripheral, error: error)
  40. onConnectionChange(central, peripheral, .disconnected(error))
  41. }
  42. }
  43. onStateChange(central, state)
  44. },
  45. onDiscovery: papply(weak: self, onDiscovery),
  46. onConnectionChange: papply(weak: self) { central, peripheral, change in
  47. central.connectRegistry.updateTask(
  48. key: peripheral.identifier,
  49. action: { $0.handleConnectionChange(change) }
  50. )
  51. switch change {
  52. case .connected:
  53. break
  54. case .failedToConnect(let error), .disconnected(let error):
  55. central.eject(peripheral, error: error ?? PluginError.connectionLost)
  56. }
  57. onConnectionChange(central, peripheral, change)
  58. }
  59. )
  60. self.peripheralDelegate = PeripheralDelegate(
  61. onServicesDiscovery: papply(weak: self) { central, peripheral, error in
  62. central.servicesWithCharacteristicsDiscoveryRegistry.updateTask(
  63. key: peripheral.identifier,
  64. action: { $0.handleServicesDiscovery(peripheral: peripheral, error: error) }
  65. )
  66. },
  67. onCharacteristicsDiscovery: papply(weak: self) { central, service, error in
  68. central.servicesWithCharacteristicsDiscoveryRegistry.updateTask(
  69. key: service.peripheral!.identifier,
  70. action: { $0.handleCharacteristicsDiscovery(service: service, error: error) }
  71. )
  72. },
  73. onCharacteristicNotificationStateUpdate: papply(weak: self) { central, characteristic, error in
  74. central.characteristicNotifyRegistry.updateTask(
  75. key: QualifiedCharacteristic(characteristic),
  76. action: { $0.complete(error: error) }
  77. )
  78. },
  79. onCharacteristicValueUpdate: papply(weak: self) { central, characteristic, error in
  80. onCharacteristicValueUpdate(central, QualifiedCharacteristic(characteristic), characteristic.value, error)
  81. },
  82. onCharacteristicValueWrite: papply(weak: self) { central, characteristic, error in
  83. central.characteristicWriteRegistry.updateTask(
  84. key: QualifiedCharacteristic(characteristic),
  85. action: { $0.handleWrite(error: error) }
  86. )
  87. }
  88. )
  89. self.centralManager = CBCentralManager(
  90. delegate: centralManagerDelegate,
  91. queue: nil
  92. )
  93. }
  94. var state: CBManagerState { return centralManager.state }
  95. func scanForDevices(with services: [ServiceID]?) {
  96. isScanning = true
  97. centralManager.scanForPeripherals(
  98. withServices: services,
  99. options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
  100. )
  101. }
  102. func stopScan() {
  103. centralManager.stopScan()
  104. isScanning = false
  105. }
  106. func connect(to peripheralID: PeripheralID, discover servicesWithCharacteristicsToDiscover: ServicesWithCharacteristicsToDiscover, timeout: TimeInterval?) throws {
  107. let peripheral = try resolve(known: peripheralID)
  108. peripheral.delegate = peripheralDelegate
  109. activePeripherals[peripheral.identifier] = peripheral
  110. connectRegistry.registerTask(
  111. key: peripheralID,
  112. params: .init(),
  113. timeout: timeout.map { timeout in (
  114. duration: timeout,
  115. handler: papply(weak: self) { (central: Central) -> Void in
  116. central.disconnect(from: peripheralID)
  117. }
  118. )},
  119. completion: papply(weak: self) { central, connectionChange in
  120. switch connectionChange {
  121. case .connected:
  122. peripheral.delegate = central.peripheralDelegate
  123. central.discoverServicesWithCharacteristics(
  124. for: peripheral,
  125. discover: servicesWithCharacteristicsToDiscover,
  126. completion: central.onServicesWithCharacteristicsInitialDiscovery
  127. )
  128. case .failedToConnect(_), .disconnected(_):
  129. break
  130. }
  131. }
  132. )
  133. connectRegistry.updateTask(
  134. key: peripheralID,
  135. action: { $0.connect(centralManager: centralManager, peripheral: peripheral) }
  136. )
  137. }
  138. func disconnect(from peripheralID: PeripheralID) {
  139. guard let peripheral = try? resolve(known: peripheralID)
  140. else { return }
  141. centralManager.cancelPeripheralConnection(peripheral)
  142. }
  143. func disconnectAll() {
  144. activePeripherals
  145. .values
  146. .forEach(centralManager.cancelPeripheralConnection)
  147. }
  148. func discoverServicesWithCharacteristics(
  149. for peripheralID: PeripheralID,
  150. discover servicesWithCharacteristicsToDiscover: ServicesWithCharacteristicsToDiscover,
  151. completion: @escaping ServicesWithCharacteristicsDiscoveryHandler
  152. ) throws -> Void {
  153. let peripheral = try resolve(connected: peripheralID)
  154. discoverServicesWithCharacteristics(
  155. for: peripheral,
  156. discover: servicesWithCharacteristicsToDiscover,
  157. completion: completion
  158. )
  159. }
  160. private func discoverServicesWithCharacteristics(
  161. for peripheral: CBPeripheral,
  162. discover servicesWithCharacteristicsToDiscover: ServicesWithCharacteristicsToDiscover,
  163. completion: @escaping ServicesWithCharacteristicsDiscoveryHandler
  164. ) -> Void {
  165. servicesWithCharacteristicsDiscoveryRegistry.registerTask(
  166. key: peripheral.identifier,
  167. params: .init(servicesWithCharacteristicsToDiscover: servicesWithCharacteristicsToDiscover),
  168. completion: papply(weak: self) { central, result in
  169. completion(central, peripheral, result)
  170. }
  171. )
  172. servicesWithCharacteristicsDiscoveryRegistry.updateTask(
  173. key: peripheral.identifier,
  174. action: { $0.start(peripheral: peripheral) }
  175. )
  176. }
  177. func turnNotifications(_ state: OnOff, for qualifiedCharacteristic: QualifiedCharacteristic, completion: @escaping CharacteristicNotifyCompletionHandler) throws {
  178. let characteristic = try resolve(characteristic: qualifiedCharacteristic)
  179. guard [CBCharacteristicProperties.notify, .notifyEncryptionRequired, .indicate, .indicateEncryptionRequired]
  180. .contains(where: characteristic.properties.contains)
  181. else { throw Failure.notificationsNotSupported(qualifiedCharacteristic) }
  182. characteristicNotifyRegistry.registerTask(
  183. key: qualifiedCharacteristic,
  184. params: .init(state: state),
  185. completion: papply(weak: self) { central, result in
  186. completion(central, result)
  187. }
  188. )
  189. characteristicNotifyRegistry.updateTask(
  190. key: qualifiedCharacteristic,
  191. action: { $0.start(characteristic: characteristic) }
  192. )
  193. }
  194. func read(characteristic qualifiedCharacteristic: QualifiedCharacteristic) throws {
  195. let characteristic = try resolve(characteristic: qualifiedCharacteristic)
  196. guard characteristic.properties.contains(.read)
  197. else { throw Failure.notReadable(qualifiedCharacteristic) }
  198. guard let peripheral = characteristic.service?.peripheral
  199. else { throw Failure.peripheralIsUnknown(qualifiedCharacteristic.peripheralID) }
  200. peripheral.readValue(for: characteristic)
  201. }
  202. func writeWithResponse(
  203. value: Data,
  204. characteristic qualifiedCharacteristic: QualifiedCharacteristic,
  205. completion: @escaping CharacteristicWriteCompletionHandler
  206. ) throws {
  207. let characteristic = try resolve(characteristic: qualifiedCharacteristic)
  208. guard characteristic.properties.contains(.write)
  209. else { throw Failure.notWritable(qualifiedCharacteristic) }
  210. characteristicWriteRegistry.registerTask(
  211. key: qualifiedCharacteristic,
  212. params: .init(value: value),
  213. completion: papply(weak: self) { central, error in
  214. completion(central, qualifiedCharacteristic, error)
  215. }
  216. )
  217. guard let peripheral = characteristic.service?.peripheral
  218. else{ throw Failure.peripheralIsUnknown(qualifiedCharacteristic.peripheralID) }
  219. characteristicWriteRegistry.updateTask(
  220. key: qualifiedCharacteristic,
  221. action: { $0.start(peripheral: peripheral) }
  222. )
  223. }
  224. func writeWithoutResponse(
  225. value: Data,
  226. characteristic qualifiedCharacteristic: QualifiedCharacteristic
  227. ) throws {
  228. let characteristic = try resolve(characteristic: qualifiedCharacteristic)
  229. guard characteristic.properties.contains(.writeWithoutResponse)
  230. else { throw Failure.notWritable(qualifiedCharacteristic) }
  231. guard let response = characteristic.service?.peripheral?.writeValue(value, for: characteristic, type: .withoutResponse)
  232. else { throw Failure.characteristicNotFound(qualifiedCharacteristic) }
  233. return response
  234. }
  235. func maximumWriteValueLength(for peripheral: PeripheralID, type: CBCharacteristicWriteType) throws -> Int {
  236. let peripheral = try resolve(connected: peripheral)
  237. return peripheral.maximumWriteValueLength(for: type)
  238. }
  239. private func eject(_ peripheral: CBPeripheral, error: Error) {
  240. peripheral.delegate = nil
  241. activePeripherals[peripheral.identifier] = nil
  242. servicesWithCharacteristicsDiscoveryRegistry.updateTasks(
  243. in: peripheral.identifier,
  244. action: { $0.cancel(error: error) }
  245. )
  246. characteristicNotifyRegistry.updateTasks(
  247. in: peripheral.identifier,
  248. action: { $0.cancel(error: error) }
  249. )
  250. characteristicWriteRegistry.updateTasks(
  251. in: peripheral.identifier,
  252. action: { $0.cancel(error: error) }
  253. )
  254. }
  255. private func resolve(known peripheralID: PeripheralID) throws -> CBPeripheral {
  256. guard let peripheral = centralManager.retrievePeripherals(withIdentifiers: [peripheralID]).first
  257. else { throw Failure.peripheralIsUnknown(peripheralID) }
  258. return peripheral
  259. }
  260. private func resolve(connected peripheralID: PeripheralID) throws -> CBPeripheral {
  261. guard let peripheral = activePeripherals[peripheralID]
  262. else { throw Failure.peripheralIsUnknown(peripheralID) }
  263. guard peripheral.state == .connected
  264. else { throw Failure.peripheralIsNotConnected(peripheralID) }
  265. return peripheral
  266. }
  267. private func resolve(characteristic qualifiedCharacteristic: QualifiedCharacteristic) throws -> CBCharacteristic {
  268. let peripheral = try resolve(connected: qualifiedCharacteristic.peripheralID)
  269. guard let service = peripheral.services?.first(where: { $0.uuid == qualifiedCharacteristic.serviceID })
  270. else { throw Failure.serviceNotFound(qualifiedCharacteristic.serviceID, qualifiedCharacteristic.peripheralID) }
  271. guard let characteristic = service.characteristics?.first(where: { $0.uuid == qualifiedCharacteristic.id })
  272. else { throw Failure.characteristicNotFound(qualifiedCharacteristic) }
  273. return characteristic
  274. }
  275. private enum Failure: Error, CustomStringConvertible {
  276. case notPoweredOn(actualState: CBManagerState)
  277. case peripheralIsUnknown(PeripheralID)
  278. case peripheralIsNotConnected(PeripheralID)
  279. case serviceNotFound(ServiceID, PeripheralID)
  280. case characteristicNotFound(QualifiedCharacteristic)
  281. case notificationsNotSupported(QualifiedCharacteristic)
  282. case notReadable(QualifiedCharacteristic)
  283. case notWritable(QualifiedCharacteristic)
  284. var description: String {
  285. switch self {
  286. case .notPoweredOn(let actualState):
  287. return "Bluetooth is not powered on (the current state code is \(actualState.rawValue))"
  288. case .peripheralIsUnknown(let peripheralID):
  289. return "A peripheral \(peripheralID.uuidString) is unknown (make sure it has been discovered)"
  290. case .peripheralIsNotConnected(let peripheralID):
  291. return "The peripheral \(peripheralID.uuidString) is not connected"
  292. case .serviceNotFound(let serviceID, let peripheralID):
  293. return "A service \(serviceID) is not found in the peripheral \(peripheralID) (make sure it has been discovered)"
  294. case .characteristicNotFound(let qualifiedCharacteristic):
  295. return "A characteristic \(qualifiedCharacteristic.id) is not found in the service \(qualifiedCharacteristic.serviceID) of the peripheral \(qualifiedCharacteristic.peripheralID) (make sure it has been discovered)"
  296. case .notificationsNotSupported(let qualifiedCharacteristic):
  297. return "The characteristic \(qualifiedCharacteristic.id) of the service \(qualifiedCharacteristic.serviceID) of the peripheral \(qualifiedCharacteristic.peripheralID) does not support either notifications or indications"
  298. case .notReadable(let qualifiedCharacteristic):
  299. return "The characteristic \(qualifiedCharacteristic.id) of the service \(qualifiedCharacteristic.serviceID) of the peripheral \(qualifiedCharacteristic.peripheralID) is not readable"
  300. case .notWritable(let qualifiedCharacteristic):
  301. return "The characteristic \(qualifiedCharacteristic.id) of the service \(qualifiedCharacteristic.serviceID) of the peripheral \(qualifiedCharacteristic.peripheralID) is not writable"
  302. }
  303. }
  304. }
  305. }