PluginController.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import class CoreBluetooth.CBUUID
  2. import class CoreBluetooth.CBService
  3. import enum CoreBluetooth.CBManagerState
  4. import var CoreBluetooth.CBAdvertisementDataServiceDataKey
  5. import var CoreBluetooth.CBAdvertisementDataServiceUUIDsKey
  6. import var CoreBluetooth.CBAdvertisementDataManufacturerDataKey
  7. import var CoreBluetooth.CBAdvertisementDataLocalNameKey
  8. final class PluginController {
  9. struct Scan {
  10. let services: [CBUUID]
  11. }
  12. private var central: Central?
  13. private var scan: StreamingTask<Scan>?
  14. var stateSink: EventSink? {
  15. didSet {
  16. DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
  17. self?.reportState()
  18. }
  19. }
  20. }
  21. var messageQueue: [CharacteristicValueInfo] = [];
  22. var connectedDeviceSink: EventSink?
  23. var characteristicValueUpdateSink: EventSink?
  24. func initialize(name: String, completion: @escaping PlatformMethodCompletionHandler) {
  25. if let central = central {
  26. central.stopScan()
  27. central.disconnectAll()
  28. }
  29. central = Central(
  30. onStateChange: papply(weak: self) { context, _, state in
  31. context.reportState(state)
  32. },
  33. onDiscovery: papply(weak: self) { context, _, peripheral, advertisementData, rssi in
  34. guard let sink = context.scan?.sink
  35. else { assert(false); return }
  36. let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? ServiceData ?? [:]
  37. let serviceUuids = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []
  38. let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data ?? Data();
  39. let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? peripheral.name ?? String();
  40. let deviceDiscoveryMessage = DeviceScanInfo.with {
  41. $0.id = peripheral.identifier.uuidString
  42. $0.name = name
  43. $0.rssi = Int32(rssi)
  44. $0.serviceData = serviceData
  45. .map { entry in
  46. ServiceDataEntry.with {
  47. $0.serviceUuid = Uuid.with { $0.data = entry.key.data }
  48. $0.data = entry.value
  49. }
  50. }
  51. $0.serviceUuids = serviceUuids.map { entry in Uuid.with { $0.data = entry.data }}
  52. $0.manufacturerData = manufacturerData
  53. }
  54. sink.add(.success(deviceDiscoveryMessage))
  55. },
  56. onConnectionChange: papply(weak: self) { context, central, peripheral, change in
  57. let failure: (code: ConnectionFailure, message: String)?
  58. switch change {
  59. case .connected:
  60. // Wait for services & characteristics to be discovered
  61. return
  62. case .failedToConnect(let underlyingError), .disconnected(let underlyingError):
  63. failure = underlyingError.map { (.failedToConnect, "\($0)") }
  64. }
  65. let message = DeviceInfo.with {
  66. $0.id = peripheral.identifier.uuidString
  67. $0.connectionState = encode(peripheral.state)
  68. if let error = failure {
  69. $0.failure = GenericFailure.with {
  70. $0.code = Int32(error.code.rawValue)
  71. $0.message = error.message
  72. }
  73. }
  74. }
  75. context.connectedDeviceSink?.add(.success(message))
  76. },
  77. onServicesWithCharacteristicsInitialDiscovery: papply(weak: self) { context, central, peripheral, errors in
  78. guard let sink = context.connectedDeviceSink
  79. else { assert(false); return }
  80. let message = DeviceInfo.with {
  81. $0.id = peripheral.identifier.uuidString
  82. $0.connectionState = encode(peripheral.state)
  83. if !errors.isEmpty {
  84. $0.failure = GenericFailure.with {
  85. $0.code = Int32(ConnectionFailure.unknown.rawValue)
  86. $0.message = errors.map(String.init(describing:)).joined(separator: "\n")
  87. }
  88. }
  89. }
  90. sink.add(.success(message))
  91. },
  92. onCharacteristicValueUpdate: papply(weak: self) { context, central, characteristic, value, error in
  93. let message = CharacteristicValueInfo.with {
  94. $0.characteristic = CharacteristicAddress.with {
  95. $0.characteristicUuid = Uuid.with { $0.data = characteristic.id.data }
  96. $0.serviceUuid = Uuid.with { $0.data = characteristic.serviceID.data }
  97. $0.deviceID = characteristic.peripheralID.uuidString
  98. }
  99. if let value = value {
  100. $0.value = value
  101. }
  102. if let error = error {
  103. $0.failure = GenericFailure.with {
  104. $0.code = Int32(CharacteristicValueUpdateFailure.unknown.rawValue)
  105. $0.message = "\(error)"
  106. }
  107. }
  108. }
  109. let sink = context.characteristicValueUpdateSink
  110. if (sink != nil) {
  111. sink!.add(.success(message))
  112. } else {
  113. // In case message arrives before sink is created
  114. context.messageQueue.append(message);
  115. }
  116. }
  117. )
  118. completion(.success(nil))
  119. }
  120. func deinitialize(name: String, completion: @escaping PlatformMethodCompletionHandler) {
  121. guard let central = central
  122. else {
  123. completion(.failure(PluginError.notInitialized.asFlutterError))
  124. return
  125. }
  126. central.stopScan()
  127. central.disconnectAll()
  128. self.central = nil
  129. completion(.success(nil))
  130. }
  131. func scanForDevices(name: String, args: ScanForDevicesRequest, completion: @escaping PlatformMethodCompletionHandler) {
  132. guard let central = central
  133. else {
  134. completion(.failure(PluginError.notInitialized.asFlutterError))
  135. return
  136. }
  137. assert(!central.isScanning)
  138. scan = StreamingTask(parameters: .init(services: args.serviceUuids.map({ uuid in CBUUID(data: uuid.data) })))
  139. completion(.success(nil))
  140. }
  141. func startScanning(sink: EventSink) -> FlutterError? {
  142. guard let central = central
  143. else { return PluginError.notInitialized.asFlutterError }
  144. guard let scan = scan
  145. else { return PluginError.internalInconcictency(details: "a scanning task has not been initialized yet, but a client has subscribed").asFlutterError }
  146. self.scan = scan.with(sink: sink)
  147. central.scanForDevices(with: scan.parameters.services)
  148. return nil
  149. }
  150. func stopScanning() -> FlutterError? {
  151. central?.stopScan()
  152. return nil
  153. }
  154. func connectToDevice(name: String, args: ConnectToDeviceRequest, completion: @escaping PlatformMethodCompletionHandler) {
  155. guard let central = central
  156. else {
  157. completion(.failure(PluginError.notInitialized.asFlutterError))
  158. return
  159. }
  160. guard let deviceID = UUID(uuidString: args.deviceID)
  161. else {
  162. completion(.failure(PluginError.invalidMethodCall(method: name, details: "\"deviceID\" is invalid").asFlutterError))
  163. return
  164. }
  165. let servicesWithCharacteristicsToDiscover: ServicesWithCharacteristicsToDiscover
  166. if args.hasServicesWithCharacteristicsToDiscover {
  167. let items = args.servicesWithCharacteristicsToDiscover.items.reduce(
  168. into: [ServiceID: [CharacteristicID]](),
  169. { dict, item in
  170. let serviceID = CBUUID(data: item.serviceID.data)
  171. let characteristicIDs = item.characteristics.map { CBUUID(data: $0.data) }
  172. dict[serviceID] = characteristicIDs
  173. }
  174. )
  175. servicesWithCharacteristicsToDiscover = ServicesWithCharacteristicsToDiscover.some(items.mapValues(CharacteristicsToDiscover.some))
  176. } else {
  177. servicesWithCharacteristicsToDiscover = .all
  178. }
  179. let timeout = args.timeoutInMs > 0 ? TimeInterval(args.timeoutInMs) / 1000 : nil
  180. completion(.success(nil))
  181. if let sink = connectedDeviceSink {
  182. let message = DeviceInfo.with {
  183. $0.id = args.deviceID
  184. $0.connectionState = encode(.connecting)
  185. }
  186. sink.add(.success(message))
  187. } else {
  188. print("Warning! No event channel set up to report a connection update")
  189. }
  190. do {
  191. try central.connect(
  192. to: deviceID,
  193. discover: servicesWithCharacteristicsToDiscover,
  194. timeout: timeout
  195. )
  196. } catch {
  197. guard let sink = connectedDeviceSink
  198. else {
  199. print("Warning! No event channel set up to report a connection failure: \(error)")
  200. return
  201. }
  202. let message = DeviceInfo.with {
  203. $0.id = args.deviceID
  204. $0.connectionState = encode(.disconnected)
  205. $0.failure = GenericFailure.with {
  206. $0.code = Int32(ConnectionFailure.failedToConnect.rawValue)
  207. $0.message = "\(error)"
  208. }
  209. }
  210. sink.add(.success(message))
  211. }
  212. }
  213. func disconnectFromDevice(name: String, args: ConnectToDeviceRequest, completion: @escaping PlatformMethodCompletionHandler) {
  214. guard let central = central
  215. else {
  216. completion(.failure(PluginError.notInitialized.asFlutterError))
  217. return
  218. }
  219. guard let deviceID = UUID(uuidString: args.deviceID)
  220. else {
  221. completion(.failure(PluginError.invalidMethodCall(method: name, details: "\"deviceID\" is invalid").asFlutterError))
  222. return
  223. }
  224. completion(.success(nil))
  225. central.disconnect(from: deviceID)
  226. }
  227. func discoverServices(name: String, args: DiscoverServicesRequest, completion: @escaping PlatformMethodCompletionHandler) {
  228. guard let central = central
  229. else {
  230. completion(.failure(PluginError.notInitialized.asFlutterError))
  231. return
  232. }
  233. guard let deviceID = UUID(uuidString: args.deviceID)
  234. else {
  235. completion(.failure(PluginError.invalidMethodCall(method: name, details: "\"deviceID\" is invalid").asFlutterError))
  236. return
  237. }
  238. func makeDiscoveredService(service: CBService) -> DiscoveredService {
  239. DiscoveredService.with {
  240. $0.serviceUuid = Uuid.with { $0.data = service.uuid.data }
  241. $0.characteristicUuids = (service.characteristics ?? []).map { characteristic in
  242. Uuid.with { $0.data = characteristic.uuid.data }
  243. }
  244. $0.characteristics = (service.characteristics ?? []).map { characteristic in
  245. DiscoveredCharacteristic.with{
  246. $0.characteristicID = Uuid.with{$0.data = characteristic.uuid.data}
  247. if characteristic.service?.uuid.data != nil {
  248. $0.serviceID = Uuid.with{$0.data = characteristic.service!.uuid.data}
  249. }
  250. $0.isReadable = characteristic.properties.contains(.read)
  251. $0.isWritableWithResponse = characteristic.properties.contains(.write)
  252. $0.isWritableWithoutResponse = characteristic.properties.contains(.writeWithoutResponse)
  253. $0.isNotifiable = characteristic.properties.contains(.notify)
  254. $0.isIndicatable = characteristic.properties.contains(.indicate)
  255. }
  256. }
  257. $0.includedServices = (service.includedServices ?? []).map(makeDiscoveredService)
  258. }
  259. }
  260. do {
  261. try central.discoverServicesWithCharacteristics(
  262. for: deviceID,
  263. discover: .all,
  264. completion: { central, peripheral, errors in
  265. completion(.success(DiscoverServicesInfo.with {
  266. $0.deviceID = deviceID.uuidString
  267. $0.services = (peripheral.services ?? []).map(makeDiscoveredService)
  268. }))
  269. }
  270. )
  271. } catch {
  272. completion(.failure(PluginError.unknown(error).asFlutterError))
  273. }
  274. }
  275. func enableCharacteristicNotifications(name: String, args: NotifyCharacteristicRequest, completion: @escaping PlatformMethodCompletionHandler) {
  276. guard let central = central
  277. else {
  278. completion(.failure(PluginError.notInitialized.asFlutterError))
  279. return
  280. }
  281. guard let characteristic = QualifiedCharacteristicIDFactory().make(from: args.characteristic)
  282. else {
  283. completion(.failure(PluginError.invalidMethodCall(method: name, details: "characteristic, service, and peripheral IDs are required").asFlutterError))
  284. return
  285. }
  286. do {
  287. try central.turnNotifications(.on, for: characteristic, completion: { _, error in
  288. if let error = error {
  289. completion(.failure(PluginError.unknown(error).asFlutterError))
  290. } else {
  291. completion(.success(nil))
  292. }
  293. })
  294. } catch {
  295. completion(.failure(PluginError.unknown(error).asFlutterError))
  296. }
  297. }
  298. func disableCharacteristicNotifications(name: String, args: NotifyNoMoreCharacteristicRequest, completion: @escaping PlatformMethodCompletionHandler) {
  299. guard let central = central
  300. else {
  301. completion(.failure(PluginError.notInitialized.asFlutterError))
  302. return
  303. }
  304. guard let characteristic = QualifiedCharacteristicIDFactory().make(from: args.characteristic)
  305. else {
  306. completion(.failure(PluginError.invalidMethodCall(method: name, details: "characteristic, service, and peripheral IDs are required").asFlutterError))
  307. return
  308. }
  309. do {
  310. try central.turnNotifications(.off, for: characteristic, completion: { _, error in
  311. if let error = error {
  312. completion(.failure(PluginError.unknown(error).asFlutterError))
  313. } else {
  314. completion(.success(nil))
  315. }
  316. })
  317. } catch {
  318. completion(.failure(PluginError.unknown(error).asFlutterError))
  319. }
  320. }
  321. func readCharacteristic(name: String, args: ReadCharacteristicRequest, completion: @escaping PlatformMethodCompletionHandler) {
  322. guard let central = central
  323. else {
  324. completion(.failure(PluginError.notInitialized.asFlutterError))
  325. return
  326. }
  327. guard let characteristic = QualifiedCharacteristicIDFactory().make(from: args.characteristic)
  328. else {
  329. completion(.failure(PluginError.invalidMethodCall(method: name, details: "characteristic, service, and peripheral IDs are required").asFlutterError))
  330. return
  331. }
  332. completion(.success(nil))
  333. do {
  334. try central.read(characteristic: characteristic)
  335. } catch {
  336. guard let sink = characteristicValueUpdateSink
  337. else {
  338. print("Warning! No subscription to report a characteristic read failure: \(error)")
  339. return
  340. }
  341. let message = CharacteristicValueInfo.with {
  342. $0.characteristic = args.characteristic
  343. $0.failure = GenericFailure.with {
  344. $0.code = Int32(CharacteristicValueUpdateFailure.unknown.rawValue)
  345. $0.message = "\(error)"
  346. }
  347. }
  348. sink.add(.success(message))
  349. }
  350. }
  351. func writeCharacteristicWithResponse(name: String, args: WriteCharacteristicRequest, completion: @escaping PlatformMethodCompletionHandler) {
  352. guard let central = central
  353. else {
  354. completion(.failure(PluginError.notInitialized.asFlutterError))
  355. return
  356. }
  357. guard let characteristic = QualifiedCharacteristicIDFactory().make(from: args.characteristic)
  358. else {
  359. completion(.failure(PluginError.invalidMethodCall(method: name, details: "characteristic, service, and peripheral IDs are required").asFlutterError))
  360. return
  361. }
  362. do {
  363. try central.writeWithResponse(
  364. value: args.value,
  365. characteristic: QualifiedCharacteristic(id: characteristic.id, serviceID: characteristic.serviceID, peripheralID: characteristic.peripheralID),
  366. completion: { _, characteristic, error in
  367. let result = WriteCharacteristicInfo.with {
  368. $0.characteristic = CharacteristicAddress.with {
  369. $0.characteristicUuid = Uuid.with { $0.data = characteristic.id.data }
  370. $0.serviceUuid = Uuid.with { $0.data = characteristic.serviceID.data }
  371. $0.deviceID = characteristic.peripheralID.uuidString
  372. }
  373. if let error = error {
  374. $0.failure = GenericFailure.with {
  375. $0.code = Int32(WriteCharacteristicFailure.unknown.rawValue)
  376. $0.message = "\(error)"
  377. }
  378. }
  379. }
  380. completion(.success(result))
  381. }
  382. )
  383. } catch {
  384. let result = WriteCharacteristicInfo.with {
  385. $0.characteristic = args.characteristic
  386. $0.failure = GenericFailure.with {
  387. $0.code = Int32(WriteCharacteristicFailure.unknown.rawValue)
  388. $0.message = "\(error)"
  389. }
  390. }
  391. completion(.success(result))
  392. }
  393. }
  394. func writeCharacteristicWithoutResponse(name: String, args: WriteCharacteristicRequest, completion: @escaping PlatformMethodCompletionHandler) {
  395. guard let central = central
  396. else {
  397. completion(.failure(PluginError.notInitialized.asFlutterError))
  398. return
  399. }
  400. guard let characteristic = QualifiedCharacteristicIDFactory().make(from: args.characteristic)
  401. else {
  402. completion(.failure(PluginError.invalidMethodCall(method: name, details: "characteristic, service, and peripheral IDs are required").asFlutterError))
  403. return
  404. }
  405. let result: WriteCharacteristicInfo
  406. do {
  407. try central.writeWithoutResponse(
  408. value: args.value,
  409. characteristic: QualifiedCharacteristic(id: characteristic.id, serviceID: characteristic.serviceID, peripheralID: characteristic.peripheralID)
  410. )
  411. result = WriteCharacteristicInfo.with {
  412. $0.characteristic = args.characteristic
  413. }
  414. } catch {
  415. result = WriteCharacteristicInfo.with {
  416. $0.characteristic = args.characteristic
  417. $0.failure = GenericFailure.with {
  418. $0.code = Int32(WriteCharacteristicFailure.unknown.rawValue)
  419. $0.message = "\(error)"
  420. }
  421. }
  422. }
  423. completion(.success(result))
  424. }
  425. func reportMaximumWriteValueLength(name: String, args: NegotiateMtuRequest, completion: @escaping PlatformMethodCompletionHandler) {
  426. guard let central = central
  427. else {
  428. completion(.failure(PluginError.notInitialized.asFlutterError))
  429. return
  430. }
  431. guard let peripheralID = UUID(uuidString: args.deviceID)
  432. else {
  433. completion(.failure(PluginError.invalidMethodCall(method: name, details: "peripheral ID is required").asFlutterError))
  434. return
  435. }
  436. let result: NegotiateMtuInfo
  437. do {
  438. let mtu = try central.maximumWriteValueLength(for: peripheralID, type: .withoutResponse)
  439. result = NegotiateMtuInfo.with {
  440. $0.deviceID = args.deviceID
  441. $0.mtuSize = Int32(mtu)
  442. }
  443. } catch {
  444. result = NegotiateMtuInfo.with {
  445. $0.deviceID = args.deviceID
  446. $0.failure = GenericFailure.with {
  447. $0.code = Int32(MaximumWriteValueLengthRetrieval.unknown.rawValue)
  448. $0.message = "\(error)"
  449. }
  450. }
  451. }
  452. completion(.success(result))
  453. }
  454. private func reportState(_ knownState: CBManagerState? = nil) {
  455. guard let sink = stateSink
  456. else { return }
  457. let stateToReport = knownState ?? central?.state ?? .unknown
  458. let message = BleStatusInfo.with { $0.status = encode(stateToReport) }
  459. sink.add(.success(message))
  460. }
  461. }