diff --git a/ios/Sources/BluetoothLe/Device.swift b/ios/Sources/BluetoothLe/Device.swift index 5f890b6..f14a069 100644 --- a/ios/Sources/BluetoothLe/Device.swift +++ b/ios/Sources/BluetoothLe/Device.swift @@ -6,8 +6,8 @@ class Device: NSObject, CBPeripheralDelegate { typealias Callback = (_ success: Bool, _ value: String) -> Void private var peripheral: CBPeripheral! - private let callbackMap = ThreadSafeDictionary() - private let timeoutMap = ThreadSafeDictionary() + private var callbackMap = [String: Callback]() + private var timeoutMap = [String: DispatchWorkItem]() private var servicesCount = 0 private var servicesDiscovered = 0 private var characteristicsCount = 0 @@ -52,14 +52,10 @@ class Device: NSObject, CBPeripheralDelegate { _ skipDescriptorDiscovery: Bool, _ callback: @escaping Callback ) { - // Delegates run on the main queue; this is called from the bridge thread. - // Sync to main so writes are visible before any delegate fires. - DispatchQueue.main.sync { - let key = "connect" - self.skipDescriptorDiscovery = skipDescriptorDiscovery - self.callbackMap[key] = callback - self.setTimeout(key, "Connection timeout", connectionTimeout) - } + let key = "connect" + self.skipDescriptorDiscovery = skipDescriptorDiscovery + self.callbackMap[key] = callback + self.setTimeout(key, "Connection timeout", connectionTimeout) } func peripheral( @@ -69,6 +65,8 @@ class Device: NSObject, CBPeripheralDelegate { log("didDiscoverServices", peripheral.services?.count ?? -1) if let error = error { log("Error", error.localizedDescription) + self.reject("connect", "Service discovery failed: \(error.localizedDescription)") + self.reject("discoverServices", "Service discovery failed: \(error.localizedDescription)") return } self.servicesCount = peripheral.services?.count ?? 0 @@ -129,6 +127,12 @@ class Device: NSObject, CBPeripheralDelegate { } } + func cancelConnectTimeout() { + let key = "connect" + self.callbackMap.removeValue(forKey: key) + self.timeoutMap.removeValue(forKey: key)?.cancel() + } + func getServices() -> [CBService] { return self.peripheral.services ?? [] } @@ -439,6 +443,7 @@ class Device: NSObject, CBPeripheralDelegate { _ message: String, _ timeout: Double ) { + self.timeoutMap.removeValue(forKey: key)?.cancel() let workItem = DispatchWorkItem { log("setTimeout", self.servicesDiscovered, self.servicesCount, diff --git a/ios/Sources/BluetoothLe/DeviceManager.swift b/ios/Sources/BluetoothLe/DeviceManager.swift index a0e7fe2..a9e7f15 100644 --- a/ios/Sources/BluetoothLe/DeviceManager.swift +++ b/ios/Sources/BluetoothLe/DeviceManager.swift @@ -11,20 +11,20 @@ enum DeviceListMode { class DeviceManager: NSObject, CBCentralManagerDelegate { typealias Callback = (_ success: Bool, _ message: String) -> Void typealias StateReceiver = (_ enabled: Bool) -> Void - typealias ScanResultCallback = (_ device: Device, _ advertisementData: [String: Any], _ rssi: NSNumber) -> Void + typealias ScanResultCallback = (_ peripheral: CBPeripheral, _ advertisementData: [String: Any], _ rssi: NSNumber) -> Void private var centralManager: CBCentralManager! private let viewController: UIViewController? private var displayStrings: [String: String]! - private let callbackMap = ThreadSafeDictionary() + private var callbackMap = [String: Callback]() private var scanResultCallback: ScanResultCallback? private var stateReceiver: StateReceiver? - private let timeoutMap = ThreadSafeDictionary() + private var timeoutMap = [String: DispatchWorkItem]() private var stopScanWorkItem: DispatchWorkItem? private var alertController: UIAlertController? private var deviceListView: DeviceListView? private var popoverController: UIPopoverPresentationController? - private let discoveredDevices = ThreadSafeDictionary() + private var discoveredPeripherals = [String: CBPeripheral]() private var deviceNameFilter: String? private var deviceNamePrefixFilter: String? private var deviceListMode: DeviceListMode = .none @@ -73,9 +73,6 @@ class DeviceManager: NSObject, CBCentralManagerDelegate { return self.centralManager.state == CBManagerState.poweredOn } - // stateReceiver is written on the bridge thread and read on the main queue - // (via centralManagerDidUpdateState). Same threading issue as startScanning - // below; not fixed as BLE state transitions are too slow for the race to manifest. func registerStateReceiver( _ stateReceiver: @escaping StateReceiver) { self.stateReceiver = stateReceiver } @@ -104,19 +101,14 @@ class DeviceManager: NSObject, CBCentralManagerDelegate { self.callbackMap["startScanning"] = callback if self.centralManager.isScanning == false { - self.discoveredDevices.removeAll() - // These are read from centralManager(_:didDiscover:) on the main queue. - // Delegates run on the main queue; this is called from the bridge thread. - // Sync to main so writes are visible before any delegate fires. - DispatchQueue.main.sync { - self.scanResultCallback = scanResultCallback - self.deviceListMode = deviceListMode - self.allowDuplicates = allowDuplicates - self.deviceNameFilter = name - self.deviceNamePrefixFilter = namePrefix - self.manufacturerDataFilters = manufacturerDataFilters - self.serviceDataFilters = serviceDataFilters - } + self.discoveredPeripherals.removeAll() + self.scanResultCallback = scanResultCallback + self.deviceListMode = deviceListMode + self.allowDuplicates = allowDuplicates + self.deviceNameFilter = name + self.deviceNamePrefixFilter = namePrefix + self.manufacturerDataFilters = manufacturerDataFilters + self.serviceDataFilters = serviceDataFilters if deviceListMode != .none { self.showDeviceList() @@ -152,13 +144,13 @@ class DeviceManager: NSObject, CBCentralManagerDelegate { guard let self = self else { return } switch self.deviceListMode { case .alert: - if self.discoveredDevices.count == 0 { + if self.discoveredPeripherals.count == 0 { self.alertController?.title = self.displayStrings["noDeviceFound"] } else { self.alertController?.title = self.displayStrings["availableDevices"] } case .list: - if self.discoveredDevices.count == 0 { + if self.discoveredPeripherals.count == 0 { self.deviceListView?.setTitle(self.displayStrings["noDeviceFound"]) } else { self.deviceListView?.setTitle(self.displayStrings["availableDevices"]) @@ -189,36 +181,31 @@ class DeviceManager: NSObject, CBCentralManagerDelegate { guard ScanFilterUtils.passesServiceDataFilter(advertisementData, filters: self.serviceDataFilters) else { return } let deviceId = peripheral.identifier.uuidString - let result = self.discoveredDevices.getOrInsert( - key: deviceId, - create: { Device(peripheral) }, - update: { $0.updatePeripheral(peripheral) } - ) - let device = result.value - let isNew = result.wasInserted + let isNew = self.discoveredPeripherals[deviceId] == nil + self.discoveredPeripherals[deviceId] = peripheral if isNew || self.allowDuplicates { - log("New device found: ", device.getName() ?? "Unknown") + log("New device found: ", peripheral.name ?? "Unknown") switch deviceListMode { case .none: if let callback = self.scanResultCallback { - callback(device, advertisementData, RSSI) + callback(peripheral, advertisementData, RSSI) } case .alert: DispatchQueue.main.async { [weak self] in - self?.alertController?.addAction(UIAlertAction(title: device.getName() ?? "Unknown", style: UIAlertAction.Style.default, handler: { (_) in + self?.alertController?.addAction(UIAlertAction(title: peripheral.name ?? "Unknown", style: UIAlertAction.Style.default, handler: { (_) in log("Selected device") self?.stopScan() - self?.resolve("startScanning", device.getId()) + self?.resolve("startScanning", deviceId) })) } case .list: DispatchQueue.main.async { [weak self] in - self?.deviceListView?.addItem(device.getName() ?? "Unknown", action: { + self?.deviceListView?.addItem(peripheral.name ?? "Unknown", action: { log("Selected device") self?.stopScan() - self?.resolve("startScanning", device.getId()) + self?.resolve("startScanning", deviceId) }) } } @@ -364,8 +351,8 @@ class DeviceManager: NSObject, CBCentralManagerDelegate { self.resolve(key, "Successfully disconnected.") } - func getDevice(_ deviceId: String) -> Device? { - return self.discoveredDevices[deviceId] + func getPeripheral(_ deviceId: String) -> CBPeripheral? { + return self.discoveredPeripherals[deviceId] } private func passesNameFilter(peripheralName: String?) -> Bool { @@ -400,6 +387,7 @@ class DeviceManager: NSObject, CBCentralManagerDelegate { _ message: String, _ timeout: Double ) { + self.timeoutMap.removeValue(forKey: key)?.cancel() let workItem = DispatchWorkItem { self.reject(key, message) } @@ -413,6 +401,7 @@ class DeviceManager: NSObject, CBCentralManagerDelegate { _ device: Device, _ connectionTimeout: Double ) { + self.timeoutMap.removeValue(forKey: connectionKey)?.cancel() let workItem = DispatchWorkItem { self.cancelConnect(device) self.reject(connectionKey, message) diff --git a/ios/Sources/BluetoothLe/Plugin.swift b/ios/Sources/BluetoothLe/Plugin.swift index 68223e1..652a462 100644 --- a/ios/Sources/BluetoothLe/Plugin.swift +++ b/ios/Sources/BluetoothLe/Plugin.swift @@ -52,7 +52,7 @@ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin { typealias BleCharacteristic = [String: Any] typealias BleDescriptor = [String: Any] private var deviceManager: DeviceManager? - private let deviceMap = ThreadSafeDictionary() + private var deviceMap = [String: Device]() private var displayStrings = [String: String]() override public func load() { @@ -60,19 +60,23 @@ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin { } @objc func initialize(_ call: CAPPluginCall) { - self.deviceManager = DeviceManager(self.bridge?.viewController, self.displayStrings, {(success, message) in - if success { - call.resolve() - } else { - call.reject(message) - } - }) + DispatchQueue.main.async { + self.deviceManager = DeviceManager(self.bridge?.viewController, self.displayStrings, {(success, message) in + if success { + call.resolve() + } else { + call.reject(message) + } + }) + } } @objc func isEnabled(_ call: CAPPluginCall) { - guard let deviceManager = self.getDeviceManager(call) else { return } - let enabled: Bool = deviceManager.isEnabled() - call.resolve(["value": enabled]) + DispatchQueue.main.async { + guard let deviceManager = self.getDeviceManager(call) else { return } + let enabled: Bool = deviceManager.isEnabled() + call.resolve(["value": enabled]) + } } @objc func requestEnable(_ call: CAPPluginCall) { @@ -88,17 +92,21 @@ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin { } @objc func startEnabledNotifications(_ call: CAPPluginCall) { - guard let deviceManager = self.getDeviceManager(call) else { return } - deviceManager.registerStateReceiver({(enabled) in - self.notifyListeners("onEnabledChanged", data: ["value": enabled]) - }) - call.resolve() + DispatchQueue.main.async { + guard let deviceManager = self.getDeviceManager(call) else { return } + deviceManager.registerStateReceiver({(enabled) in + self.notifyListeners("onEnabledChanged", data: ["value": enabled]) + }) + call.resolve() + } } @objc func stopEnabledNotifications(_ call: CAPPluginCall) { - guard let deviceManager = self.getDeviceManager(call) else { return } - deviceManager.unregisterStateReceiver() - call.resolve() + DispatchQueue.main.async { + guard let deviceManager = self.getDeviceManager(call) else { return } + deviceManager.unregisterStateReceiver() + call.resolve() + } } @objc func isLocationEnabled(_ call: CAPPluginCall) { @@ -133,174 +141,169 @@ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin { } @objc func setDisplayStrings(_ call: CAPPluginCall) { - for key in ["noDeviceFound", "availableDevices", "scanning", "cancel"] { - if let value = call.getString(key) { - self.displayStrings[key] = value + DispatchQueue.main.async { + for key in ["noDeviceFound", "availableDevices", "scanning", "cancel"] { + if let value = call.getString(key) { + self.displayStrings[key] = value + } } + call.resolve() } - call.resolve() } @objc func requestDevice(_ call: CAPPluginCall) { - guard let deviceManager = self.getDeviceManager(call) else { return } - deviceManager.setDisplayStrings(self.displayStrings) - - let serviceUUIDs = self.getServiceUUIDs(call) - let name = call.getString("name") - let namePrefix = call.getString("namePrefix") - let manufacturerDataFilters = self.getManufacturerDataFilters(call) - let serviceDataFilters = self.getServiceDataFilters(call) - - let displayModeString = (call.getString("displayMode") ?? "alert").lowercased() - guard ["alert", "list"].contains(displayModeString) else { - call.reject("Invalid displayMode '\(call.getString("displayMode") ?? "")'. Use 'alert' or 'list'.") - return - } - let deviceListMode: DeviceListMode = displayModeString == "list" ? .list : .alert - - deviceManager.startScanning( - serviceUUIDs, - name, - namePrefix, - manufacturerDataFilters, - serviceDataFilters, - false, - deviceListMode, - 30, - {(success, message) in - if success { - guard let device = deviceManager.getDevice(message) else { - call.reject("Device not found.") - return + DispatchQueue.main.async { + guard let deviceManager = self.getDeviceManager(call) else { return } + deviceManager.setDisplayStrings(self.displayStrings) + + let serviceUUIDs = self.getServiceUUIDs(call) + let name = call.getString("name") + let namePrefix = call.getString("namePrefix") + let manufacturerDataFilters = self.getManufacturerDataFilters(call) + let serviceDataFilters = self.getServiceDataFilters(call) + + let displayModeString = (call.getString("displayMode") ?? "alert").lowercased() + guard ["alert", "list"].contains(displayModeString) else { + call.reject("Invalid displayMode '\(call.getString("displayMode") ?? "")'. Use 'alert' or 'list'.") + return + } + let deviceListMode: DeviceListMode = displayModeString == "list" ? .list : .alert + + deviceManager.startScanning( + serviceUUIDs, + name, + namePrefix, + manufacturerDataFilters, + serviceDataFilters, + false, + deviceListMode, + 30, + {(success, message) in + if success { + guard let peripheral = deviceManager.getPeripheral(message) else { + call.reject("Device not found.") + return + } + let storedDevice = self.getOrCreateDevice(peripheral) + let bleDevice: BleDevice = self.getBleDevice(storedDevice) + call.resolve(bleDevice) + } else { + call.reject(message) } - let storedDevice = self.deviceMap.getOrInsert( - key: device.getId(), - create: { device }, - update: { $0.updatePeripheral(device.getPeripheral()) } - ).value - let bleDevice: BleDevice = self.getBleDevice(storedDevice) - call.resolve(bleDevice) - } else { - call.reject(message) - } - }, - { (_, _, _) in } - ) + }, + { (_, _, _) in } + ) + } } @objc func requestLEScan(_ call: CAPPluginCall) { - guard let deviceManager = self.getDeviceManager(call) else { return } - - let serviceUUIDs = self.getServiceUUIDs(call) - let name = call.getString("name") - let namePrefix = call.getString("namePrefix") - let allowDuplicates = call.getBool("allowDuplicates", false) - let manufacturerDataFilters = self.getManufacturerDataFilters(call) - let serviceDataFilters = self.getServiceDataFilters(call) - - deviceManager.startScanning( - serviceUUIDs, - name, - namePrefix, - manufacturerDataFilters, - serviceDataFilters, - allowDuplicates, - .none, - nil, - { (success, message) in - if success { - call.resolve() - } else { - call.reject(message) + DispatchQueue.main.async { + guard let deviceManager = self.getDeviceManager(call) else { return } + + let serviceUUIDs = self.getServiceUUIDs(call) + let name = call.getString("name") + let namePrefix = call.getString("namePrefix") + let allowDuplicates = call.getBool("allowDuplicates", false) + let manufacturerDataFilters = self.getManufacturerDataFilters(call) + let serviceDataFilters = self.getServiceDataFilters(call) + + deviceManager.startScanning( + serviceUUIDs, + name, + namePrefix, + manufacturerDataFilters, + serviceDataFilters, + allowDuplicates, + .none, + nil, + { (success, message) in + if success { + call.resolve() + } else { + call.reject(message) + } + }, { (peripheral, advertisementData, rssi) in + let storedDevice = self.getOrCreateDevice(peripheral) + let data = self.getScanResult(storedDevice, advertisementData, rssi) + self.notifyListeners("onScanResult", data: data) } - }, { (device, advertisementData, rssi) in - let storedDevice = self.deviceMap.getOrInsert( - key: device.getId(), - create: { device }, - update: { $0.updatePeripheral(device.getPeripheral()) } - ).value - let data = self.getScanResult(storedDevice, advertisementData, rssi) - self.notifyListeners("onScanResult", data: data) - } - ) + ) + } } @objc func stopLEScan(_ call: CAPPluginCall) { - guard let deviceManager = self.getDeviceManager(call) else { return } - deviceManager.stopScan() - call.resolve() + DispatchQueue.main.async { + guard let deviceManager = self.getDeviceManager(call) else { return } + deviceManager.stopScan() + call.resolve() + } } @objc func getDevices(_ call: CAPPluginCall) { - guard let deviceManager = self.getDeviceManager(call) else { return } - guard let deviceIds = call.getArray("deviceIds", String.self) else { - call.reject("deviceIds must be provided") - return + DispatchQueue.main.async { + guard let deviceManager = self.getDeviceManager(call) else { return } + guard let deviceIds = call.getArray("deviceIds", String.self) else { + call.reject("deviceIds must be provided") + return + } + let deviceUUIDs: [UUID] = deviceIds.compactMap({ deviceId in + return UUID(uuidString: deviceId) + }) + let peripherals = deviceManager.getDevices(deviceUUIDs) + let bleDevices: [BleDevice] = peripherals.map({peripheral in + let device = self.getOrCreateDevice(peripheral) + return self.getBleDevice(device) + }) + call.resolve(["devices": bleDevices]) } - let deviceUUIDs: [UUID] = deviceIds.compactMap({ deviceId in - return UUID(uuidString: deviceId) - }) - let peripherals = deviceManager.getDevices(deviceUUIDs) - let bleDevices: [BleDevice] = peripherals.map({peripheral in - let deviceId = peripheral.identifier.uuidString - let device = self.deviceMap.getOrInsert( - key: deviceId, - create: { Device(peripheral) }, - update: { $0.updatePeripheral(peripheral) } - ).value - return self.getBleDevice(device) - }) - call.resolve(["devices": bleDevices]) } @objc func getConnectedDevices(_ call: CAPPluginCall) { - guard let deviceManager = self.getDeviceManager(call) else { return } - guard let services = call.getArray("services", String.self) else { - call.reject("services must be provided") - return + DispatchQueue.main.async { + guard let deviceManager = self.getDeviceManager(call) else { return } + guard let services = call.getArray("services", String.self) else { + call.reject("services must be provided") + return + } + let serviceUUIDs: [CBUUID] = services.compactMap({ service in + return CBUUID(string: service) + }) + let peripherals = deviceManager.getConnectedDevices(serviceUUIDs) + let bleDevices: [BleDevice] = peripherals.map({peripheral in + let device = self.getOrCreateDevice(peripheral) + return self.getBleDevice(device) + }) + call.resolve(["devices": bleDevices]) } - let serviceUUIDs: [CBUUID] = services.compactMap({ service in - return CBUUID(string: service) - }) - let peripherals = deviceManager.getConnectedDevices(serviceUUIDs) - let bleDevices: [BleDevice] = peripherals.map({peripheral in - let deviceId = peripheral.identifier.uuidString - let device = self.deviceMap.getOrInsert( - key: deviceId, - create: { Device(peripheral) }, - update: { $0.updatePeripheral(peripheral) } - ).value - return self.getBleDevice(device) - }) - call.resolve(["devices": bleDevices]) } @objc func connect(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call, checkConnection: false) else { return } - let timeout = self.getTimeout(call, defaultTimeout: CONNECTION_TIMEOUT) - let skipDescriptorDiscovery = call.getBool("skipDescriptorDiscovery") ?? false - device.setOnConnected(timeout, skipDescriptorDiscovery, {(success, message) in - if success { - // only resolve after service discovery - call.resolve() - } else { - self.deviceManager?.cancelConnect(device) - call.reject(message) - } - }) - self.deviceManager?.setOnDisconnected(device, {(_, _) in - let key = "disconnected|\(device.getId())" - self.notifyListeners(key, data: nil) - }) - self.deviceManager?.connect(device, timeout, {(success, message) in - if success { - log("Connected to peripheral. Waiting for service discovery.") - } else { - call.reject(message) - } - }) - + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call, checkConnection: false) else { return } + let timeout = self.getTimeout(call, defaultTimeout: CONNECTION_TIMEOUT) + let skipDescriptorDiscovery = call.getBool("skipDescriptorDiscovery") ?? false + device.setOnConnected(timeout, skipDescriptorDiscovery, {(success, message) in + if success { + call.resolve() + } else { + self.deviceManager?.cancelConnect(device) + call.reject(message) + } + }) + self.deviceManager?.setOnDisconnected(device, {(_, _) in + let key = "disconnected|\(device.getId())" + self.notifyListeners(key, data: nil) + }) + self.deviceManager?.connect(device, timeout, {(success, message) in + if success { + log("Connected to peripheral. Waiting for service discovery.") + } else { + device.cancelConnectTimeout() + call.reject(message) + } + }) + } } @objc func createBond(_ call: CAPPluginCall) { @@ -316,44 +319,48 @@ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin { } @objc func disconnect(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call, checkConnection: false) else { return } - let timeout = self.getTimeout(call) - self.deviceManager?.disconnect(device, timeout, {(success, message) in - if success { - call.resolve() - } else { - call.reject(message) - } - }) + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call, checkConnection: false) else { return } + let timeout = self.getTimeout(call) + self.deviceManager?.disconnect(device, timeout, {(success, message) in + if success { + call.resolve() + } else { + call.reject(message) + } + }) + } } @objc func getServices(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - let services = device.getServices() - var bleServices = [BleService]() - for service in services { - var bleCharacteristics = [BleCharacteristic]() - for characteristic in service.characteristics ?? [] { - var bleDescriptors = [BleDescriptor]() - for descriptor in characteristic.descriptors ?? [] { - bleDescriptors.append([ - "uuid": cbuuidToString(descriptor.uuid) + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + let services = device.getServices() + var bleServices = [BleService]() + for service in services { + var bleCharacteristics = [BleCharacteristic]() + for characteristic in service.characteristics ?? [] { + var bleDescriptors = [BleDescriptor]() + for descriptor in characteristic.descriptors ?? [] { + bleDescriptors.append([ + "uuid": cbuuidToString(descriptor.uuid) + ]) + } + bleCharacteristics.append([ + "uuid": cbuuidToString(characteristic.uuid), + "properties": self.getProperties(characteristic), + "descriptors": bleDescriptors ]) } - bleCharacteristics.append([ - "uuid": cbuuidToString(characteristic.uuid), - "properties": getProperties(characteristic), - "descriptors": bleDescriptors + bleServices.append([ + "uuid": cbuuidToString(service.uuid), + "characteristics": bleCharacteristics ]) } - bleServices.append([ - "uuid": cbuuidToString(service.uuid), - "characteristics": bleCharacteristics - ]) + call.resolve(["services": bleServices]) } - call.resolve(["services": bleServices]) } private func getProperties(_ characteristic: CBCharacteristic) -> [String: Bool] { @@ -372,24 +379,28 @@ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin { } @objc func discoverServices(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - let timeout = self.getTimeout(call) - device.discoverServices(timeout, {(success, value) in - if success { - call.resolve() - } else { - call.reject(value) - } - }) + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + let timeout = self.getTimeout(call) + device.discoverServices(timeout, {(success, value) in + if success { + call.resolve() + } else { + call.reject(value) + } + }) + } } @objc func getMtu(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - call.resolve([ - "value": device.getMtu() - ]) + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + call.resolve([ + "value": device.getMtu() + ]) + } } @objc func requestConnectionPriority(_ call: CAPPluginCall) { @@ -397,94 +408,29 @@ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin { } @objc func readRssi(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - let timeout = self.getTimeout(call) - device.readRssi(timeout, {(success, value) in - if success { - call.resolve([ - "value": value - ]) - } else { - call.reject(value) - } - }) - } - - @objc func read(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - guard let characteristic = self.getCharacteristic(call) else { return } - let timeout = self.getTimeout(call) - device.read(characteristic.0, characteristic.1, timeout, {(success, value) in - if success { - call.resolve([ - "value": value - ]) - } else { - call.reject(value) - } - }) - } - - @objc func write(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - guard let characteristic = self.getCharacteristic(call) else { return } - guard let value = call.getString("value") else { - call.reject("value must be provided") - return - } - let writeType = CBCharacteristicWriteType.withResponse - let timeout = self.getTimeout(call) - device.write( - characteristic.0, - characteristic.1, - value, - writeType, - timeout, {(success, value) in + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + let timeout = self.getTimeout(call) + device.readRssi(timeout, {(success, value) in if success { - call.resolve() + call.resolve([ + "value": value + ]) } else { call.reject(value) } }) - } - - @objc func writeWithoutResponse(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - guard let characteristic = self.getCharacteristic(call) else { return } - guard let value = call.getString("value") else { - call.reject("value must be provided") - return } - let writeType = CBCharacteristicWriteType.withoutResponse - let timeout = self.getTimeout(call) - device.write( - characteristic.0, - characteristic.1, - value, - writeType, - timeout, {(success, value) in - if success { - call.resolve() - } else { - call.reject(value) - } - }) } - @objc func readDescriptor(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - guard let descriptor = self.getDescriptor(call) else { return } - let timeout = self.getTimeout(call) - device.readDescriptor( - descriptor.0, - descriptor.1, - descriptor.2, - timeout, {(success, value) in + @objc func read(_ call: CAPPluginCall) { + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + guard let characteristic = self.getCharacteristic(call) else { return } + let timeout = self.getTimeout(call) + device.read(characteristic.0, characteristic.1, timeout, {(success, value) in if success { call.resolve([ "value": value @@ -493,69 +439,150 @@ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin { call.reject(value) } }) + } + } + + @objc func write(_ call: CAPPluginCall) { + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + guard let characteristic = self.getCharacteristic(call) else { return } + guard let value = call.getString("value") else { + call.reject("value must be provided") + return + } + let writeType = CBCharacteristicWriteType.withResponse + let timeout = self.getTimeout(call) + device.write( + characteristic.0, + characteristic.1, + value, + writeType, + timeout, {(success, value) in + if success { + call.resolve() + } else { + call.reject(value) + } + }) + } + } + + @objc func writeWithoutResponse(_ call: CAPPluginCall) { + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + guard let characteristic = self.getCharacteristic(call) else { return } + guard let value = call.getString("value") else { + call.reject("value must be provided") + return + } + let writeType = CBCharacteristicWriteType.withoutResponse + let timeout = self.getTimeout(call) + device.write( + characteristic.0, + characteristic.1, + value, + writeType, + timeout, {(success, value) in + if success { + call.resolve() + } else { + call.reject(value) + } + }) + } + } + + @objc func readDescriptor(_ call: CAPPluginCall) { + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + guard let descriptor = self.getDescriptor(call) else { return } + let timeout = self.getTimeout(call) + device.readDescriptor( + descriptor.0, + descriptor.1, + descriptor.2, + timeout, {(success, value) in + if success { + call.resolve([ + "value": value + ]) + } else { + call.reject(value) + } + }) + } } @objc func writeDescriptor(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - guard let descriptor = self.getDescriptor(call) else { return } - guard let value = call.getString("value") else { - call.reject("value must be provided") - return + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + guard let descriptor = self.getDescriptor(call) else { return } + guard let value = call.getString("value") else { + call.reject("value must be provided") + return + } + let timeout = self.getTimeout(call) + device.writeDescriptor( + descriptor.0, + descriptor.1, + descriptor.2, + value, + timeout, {(success, value) in + if success { + call.resolve() + } else { + call.reject(value) + } + }) } - let timeout = self.getTimeout(call) - device.writeDescriptor( - descriptor.0, - descriptor.1, - descriptor.2, - value, - timeout, {(success, value) in - if success { - call.resolve() - } else { - call.reject(value) - } - }) } @objc func startNotifications(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - guard let characteristic = self.getCharacteristic(call) else { return } - let timeout = self.getTimeout(call) - device.setNotifications( - characteristic.0, - characteristic.1, - true, {(_, value) in - let key = "notification|\(device.getId())|\(characteristic.0.uuidString.lowercased())|\(characteristic.1.uuidString.lowercased())" - self.notifyListeners(key, data: ["value": value]) - }, - timeout, {(success, value) in - if success { - call.resolve() - } else { - call.reject(value) - } - }) + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + guard let characteristic = self.getCharacteristic(call) else { return } + let timeout = self.getTimeout(call) + device.setNotifications( + characteristic.0, + characteristic.1, + true, {(_, value) in + let key = "notification|\(device.getId())|\(characteristic.0.uuidString.lowercased())|\(characteristic.1.uuidString.lowercased())" + self.notifyListeners(key, data: ["value": value]) + }, + timeout, {(success, value) in + if success { + call.resolve() + } else { + call.reject(value) + } + }) + } } @objc func stopNotifications(_ call: CAPPluginCall) { - guard self.getDeviceManager(call) != nil else { return } - guard let device = self.getDevice(call) else { return } - guard let characteristic = self.getCharacteristic(call) else { return } - let timeout = self.getTimeout(call) - device.setNotifications( - characteristic.0, - characteristic.1, - false, - nil, - timeout, {(success, value) in - if success { - call.resolve() - } else { - call.reject(value) - } - }) + DispatchQueue.main.async { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + guard let characteristic = self.getCharacteristic(call) else { return } + let timeout = self.getTimeout(call) + device.setNotifications( + characteristic.0, + characteristic.1, + false, + nil, + timeout, {(success, value) in + if success { + call.resolve() + } else { + call.reject(value) + } + }) + } } private func getDisplayStrings() -> [String: String] { @@ -594,7 +621,6 @@ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin { for index in 0.. Device { + let key = peripheral.identifier.uuidString + if let existing = self.deviceMap[key] { + existing.updatePeripheral(peripheral) + return existing + } + let device = Device(peripheral) + self.deviceMap[key] = device + return device + } + private func getBleDevice(_ device: Device) -> BleDevice { var bleDevice = [ "deviceId": device.getId() diff --git a/ios/Sources/BluetoothLe/ThreadSafeDictionary.swift b/ios/Sources/BluetoothLe/ThreadSafeDictionary.swift deleted file mode 100644 index f4ab38c..0000000 --- a/ios/Sources/BluetoothLe/ThreadSafeDictionary.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation - -class ThreadSafeDictionary { - private var dictionary: [K: T] = [:] - private let queue = DispatchQueue(label: "threadSafeDictionaryQueue", attributes: .concurrent) - - subscript(key: K) -> T? { - get { - return queue.sync { dictionary[key] } - } - set { - queue.sync(flags: .barrier) { self.dictionary[key] = newValue } - } - } - - func removeValue(forKey key: K) -> T? { - return queue.sync(flags: .barrier) { - return dictionary.removeValue(forKey: key) - } - } - - var count: Int { - return queue.sync { dictionary.count } - } - - func removeAll() { - queue.sync(flags: .barrier) { - self.dictionary.removeAll() - } - } - - /// Atomically gets existing value or inserts and returns new value - /// The create closure is only called if the key doesn't exist - /// Returns tuple of (value, wasInserted) where wasInserted indicates if a new value was created - func getOrInsert(key: K, create: () -> T) -> (value: T, wasInserted: Bool) { - return queue.sync(flags: .barrier) { - if let existing = dictionary[key] { - return (existing, false) - } - let newValue = create() - dictionary[key] = newValue - return (newValue, true) - } - } - - /// Atomically gets existing value (calling update on it) or inserts new value - /// The create closure is only called if the key doesn't exist - /// The update closure is called on existing values before returning - /// Returns tuple of (value, wasInserted) where wasInserted indicates if a new value was created - func getOrInsert(key: K, create: () -> T, update: (T) -> Void) -> (value: T, wasInserted: Bool) { - return queue.sync(flags: .barrier) { - if let existing = dictionary[key] { - update(existing) - return (existing, false) - } - let newValue = create() - dictionary[key] = newValue - return (newValue, true) - } - } -}