-
Notifications
You must be signed in to change notification settings - Fork 96
/
Copy pathRNBluetoothClassic.swift
677 lines (586 loc) · 25 KB
/
RNBluetoothClassic.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
//
// RNBluetoothClassic.swift
// RNBluetoothClassic
//
// Created by Ken Davidson on 2019-06-17.
// Copyright © 2019 Facebook. All rights reserved.
//
import Foundation
import ExternalAccessory
import CoreBluetooth
/**
Implementation of the RNBluetoothClassic React Native module. For information on how this
module was created and developed see the following:
- https://facebook.github.io/react-native/docs/native-modules-setup
- https://facebook.github.io/react-native/docs/native-modules-ios
or the README.md located in the parent (Javascript) project.
RNBluetoothClassic is responsible for interacting with the ExternalAccessory framework
and providing wrappers for listing, connecting, reading, writing, etc. The actual
communication has been handed off to the BluetoothDevice class - allowing (in the future)
more that one BluetoothDevice to be connected at one time.
Currently the module communicates using Base64 .utf8 encoded strings. This should
be updated in the future to use [UInt8] to match the communication on the
BluetoothDevice side. This means that the responsiblity of converting and managing
data is done in Javascript/client rather than in the module.
*/
@objc(RNBluetoothClassic)
class RNBluetoothClassic : NSObject, RCTBridgeModule {
static func moduleName() -> String! {
return "RNBluetoothClassic"
}
@objc var bridge: RCTBridge!
var connectionFactories: Dictionary<String,DeviceConnectionFactory>
private let eaManager: EAAccessoryManager
/**
* By default, initialize CBCentralManager when bluetooth is not available prompts
* "Turn On Bluetooth to Allow [app name] to Connect to Accessories" dialog.
* See CBCentralManagerOptionShowPowerAlertKey for more details about this behavior
*
* By using Lazy initialization on CBCentralManager it will prompt bluetooth permission
* on first call of any bluetooth-related method.
*/
private lazy var cbCentral: CBCentralManager = CBCentralManager()
private let notificationCenter: NotificationCenter
private let supportedProtocols: [String]
private var listeners: Dictionary<String,Int>
private var connections: Dictionary<String,DeviceConnection>
/**
* Initializes the RNBluetoothClassic module. At this point it's not quite as customizable as the
* Java version, but I'm slowly working on figuring out how to incorporate the same logic in a
* Swify way, but my ObjC and Swift is not strong, very very not strong.
*/
override init() {
self.eaManager = EAAccessoryManager.shared()
self.notificationCenter = NotificationCenter.default
self.supportedProtocols = Bundle.main
.object(forInfoDictionaryKey: "UISupportedExternalAccessoryProtocols") as! [String]
self.connectionFactories = Dictionary()
self.connectionFactories["delimited"] = DelimitedStringDeviceConnectionFactory()
self.connections = Dictionary()
self.listeners = Dictionary()
super.init()
self.registerForLocalNotifications()
}
/**
Clean up:
- Notifications
- Observers
- Any other objects that need to be manually released
*/
deinit {
unregisterForLocalNotifications()
}
/**
Register with the NotificationCenter and add all appropriate Observers. Currently the available
notification types are:
- .EAAccessoryDidConnect = BTEvent.BLUETOOTH_CONNECTED
- .EAAccessoryDidDisconnect = BTEvent.BLUETOOTH_DISCONNECTED
using the appropriate BTEvent type(s)
*/
private func registerForLocalNotifications() {
eaManager.registerForLocalNotifications()
notificationCenter.addObserver(self,
selector: #selector(accessoryDidConnect),
name: .EAAccessoryDidConnect,
object: nil)
notificationCenter.addObserver(self,
selector: #selector(accessoryDidDisconnect),
name: .EAAccessoryDidDisconnect,
object: nil)
}
/**
Remove all Observers and unregister with the NotificationCenter
*/
private func unregisterForLocalNotifications() {
notificationCenter.removeObserver(self)
eaManager.unregisterForLocalNotifications()
}
/**
* Implements the EAAccessoryDidConnect delegate observer. Fires a DeviceConnected event
* to react native. In terms of IOS connection this means that the perfipheral is ON and BONDED
* (which IOS calls connected, the device might actually show connected) but this does not mean
* that there is an active socket/stream open
*/
@objc
func accessoryDidConnect(_ notification:Notification) {
// Unlike the disconnect we just need to pass the event to the application. It
// will decide whether or not to connect.
if let connected: EAAccessory = notification.userInfo!["EAAccessoryKey"] as? EAAccessory {
sendEvent(EventType.DEVICE_CONNECTED.name,
body: NativeDevice(accessory: connected).map())
}
}
/**
* Received a disconnct notification from IOS. If we are currently connected to this device, we need to disconnect it
* and remove it from the connected peripherals map. In terms of IOS connection this means that the perfipheral
* is OFF and BONDED (which IOS calls connected, the device might actually show connected) but this does not mean
* that there is an active socket/stream open
*/
@objc
func accessoryDidDisconnect(_ notification:Notification) {
if let disconnected: EAAccessory = notification.userInfo!["EAAccessoryKey"] as? EAAccessory {
// If we are currently connected to this, then we need to
// disconnected it and remove the current peripheral
if let currentDevice = connections.removeValue(forKey: disconnected.serialNumber) {
currentDevice.disconnect()
}
// Finally send the notification
sendEvent(EventType.DEVICE_DISCONNECTED.name,
body: NativeDevice(accessory: disconnected).map())
}
}
/**
RCTEventEmitter -
Turned off the main queue setup for now - testing. But this will need to be turned
on as the ExternalAccessory event handling needs to occur on a separate thread and
be haneled correctly.
*/
static func requiresMainQueueSetup() -> Bool {
return true;
}
/**
RCTEventEmitter -
Return the constants for BTEvents and BTCharsets specific to IOS.
*/
func constantsToExport() -> [AnyHashable : Any]! {
return [:];
}
/**
* Whether or not bluetooth is currently enabled - currently this is done by using the
* CoreBluetooth (BLE) framework, as it should hopefully be good enough for performing
* bluetooth system tasks.
* - parameter resolver: resovles with true|false based on enable
* - parameter reject: should never be rejected
*/
@objc
func isBluetoothEnabled(
_ resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
resolve(checkBluetoothAdapter())
}
/**
* Check the Core Bluetooth Central Manager for status
*/
private func checkBluetoothAdapter() -> Bool {
var enabled = false
if #available(iOS 10.0, *) {
enabled = (cbCentral.state == CBManagerState.poweredOn)
} else {
enabled = (cbCentral.state.rawValue == CBCentralManagerState.poweredOn.rawValue)
}
return enabled
}
private func rejectBluetoothDisabled(rejecter reject: RCTPromiseRejectBlock) {
let error = BluetoothError.BLUETOOTH_DISABLED
reject(error.info.abbr, error.info.message, error.error)
}
/**
* Lists currently connected/bonded devices - devices must have matching protocols
* to those configured in the .plist UISupportedExternalAccessoryProtocols key.
* The call should never be rejected, only resolved with an empty list. Uses
* the EAAccessoryManager.shared().connectedAccessories to get the devices.
* - parameter resolver: resovles with the list (possibly empty)
* - parameter reject: should never be rejected
*/
@objc
func getBondedDevices(
_ resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
var accessories:[NSDictionary] = [NSDictionary]()
for connected in eaManager.connectedAccessories {
let device = NativeDevice(accessory: connected)
accessories.append(device.map())
}
resolve(accessories)
}
/**
* Determines whether the device is still connected and attempts to open
* a connection to it. This is done by providing the self to the BluetoothDevice
* as a delegate during the open request. If the device cannot be connected to,
* fails connection or bluetooth is just not enabled, then the request
* is rejected.
* - parameter _: the device Id/address in which to connect
* - parameter resolve: resolve when the connection has been established
* - parameter reject: reject a failed connection
*/
@objc
func connectToDevice(
_ deviceId: String,
options: NSDictionary,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
guard connections[deviceId] == nil else {
let connection = connections[deviceId]!
resolve(NativeDevice(accessory: connection.accessory).map())
return
}
var connectionOptions = Dictionary<String,Any>()
connectionOptions.merge(options as! [String : Any]) { $1 }
let connectionType = connectionOptions["CONNECTION_TYPE"] ?? "delimited";
guard let factory = connectionFactories[connectionType as! String] else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 200)
reject("invalid_connection_type", "Invalid connection type", error)
return;
}
// Now check to see that the device is still connected and available
// using the EAAccessoryManager, if found we create a new BluetoothDevice
// which will be responsible for managing our connection
if let accessory = eaManager.connectedAccessories.first(where: { $0.serialNumber == deviceId }) {
if let protocolString:String = determineProtocolString(forDevice: accessory) {
connectionOptions["PROTOCOL_STRING"] = protocolString
NSLog("(RNBluetoothClassic:connect) Connecting to %@ with %@", accessory.name, protocolString)
let connection = factory.create(accessory: accessory, options: connectionOptions)
do {
try connection.connect()
self.connections[deviceId] = connection
resolve(NativeDevice(accessory: accessory).map())
} catch {
let error = BluetoothError.CONNECTION_FAILED
reject(error.info.abbr, error.info.message, error.error)
}
} else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 201)
reject("connect_failed", "Device could not establish connection", error)
}
} else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 202)
reject("device_not_found", "Device is not currently bonded/paired", error)
}
}
private func determineProtocolString(forDevice accessory:EAAccessory) -> String? {
return supportedProtocols.first(where: {
accessory.protocolStrings.contains($0)
})
}
/**
Disconnect from the currently connected device.
- parameter _: the device Id/address from which we will disconnect
- parameter resolver: resolve the disconnection
- parameter reject: reject if the disconnection fails
*/
@objc
func disconnectFromDevice(
_ deviceId: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
guard let connected = connections.removeValue(forKey: deviceId) else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 203)
reject("device_not_connected", "Device is not currently connected", error)
return
}
NSLog("(RNBluetoothClassic:disconnect) Disconnecting %@", connected.accessory.name)
connected.disconnect()
resolve(true)
}
/**
Determine whether there is a connected device
- parameter _: device Id to check for connection
- parameter resolver: resolve with the whether the device is connected
- parameter rejecter: reject if Bluetooth is disabled or there are any issues.
*/
@objc
func isDeviceConnected(
_ deviceId: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
resolve(connections[deviceId] != nil)
}
/**
Resolve the connected device.
- parameter _: device Id to check for connection
- parameter resolver: resolve with the whether the device is connected
- parameter rejecter: reject if Bluetooth is disabled or there are any issues.
*/
@objc
func getConnectedDevice(
_ deviceId: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
guard let connected = connections[deviceId] else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 203)
reject("device_not_connected", "Device is not currently connected", error)
return
}
resolve(NativeDevice(accessory: connected.accessory).map())
}
/**
Resolve with a list of connected devices.
- parameter resolver: resolve with the connected device list
- parameter rejecter: reject if Bluetooth is disabled or there are issues with the list
*/
@objc
func getConnectedDevices(
_ resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
var accessories:[NSDictionary] = [NSDictionary]()
for (_, device) in connections {
let device = NativeDevice(accessory: device.accessory)
accessories.append(device.map())
}
resolve(accessories)
}
/**
Writes the supplied message to the device - the message should be Base64
encoded.
- parameter _: device Id to check for connection
- parameter message: the message to send
- parameter resolver: resolve with the whether the device is connected
- parameter rejecter: reject if Bluetooth is disabled or there are any issues.
*/
@objc
func writeToDevice(
_ deviceId: String,
withMessage message: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
guard let connected = connections[deviceId] else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 203)
reject("device_not_connected", "Device is not currently connected", error)
return
}
if let decoded = Data(base64Encoded: message) {
resolve(connected.write(decoded))
} else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 204)
reject("cannot_decode_data", "Cannot decode data", error)
}
}
/**
Attempts to read all of the data from the buffer, ignoring the delimiter. If no
data is in the buffer, an empty String will be returned.
- parameter _: device Id to check for connection
- parameter resolver: resolve with the whether the device is connected
- parameter rejecter: reject if Bluetooth is disabled or there are any issues.
*/
@objc
func readFromDevice(
_ deviceId: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
guard let connected = connections[deviceId] else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 203)
reject("device_not_connected", "Device is not currently connected", error)
return
}
resolve(connected.read())
}
/**
Clear the buffer.
- parameter _: device Id to check for connection
- parameter resolver: resolve with the whether the device is connected
- parameter rejecter: reject if Bluetooth is disabled or there are any issues.
*/
@objc
func clearFromDevice(
_ deviceId: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
guard let connected = connections[deviceId] else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 203)
reject("device_not_connected", "Device is not currently connected", error)
return
}
connected.clear()
resolve(true)
}
/**
Resolves with the amount of data available to be read. This is the total
buffer length, with no regard for the delimiter. Should possibly add in
a delimiter value.
- parameter _: device Id to check for connection
- parameter resolver: resolve with the whether the device is connected
- parameter rejecter: reject if Bluetooth is disabled or there are any issues.
*/
@objc
func availableFromDevice(
_ deviceId: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
guard checkBluetoothAdapter() else {
rejectBluetoothDisabled(rejecter: reject)
return
}
guard let connected = connections[deviceId] else {
let error = NSError(domain: "kjd.reactnative.bluetooth", code: 203)
reject("device_not_connected", "Device is not currently connected", error)
return
}
resolve(connected.available())
}
func sendEvent(_ eventName: String, body: Any?) {
guard let bridge = self.bridge else {
NSLog("Error when sending event \(eventName) with body \(body ?? ""); Bridge not set")
return
}
guard (listeners[eventName] != nil || listeners[eventName] == 0) else {
NSLog("Sending '%@' with no listeners registered; was skipped", eventName)
return
}
var data: [Any] = [eventName]
if let actualBody = body {
data.append(actualBody)
}
bridge.enqueueJSCall("RCTDeviceEventEmitter",
method: "emit",
args: data,
completion: nil)
}
@objc
func addListener(
_ requestedEvent: String
) {
var eventName = requestedEvent
var deviceId: String?
if (requestedEvent.contains("@")) {
let split = requestedEvent.split(separator: "@")
eventName = String(split[0])
deviceId = String(split[1])
}
guard EventType.allCases.firstIndex(where: { $0.name == eventName}) ?? -1 >= 0 else {
NSLog("%@ is not a supported EventType", eventName)
return
}
// When saving the listener, we need to use the requested event now that we know
// it's legal, this way we maintain the DEVICE_READ@<serialNumber>
let listenerCount = listeners[requestedEvent] ?? 0
listeners[requestedEvent] = listenerCount + 1
if let forDevice = deviceId {
onAddListener(eventName, deviceId: forDevice)
}
}
@objc
func removeListener(_ requestedEvent: String) {
var eventName = requestedEvent
var eventDevice: String?
if (requestedEvent.contains("@")) {
let split = requestedEvent.split(separator: "@")
eventName = String(split[0])
eventDevice = String(split[1])
}
guard EventType.allCases.firstIndex(where: { $0.name == eventName}) ?? -1 >= 0 else {
NSLog("%@ is not a supported EventType", eventName)
return
}
let listenerCount = listeners[eventName] ?? 0
if listenerCount > 0 {
listeners[eventName] = listenerCount - 1
if let deviceId = eventDevice {
onRemoveListener(eventName, deviceId: deviceId)
}
}
}
@objc
func removeAllListeners(_ requestedEvent: String) {
var eventName = requestedEvent
var eventDevice: String?
if (requestedEvent.contains("@")) {
let split = requestedEvent.split(separator: "@")
eventName = String(split[0])
eventDevice = String(split[1])
}
guard EventType.allCases.firstIndex(where: { $0.name == eventName}) ?? -1 >= 0 else {
NSLog("%@ is not a supported EventType", eventName)
return
}
let listenerCount = listeners[eventName] ?? 0
if listenerCount > 0 {
listeners[eventName] = listenerCount - 1
if let deviceId = eventDevice {
onRemoveListener(eventName, deviceId: deviceId)
}
}
}
func onAddListener(_ eventName: String, deviceId: String) {
if var connection = connections[deviceId] {
connection.dataReceivedDelegate = self;
} else {
NSLog("Device %@ is not currently connected, unable to set delegate", deviceId)
}
}
func onRemoveListener(_ eventName: String, deviceId: String) {
if var connection = connections[deviceId] {
connection.dataReceivedDelegate = nil;
} else {
NSLog("Device %@ is not currently connected, unable to remove delegate", deviceId)
}
}
func onRemoveAllListeners(_ eventName: String, deviceId: String) {
if var connection = connections[deviceId] {
connection.dataReceivedDelegate = nil;
} else {
NSLog("Device %@ is not currently connected, unable to remove delegate", deviceId)
}
}
}
// MARK: BluetoothReceivedDelegate implementation
/**
* Extension implementing the DataReceivedDelegate
*
* Responsible for accepting data from the device (when a listener has been requested) and passing
* such data through to React Native.
*/
extension RNBluetoothClassic : DataReceivedDelegate {
/**
* Receives data from the device, this data should already be:
* - Encoded correctly
* - Bundled correctly
* - essentiatlly everything ready to go to React Native
*
* Once there, Javascript will be responsible for parsing the data.
*/
func onReceivedData(fromDevice: EAAccessory, receivedData: String) {
// Need to gaurd against whether to send this information. But for now
// we'll just send it anyhow.
NSLog("(RNBluetoothClassic:onReceiveData) Sending DEVICE_READ with data: %@", receivedData)
let bluetoothMessage:BluetoothMessage = BluetoothMessage<String>(
fromDevice: NativeDevice(accessory: fromDevice), data: receivedData)
sendEvent("\(EventType.DEVICE_READ.name)@\(fromDevice.serialNumber)", body: bluetoothMessage.map())
}
}