UNPKG

@nativescript-community/ble

Version:

Connect to and interact with Bluetooth LE peripherals.

1,060 lines (1,059 loc) 76.1 kB
import { Trace } from '@nativescript/core'; import { BluetoothCommon, BluetoothError, CLog, CLogTypes, bluetoothEnabled, prepareArgs, shortenUuidIfAssignedNumber } from './index.common'; export { BluetoothError, BleTraceCategory, CallbackType, MatchMode, MatchNum, ScanMode } from './index.common'; function nativeEncoding(encoding) { switch (encoding) { case 'utf-8': return NSUTF8StringEncoding; case 'latin2': case 'iso-8859-2': return NSISOLatin2StringEncoding; case 'shift-jis': return NSShiftJISStringEncoding; case 'iso-2022-jp': return NSISO2022JPStringEncoding; case 'euc-jp': return NSJapaneseEUCStringEncoding; case 'windows-1250': return NSWindowsCP1250StringEncoding; case 'windows-1251': return NSWindowsCP1251StringEncoding; case 'windows-1252': return NSWindowsCP1252StringEncoding; case 'windows-1253': return NSWindowsCP1253StringEncoding; case 'windows-1254': return NSWindowsCP1254StringEncoding; case 'utf-16be': return NSUTF16BigEndianStringEncoding; case 'utf-16le': return NSUTF16LittleEndianStringEncoding; default: case 'iso-8859-1': case 'latin1': return NSISOLatin1StringEncoding; } } function valueToNSData(value, encoding = 'iso-8859-1') { if (value instanceof NSData) { return value; } else if (value instanceof ArrayBuffer) { // for ArrayBuffer to NSData return NSData.dataWithData(value); } else if (value.buffer) { // typed array return NSData.dataWithData(value.buffer); } else if (Array.isArray(value)) { return NSData.dataWithData(new Uint8Array(value).buffer); } const type = typeof value; if (type === 'string') { return NSString.stringWithString(value).dataUsingEncoding(nativeEncoding(encoding)); } else if (type === 'number') { return NSData.dataWithData(new Uint8Array([value]).buffer); } return null; } function valueToString(value) { if (value instanceof NSData) { const data = new Uint8Array(interop.bufferFromData(value)); const result = []; data.forEach((v, i) => (result[i] = v)); return result; } return value; } import { iOSNativeHelper } from '@nativescript/core/utils/native-helper'; const FIXED_IOS_MTU = 185; var CBPeripheralDelegateImpl = /** @class */ (function (_super) { __extends(CBPeripheralDelegateImpl, _super); function CBPeripheralDelegateImpl() { return _super !== null && _super.apply(this, arguments) || this; } CBPeripheralDelegateImpl.prototype.addSubDelegate = function (delegate) { var index = this.subDelegates.indexOf(delegate); if (index === -1) { this.subDelegates.push(delegate); } }; CBPeripheralDelegateImpl.prototype.removeSubDelegate = function (delegate) { var index = this.subDelegates.indexOf(delegate); if (index !== -1) { this.subDelegates.splice(index, 1); } }; CBPeripheralDelegateImpl.new = function () { return _super.new.call(this); }; CBPeripheralDelegateImpl.prototype.initWithOwner = function (owner) { this._owner = owner; this.subDelegates = []; this.onNotifyCallbacks = {}; if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.initWithOwner: ".concat(owner.get())); } return this; }; /** * Invoked when you discover the peripheral’s available services. * This method is invoked when your app calls the discoverServices(_:) method. * If the services of the peripheral are successfully discovered, you can access them through the peripheral’s services property. * If successful, the error parameter is nil. * If unsuccessful, the error parameter returns the cause of the failure. * @param peripheral [CBPeripheral] - The peripheral that the services belong to. * @param error [NSError] - If an error occurred, the cause of the failure. */ CBPeripheralDelegateImpl.prototype.peripheralDidDiscoverServices = function (peripheral, error) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidDiscoverServices ---- peripheral: ".concat(peripheral, ", ").concat(error, ", ").concat(this)); } this.subDelegates.forEach(function (d) { if (d.peripheralDidDiscoverServices) { d.peripheralDidDiscoverServices(peripheral, error); } }); }; /** * Invoked when you discover the included services of a specified service. * @param peripheral [CBPeripheral] - The peripheral providing this information. * @param service [CBService] - The CBService object containing the included service. * @param error [NSError] - If an error occurred, the cause of the failure. */ CBPeripheralDelegateImpl.prototype.peripheralDidDiscoverIncludedServicesForServiceError = function (peripheral, service, error) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidDiscoverIncludedServicesForServiceError ---- peripheral: ".concat(peripheral, ", service: ").concat(service, ", error: ").concat(error)); } this.subDelegates.forEach(function (d) { if (d.peripheralDidDiscoverIncludedServicesForServiceError) { d.peripheralDidDiscoverIncludedServicesForServiceError(peripheral, service, error); } }); }; /** * Invoked when you discover the characteristics of a specified service. * @param peripheral [CBPeripheral] - The peripheral providing this information. * @param service [CBService] - The CBService object containing the included service. * @param error [NSError] - If an error occurred, the cause of the failure. */ CBPeripheralDelegateImpl.prototype.peripheralDidDiscoverCharacteristicsForServiceError = function (peripheral, service, error) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidDiscoverCharacteristicsForServiceError ---- peripheral: ".concat(peripheral, ", service: ").concat(service, ", error: ").concat(error)); } this.subDelegates.forEach(function (d) { if (d.peripheralDidDiscoverCharacteristicsForServiceError) { d.peripheralDidDiscoverCharacteristicsForServiceError(peripheral, service, error); } }); }; /** * Invoked when you discover the descriptors of a specified characteristic. * @param peripheral [CBPeripheral] - The peripheral providing this information. * @param characteristic [CBCharacteristic] - The characteristic that the characteristic descriptors belong to. * @param error [NSError] - If an error occurred, the cause of the failure. */ CBPeripheralDelegateImpl.prototype.peripheralDidDiscoverDescriptorsForCharacteristicError = function (peripheral, characteristic, error) { // NOTE that this cb won't be invoked bc we currently don't discover descriptors if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidDiscoverDescriptorsForCharacteristicError ---- peripheral: ".concat(peripheral, ", characteristic: ").concat(characteristic, ", error: ").concat(error)); } this.subDelegates.forEach(function (d) { if (d.peripheralDidDiscoverDescriptorsForCharacteristicError) { d.peripheralDidDiscoverDescriptorsForCharacteristicError(peripheral, characteristic, error); } }); }; /** * Invoked when you retrieve a specified characteristic’s value, or when * the peripheral device notifies your app that the characteristic’s * value has changed. */ CBPeripheralDelegateImpl.prototype.peripheralDidUpdateValueForCharacteristicError = function (peripheral, characteristic, error) { if (!characteristic) { if (Trace.isEnabled()) { CLog(CLogTypes.warning, 'CBPeripheralDelegateImpl.peripheralDidUpdateValueForCharacteristicError ---- No CBCharacteristic.'); } return; } this.subDelegates.forEach(function (d) { if (d.peripheralDidUpdateValueForCharacteristicError) { d.peripheralDidUpdateValueForCharacteristicError(peripheral, characteristic, error); } }); if (error !== null) { if (Trace.isEnabled()) { CLog(CLogTypes.error, "CBPeripheralDelegateImpl.peripheralDidUpdateValueForCharacteristicError ---- ".concat(error)); } return; } if (characteristic.isNotifying) { var pUUID = NSUUIDToString(peripheral.identifier); var cUUID = CBUUIDToString(characteristic.UUID); var sUUID = CBUUIDToString(characteristic.service.UUID); var key = sUUID + '/' + cUUID; if (this.onNotifyCallbacks[key]) { this.onNotifyCallbacks[key]({ // type: 'notification', peripheralUUID: pUUID, serviceUUID: sUUID, characteristicUUID: cUUID, ios: characteristic.value, value: toArrayBuffer(characteristic.value) }); } else { if (Trace.isEnabled()) { CLog(CLogTypes.info, '----- CALLBACK IS GONE -----'); } } } }; /** * Invoked when you retrieve a specified characteristic descriptor’s value. */ CBPeripheralDelegateImpl.prototype.peripheralDidUpdateValueForDescriptorError = function (peripheral, descriptor, error) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidUpdateValueForDescriptorError ---- peripheral: ".concat(peripheral, ", descriptor: ").concat(descriptor, ", error: ").concat(error)); } this.subDelegates.forEach(function (d) { if (d.peripheralDidUpdateValueForDescriptorError) { d.peripheralDidUpdateValueForDescriptorError(peripheral, descriptor, error); } }); }; /** * Invoked when you write data to a characteristic’s value. */ CBPeripheralDelegateImpl.prototype.peripheralDidWriteValueForCharacteristicError = function (peripheral, characteristic, error) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidWriteValueForCharacteristicError ---- peripheral: ".concat(peripheral, ", characteristic: ").concat(characteristic, ", error: ").concat(error)); } this.subDelegates.forEach(function (d) { if (d.peripheralDidWriteValueForCharacteristicError) { d.peripheralDidWriteValueForCharacteristicError(peripheral, characteristic, error); } }); }; /** * Invoked when the peripheral receives a request to start or stop * providing notifications for a specified characteristic’s value. */ CBPeripheralDelegateImpl.prototype.peripheralDidUpdateNotificationStateForCharacteristicError = function (peripheral, characteristic, error) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidUpdateNotificationStateForCharacteristicError ---- peripheral: ".concat(peripheral, ", characteristic: ").concat(characteristic, ", error: ").concat(error)); } this.subDelegates.forEach(function (d) { if (d.peripheralDidUpdateNotificationStateForCharacteristicError) { d.peripheralDidUpdateNotificationStateForCharacteristicError(peripheral, characteristic, error); } }); if (error) { if (Trace.isEnabled()) { CLog(CLogTypes.error, "CBPeripheralDelegateImpl.peripheralDidUpdateNotificationStateForCharacteristicError ---- ".concat(error)); } } else { if (characteristic.isNotifying) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidUpdateNotificationStateForCharacteristicError ---- Notification began on ".concat(characteristic)); } } else { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidUpdateNotificationStateForCharacteristicError ---- Notification stopped on ".concat(characteristic)); } // Bluetooth._manager.cancelPeripheralConnection(peripheral); } } }; /** * IInvoked when you write data to a characteristic descriptor’s value. */ CBPeripheralDelegateImpl.prototype.peripheralDidWriteValueForDescriptorError = function (peripheral, descriptor, error) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBPeripheralDelegateImpl.peripheralDidWriteValueForDescriptorError ---- peripheral: ".concat(peripheral, ", descriptor: ").concat(descriptor, ", error: ").concat(error)); } this.subDelegates.forEach(function (d) { if (d.peripheralDidWriteValueForDescriptorError) { d.peripheralDidWriteValueForDescriptorError(peripheral, descriptor, error); } }); }; CBPeripheralDelegateImpl.ObjCProtocols = [CBPeripheralDelegate]; return CBPeripheralDelegateImpl; }(NSObject)); var CBCentralManagerDelegateImpl = /** @class */ (function (_super) { __extends(CBCentralManagerDelegateImpl, _super); function CBCentralManagerDelegateImpl() { return _super !== null && _super.apply(this, arguments) || this; } CBCentralManagerDelegateImpl.prototype.addSubDelegate = function (delegate) { var index = this.subDelegates.indexOf(delegate); if (index === -1) { this.subDelegates.push(delegate); } }; CBCentralManagerDelegateImpl.prototype.removeSubDelegate = function (delegate) { var index = this.subDelegates.indexOf(delegate); if (index !== -1) { this.subDelegates.splice(index, 1); } }; CBCentralManagerDelegateImpl.new = function () { return _super.new.call(this); }; CBCentralManagerDelegateImpl.prototype.initWithOwner = function (owner) { this._owner = owner; this.subDelegates = []; if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBCentralManagerDelegateImpl.initWithOwner: ".concat(this._owner)); } // this._callback = callback; return this; }; /** * Invoked when a connection is successfully created with a peripheral. * This method is invoked when a call to connect(_:options:) is successful. * You typically implement this method to set the peripheral’s delegate and to discover its services. * @param central [CBCentralManager] - The central manager providing this information. * @param peripheral [CBPeripheral] - The peripheral that has been connected to the system. */ CBCentralManagerDelegateImpl.prototype.centralManagerDidConnectPeripheral = function (central, peripheral) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "----- CBCentralManagerDelegateImpl centralManager:didConnectPeripheral: ".concat(peripheral)); } this._owner.get().onPeripheralConnected(peripheral); if (Trace.isEnabled()) { CLog(CLogTypes.info, "----- CBCentralManagerDelegateImpl centralManager:didConnectPeripheral, let's discover service"); } this.subDelegates.forEach(function (d) { if (d.centralManagerDidConnectPeripheral) { d.centralManagerDidConnectPeripheral(central, peripheral); } }); }; /** * Invoked when an existing connection with a peripheral is torn down. * This method is invoked when a peripheral connected via the connect(_:options:) method is disconnected. * If the disconnection was not initiated by cancelPeripheralConnection(_:), the cause is detailed in error. * After this method is called, no more methods are invoked on the peripheral device’s CBPeripheralDelegate object. * Note that when a peripheral is disconnected, all of its services, characteristics, and characteristic descriptors are invalidated. * @param central [CBCentralManager] - The central manager providing this information. * @param peripheral [CBPeripheral] - The peripheral that has been disconnected. * @param error? [NSError] - If an error occurred, the cause of the failure. */ CBCentralManagerDelegateImpl.prototype.centralManagerDidDisconnectPeripheralError = function (central, peripheral, error) { // this event needs to be honored by the client as any action afterwards crashes the app var UUID = NSUUIDToString(peripheral.identifier); if (Trace.isEnabled()) { CLog(CLogTypes.info, 'CBCentralManagerDelegate.centralManagerDidDisconnectPeripheralError ----', central, peripheral, error); } this._owner.get().onPeripheralDisconnected(peripheral); this.subDelegates.forEach(function (d) { if (d.centralManagerDidDisconnectPeripheralError) { d.centralManagerDidDisconnectPeripheralError(central, peripheral, error); } }); }; /** * Invoked when the central manager fails to create a connection with a peripheral. * This method is invoked when a connection initiated via the connect(_:options:) method fails to complete. * Because connection attempts do not time out, a failed connection usually indicates a transient issue, in which case you may attempt to connect to the peripheral again. * @param central [CBCentralManager] - The central manager providing this information. * @param peripheral [CBPeripheral] - The peripheral that failed to connect. * @param error? [NSError] - The cause of the failure. */ CBCentralManagerDelegateImpl.prototype.centralManagerDidFailToConnectPeripheralError = function (central, peripheral, error) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'CBCentralManagerDelegate.centralManagerDidFailToConnectPeripheralError ----', central, peripheral, error); } this.subDelegates.forEach(function (d) { if (d.centralManagerDidDisconnectPeripheralError) { d.centralManagerDidFailToConnectPeripheralError(central, peripheral, error); } }); }; /** * Invoked when the central manager discovers a peripheral while scanning. * The advertisement data can be accessed through the keys listed in Advertisement Data Retrieval Keys. * You must retain a local copy of the peripheral if any command is to be performed on it. * In use cases where it makes sense for your app to automatically connect to a peripheral that is located within a certain range, you can use RSSI data to determine the proximity of a discovered peripheral device. * @param central [CBCentralManager] - The central manager providing the update. * @param peripheral [CBPeripheral] - The discovered peripheral. * @param advData [NSDictionary<string, any>] - A dictionary containing any advertisement data. * @param RSSI [NSNumber] - The current received signal strength indicator (RSSI) of the peripheral, in decibels. */ CBCentralManagerDelegateImpl.prototype.centralManagerDidDiscoverPeripheralAdvertisementDataRSSI = function (central, peripheral, advData, RSSI) { var UUIDString = NSUUIDToString(peripheral.identifier); if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBCentralManagerDelegateImpl.centralManagerDidDiscoverPeripheralAdvertisementDataRSSI ---- ".concat(peripheral.name, " @ ").concat(UUIDString, " @ ").concat(RSSI, " @ ").concat(advData)); } var owner = this._owner && this._owner.get(); if (!owner) { return; } owner.adddDiscoverPeripheral(peripheral); var advertismentData = new AdvertismentData(advData); var payload = { UUID: UUIDString, name: peripheral.name, localName: advertismentData.localName, RSSI: RSSI, advertismentData: advertismentData, nativeDevice: peripheral, state: this._owner.get()._getState(peripheral.state), manufacturerId: advertismentData.manufacturerId }; owner._advData[UUIDString] = advertismentData; if (owner._onDiscovered) { owner._onDiscovered(payload); } owner.sendEvent(Bluetooth.device_discovered_event, payload); }; /** * Invoked when the central manager’s state is updated. * You implement this required method to ensure that Bluetooth low energy is supported and available to use on the central device. * You should issue commands to the central manager only when the state of the central manager is powered on, as indicated by the poweredOn constant. * A state with a value lower than poweredOn implies that scanning has stopped and that any connected peripherals have been disconnected. * If the state moves below poweredOff, all CBPeripheral objects obtained from this central manager become invalid and must be retrieved or discovered again. * For a complete list and discussion of the possible values representing the state of the central manager, see the CBCentralManagerState enumeration in CBCentralManager. * @param central [CBCentralManager] - The central manager providing this information. */ CBCentralManagerDelegateImpl.prototype.centralManagerDidUpdateState = function (central) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBCentralManagerDelegateImpl.centralManagerDidUpdateState: ".concat(central.state)); } if (central.state === CBManagerState.Unsupported) { if (Trace.isEnabled()) { CLog(CLogTypes.warning, 'CBCentralManagerDelegateImpl.centralManagerDidUpdateState ---- This hardware does not support Bluetooth Low Energy.'); } } this._owner.get().state = central.state; }; /** * Invoked when the central manager is about to be restored by the system. * @param central [CBCentralManager] - The central manager providing this information. * @param dict [NSDictionary<string, any>] - A dictionary containing information about the central manager that was preserved by the system at the time the app was terminated. * For the available keys to this dictionary, see Central Manager State Restoration Options. * @link - https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/central_manager_state_restoration_options */ CBCentralManagerDelegateImpl.prototype.centralManagerWillRestoreState = function (central, dict) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "CBCentralManagerDelegateImpl.centralManagerWillRestoreState ---- central: ".concat(central, ", dict: ").concat(dict)); } }; CBCentralManagerDelegateImpl.ObjCProtocols = [CBCentralManagerDelegate]; return CBCentralManagerDelegateImpl; }(NSObject)); export class AdvertismentData { constructor(advData) { this.advData = advData; } get manufacturerData() { const data = this.advData.objectForKey(CBAdvertisementDataManufacturerDataKey); if (data && data.length > 2) { return toArrayBuffer(data.subdataWithRange(NSMakeRange(2, data.length - 2))); } return undefined; } get data() { return toArrayBuffer(this.advData); } get manufacturerId() { const data = this.advData.objectForKey(CBAdvertisementDataManufacturerDataKey); if (data && data.length >= 2) { const manufacturerIdBuffer = toArrayBuffer(data.subdataWithRange(NSMakeRange(0, 2))); return new DataView(manufacturerIdBuffer, 0).getUint16(0, true); } return -1; } get txPowerLevel() { return this.advData.objectForKey(CBAdvertisementDataTxPowerLevelKey) || Number.MIN_VALUE; } get localName() { return this.advData.objectForKey(CBAdvertisementDataLocalNameKey); } get flags() { return -1; } get serviceUUIDs() { const result = []; const serviceUuids = this.advData.objectForKey(CBAdvertisementDataServiceUUIDsKey); if (serviceUuids) { for (let i = 0; i < serviceUuids.count; i++) { result.push(CBUUIDToString(serviceUuids[i])); } } return result; } get overtflow() { const result = []; const serviceUuids = this.advData.objectForKey(CBAdvertisementDataOverflowServiceUUIDsKey); if (serviceUuids) { for (let i = 0; i < serviceUuids.count; i++) { result.push(CBUUIDToString(serviceUuids[i])); } } return result; } get solicitedServices() { const result = []; const serviceUuids = this.advData.objectForKey(CBAdvertisementDataSolicitedServiceUUIDsKey); if (serviceUuids) { for (let i = 0; i < serviceUuids.count; i++) { result.push(CBUUIDToString(serviceUuids[i])); } } return result; } get connectable() { return this.advData.objectForKey(CBAdvertisementDataIsConnectable); } get serviceData() { const result = {}; const obj = this.advData.objectForKey(CBAdvertisementDataServiceDataKey); if (obj) { obj.enumerateKeysAndObjectsUsingBlock((key, data) => { result[CBUUIDToString(key)] = toArrayBuffer(data); }); } return result; } } let _bluetoothInstance; export function getBluetoothInstance() { if (!_bluetoothInstance) { _bluetoothInstance = new Bluetooth(); } return _bluetoothInstance; } export function toArrayBuffer(value) { return value ? interop.bufferFromData(value) : null; } function _getProperties(characteristic) { const props = characteristic.properties; return { // broadcast: (props & CBCharacteristicPropertyBroadcast) === CBCharacteristicPropertyBroadcast, broadcast: (props & 1 /* CBCharacteristicProperties.PropertyBroadcast */) === 1 /* CBCharacteristicProperties.PropertyBroadcast */, read: (props & 2 /* CBCharacteristicProperties.PropertyRead */) === 2 /* CBCharacteristicProperties.PropertyRead */, broadcast2: (props & 1 /* CBCharacteristicProperties.PropertyBroadcast */) === 1 /* CBCharacteristicProperties.PropertyBroadcast */, read2: (props & 2 /* CBCharacteristicProperties.PropertyRead */) === 2 /* CBCharacteristicProperties.PropertyRead */, write: (props & 8 /* CBCharacteristicProperties.PropertyWrite */) === 8 /* CBCharacteristicProperties.PropertyWrite */, writeWithoutResponse: (props & 4 /* CBCharacteristicProperties.PropertyWriteWithoutResponse */) === 4 /* CBCharacteristicProperties.PropertyWriteWithoutResponse */, notify: (props & 16 /* CBCharacteristicProperties.PropertyNotify */) === 16 /* CBCharacteristicProperties.PropertyNotify */, indicate: (props & 32 /* CBCharacteristicProperties.PropertyIndicate */) === 32 /* CBCharacteristicProperties.PropertyIndicate */, authenticatedSignedWrites: (props & 64 /* CBCharacteristicProperties.PropertyAuthenticatedSignedWrites */) === 64 /* CBCharacteristicProperties.PropertyAuthenticatedSignedWrites */, extendedProperties: (props & 128 /* CBCharacteristicProperties.PropertyExtendedProperties */) === 128 /* CBCharacteristicProperties.PropertyExtendedProperties */, notifyEncryptionRequired: (props & 256 /* CBCharacteristicProperties.PropertyNotifyEncryptionRequired */) === 256 /* CBCharacteristicProperties.PropertyNotifyEncryptionRequired */, indicateEncryptionRequired: (props & 512 /* CBCharacteristicProperties.PropertyIndicateEncryptionRequired */) === 512 /* CBCharacteristicProperties.PropertyIndicateEncryptionRequired */ }; } export function NSUUIDToString(uuid) { return uuid.toString().toLowerCase(); } export function CBUUIDToString(uuid) { return uuid.UUIDString.toLowerCase(); } export class Bluetooth extends BluetoothCommon { clear() { // doing nothing on ios } set state(state) { if (this._state !== state) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'BLE state change', state); } this._state = state; this.sendEvent(BluetoothCommon.bluetooth_status_event, { state: state === 2 /* CBManagerState.Unsupported */ ? 'unsupported' : state === 5 /* CBManagerState.PoweredOn */ ? 'on' : 'off' }); } } get state() { return this._state; } get centralDelegate() { if (!this._centralDelegate) { this._centralDelegate = CBCentralManagerDelegateImpl.new().initWithOwner(new WeakRef(this)); } return this._centralDelegate; } ensureCentralManager() { if (!this._centralManager) { const options = new NSMutableDictionary([this.showPowerAlertPopup], [CBCentralManagerOptionShowPowerAlertKey]); if (this.restoreIdentifier) { options.setObjectForKey(this.restoreIdentifier, CBCentralManagerOptionRestoreIdentifierKey); } this._centralManager = CBCentralManager.alloc().initWithDelegateQueueOptions(this.centralDelegate, null, options); if (Trace.isEnabled()) { CLog(CLogTypes.info, `creating CBCentralManager: ${this._centralManager}`); } } } get centralManager() { this.ensureCentralManager(); return this._centralManager; } constructor(restoreIdentifierOrOptions = 'ns_bluetooth', showPowerAlertPopup = false) { super(); this.showPowerAlertPopup = showPowerAlertPopup; this._centralDelegate = null; this._centralManager = null; this._discoverPeripherals = {}; this._connectedPeripherals = {}; this._connectCallbacks = {}; this._disconnectCallbacks = {}; // _advData is used to store Advertisment Data so that we can send it to connection callback this._advData = {}; this._onDiscovered = null; this._state = 2 /* CBManagerState.Unsupported */; if (typeof restoreIdentifierOrOptions === 'object') { if (restoreIdentifierOrOptions.restoreIdentifier === undefined) { this.restoreIdentifier = 'ns_bluetooth'; } else { this.restoreIdentifier = restoreIdentifierOrOptions.restoreIdentifier; } this.showPowerAlertPopup = !!restoreIdentifierOrOptions.showPowerAlertPopup; } else { this.restoreIdentifier = restoreIdentifierOrOptions; } if (Trace.isEnabled()) { CLog(CLogTypes.info, 'Creating Bluetooth instance', this.restoreIdentifier); } } onListenerAdded(eventName, count) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'onListenerAdded', eventName, count); } if (eventName === Bluetooth.bluetooth_status_event) { // ensure centralManager is set to have status event this.ensureCentralManager(); } } _getState(state) { if (state === 1 /* CBPeripheralState.Connecting */) { return 'connecting'; } else if (state === 2 /* CBPeripheralState.Connected */) { return 'connected'; } else if (state === 0 /* CBPeripheralState.Disconnected */) { return 'disconnected'; } else { if (Trace.isEnabled()) { CLog(CLogTypes.warning, '_getState ---- Unexpected state, returning "disconnected" for state of', state); } return 'disconnected'; } } prepareConnectedPeripheralDelegate(peripheral) { if (!peripheral.delegate) { const UUID = NSUUIDToString(peripheral.identifier); const delegate = CBPeripheralDelegateImpl.new().initWithOwner(new WeakRef(this)); CFRetain(delegate); peripheral.delegate = delegate; } } onPeripheralDisconnected(peripheral) { const UUID = NSUUIDToString(peripheral.identifier); const cb = this._disconnectCallbacks[UUID]; if (cb) { cb({ UUID, name: peripheral.name }); delete this._disconnectCallbacks[UUID]; } this.sendEvent(Bluetooth.device_disconnected_event, { UUID, name: peripheral.name }); if (peripheral.delegate) { CFRelease(peripheral.delegate); peripheral.delegate = null; } delete this._connectedPeripherals[UUID]; } onPeripheralConnected(peripheral) { const UUID = NSUUIDToString(peripheral.identifier); this.prepareConnectedPeripheralDelegate(peripheral); this._connectedPeripherals[UUID] = peripheral; } async isBluetoothEnabled() { if (this._state === 2 /* CBManagerState.Unsupported */) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'isBluetoothEnabled', 'central manager not ready, waiting for it to start'); } this.ensureCentralManager(); return new Promise((resolve) => { this.once(BluetoothCommon.bluetooth_status_event, () => { resolve(this._isEnabled()); }); }); } return this._isEnabled(); } startScanning(args) { const methodName = 'startScanning'; return new Promise((resolve, reject) => { try { this._discoverPeripherals = {}; this._onDiscovered = args.onDiscovered; let services = null; if (args.filters) { services = []; args.filters.forEach((f) => { if (f.serviceUUID) { services.push(CBUUID.UUIDWithString(f.serviceUUID)); } }); } if (Trace.isEnabled()) { CLog(CLogTypes.info, methodName, '---- services:', services); } const options = new NSMutableDictionary(); if (!args.avoidDuplicates) { options.setObjectForKey(true, CBCentralManagerScanOptionAllowDuplicatesKey); } // TODO: check on the services as any casting this.centralManager.scanForPeripheralsWithServicesOptions(services, options); if (this.scanningReferTimer) { clearTimeout(this.scanningReferTimer.timer); this.scanningReferTimer.resolve(); } this.scanningReferTimer = {}; if (args.seconds) { this.scanningReferTimer.timer = setTimeout(() => { // note that by now a manual 'stop' may have been invoked, but that doesn't hurt this.centralManager.stopScan(); resolve(); }, args.seconds * 1000); this.scanningReferTimer.resolve = resolve; } else { resolve(); } if (__DEV__) { } } catch (ex) { if (Trace.isEnabled()) { CLog(CLogTypes.error, methodName, '---- error:', ex); } reject(new BluetoothError(ex.message, { stack: ex.stack, nativeException: ex.nativeException, method: methodName, arguments: args })); } }); } enable() { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'enable ---- Not possible on iOS'); } return this.isBluetoothEnabled(); } openBluetoothSettings(url) { return Promise.reject(new BluetoothError(BluetoothCommon.msg_cant_open_settings)); // return this.isBluetoothEnabled().then(isEnabled => { // if (!isEnabled) { // return Promise.resolve().then(() => { // const settingsUrl = NSURL.URLWithString(url || UIApplicationOpenSettingsURLString); // if (UIApplication.sharedApplication.canOpenURL(settingsUrl)) { // UIApplication.sharedApplication.openURLOptionsCompletionHandler(settingsUrl, null, function(success) { // // we get the callback for opening the URL, not enabling the bluetooth! // if (success) { // return Promise.reject(undefined); // } else { // return Promise.reject(BluetoothCommon.msg_cant_open_settings); // } // }); // } // }); // } // return null; // }); } stopScanning() { const methodName = 'stopScanning'; return new Promise((resolve, reject) => { try { this.centralManager.stopScan(); if (this.scanningReferTimer) { this.scanningReferTimer.resolve && this.scanningReferTimer.resolve(); clearTimeout(this.scanningReferTimer.timer); this.scanningReferTimer = null; } resolve(); } catch (ex) { if (Trace.isEnabled()) { CLog(CLogTypes.error, methodName, '---- error:', ex); } reject(new BluetoothError(ex.message, { stack: ex.stack, nativeException: ex.nativeException, method: methodName })); } }); } clearAdvertismentCache() { this._advData = {}; } async getDevice(args) { const methodName = 'getDevice'; if (!args.UUID) { return Promise.reject(new BluetoothError(BluetoothCommon.msg_missing_parameter, { method: methodName, type: BluetoothCommon.UUIDKey, arguments: args })); } return this.findDiscoverPeripheral(args.UUID); } async connect(args) { const methodName = 'connect'; try { if (!args.UUID) { return Promise.reject(new BluetoothError(BluetoothCommon.msg_missing_parameter, { method: methodName, type: BluetoothCommon.UUIDKey, arguments: args })); } const connectingUUID = args.UUID; if (Trace.isEnabled()) { CLog(CLogTypes.info, methodName, '----', args.UUID); } const peripheral = this.findDiscoverPeripheral(args.UUID); if (Trace.isEnabled()) { CLog(CLogTypes.info, methodName, '---- peripheral found', peripheral); } if (!peripheral) { throw new BluetoothError(BluetoothCommon.msg_no_peripheral, { method: methodName, arguments: args }); } else { await new Promise((resolve, reject) => { const subD = { centralManagerDidConnectPeripheral: (central, peripheral) => { const UUID = NSUUIDToString(peripheral.identifier); if (UUID === connectingUUID) { resolve(); this._centralDelegate.removeSubDelegate(subD); } }, centralManagerDidFailToConnectPeripheralError: (central, peripheral, error) => { const UUID = NSUUIDToString(peripheral.identifier); if (UUID === connectingUUID) { reject(new BluetoothError(error.localizedDescription, { method: methodName, status: error.code })); this._centralDelegate.removeSubDelegate(subD); } } }; if (Trace.isEnabled()) { CLog(CLogTypes.info, methodName, '---- Connecting to peripheral with UUID:', connectingUUID, this._centralDelegate, this._centralManager); } this.centralDelegate.addSubDelegate(subD); this._connectCallbacks[connectingUUID] = args.onConnected; this._disconnectCallbacks[connectingUUID] = args.onDisconnected; if (Trace.isEnabled()) { CLog(CLogTypes.info, methodName, '----about to connect:', connectingUUID, this._centralDelegate, this._centralManager); } this.centralManager.connectPeripheralOptions(peripheral, null); }); let services, mtu = FIXED_IOS_MTU; if (args.autoDiscoverAll !== false) { services = (await this.discoverAll({ peripheralUUID: connectingUUID }))?.services; } else if (args.serviceUUIDs) { services = (await this.discoverAll({ peripheralUUID: connectingUUID, serviceUUIDs: args.serviceUUIDs }))?.services; } if (!!args.autoMaxMTU) { mtu = await this.requestMtu({ peripheralUUID: connectingUUID, value: FIXED_IOS_MTU }); } const adv = this._advData[connectingUUID]; const dataToSend = { UUID: connectingUUID, name: peripheral.name, state: this._getState(peripheral.state), services, nativeDevice: peripheral, localName: adv?.localName, manufacturerId: adv?.manufacturerId, advertismentData: adv, mtu }; // delete this._advData[connectingUUID]; const cb = this._connectCallbacks[connectingUUID]; if (cb) { cb(dataToSend); delete this._connectCallbacks[connectingUUID]; } this.sendEvent(Bluetooth.device_connected_event, dataToSend); return dataToSend; } } catch (ex) { if (Trace.isEnabled()) { CLog(CLogTypes.error, methodName, '---- error:', ex); } throw new BluetoothError(ex.message, { stack: ex.stack, nativeException: ex.nativeException, method: methodName, arguments: args }); } } async disconnect(args) { const methodName = 'disconnect'; try { if (!args.UUID) { throw new BluetoothError(BluetoothCommon.msg_missing_parameter, { method: methodName, type: BluetoothCommon.UUIDKey, arguments: args }); } const pUUID = args.UUID; const peripheral = this.findPeripheral(pUUID); if (!peripheral) { throw new BluetoothError(BluetoothCommon.msg_no_peripheral, { method: methodName, arguments: args }); } else { // no need to send an error when already disconnected, but it's wise to check it if (peripheral.state !== 0 /* CBPeripheralState.Disconnected */) { return new Promise((resolve, reject) => { const subD = { centralManagerDidDisconnectPeripheralError: (central, peripheral, error) => { const UUID = NSUUIDToString(peripheral.identifier); if (UUID === pUUID) { if (error) { reject(new BluetoothError(error.localizedDescription, { method: methodName, status: error.code })); } else { resolve(); } this._centralDelegate.removeSubDelegate(subD); } } }; this.centralDelegate.addSubDelegate(subD); if (Trace.isEnabled()) { CLog(CLogTypes.info, methodName, '---- Disconnecting peripheral with UUID', pUUID); } this.centralManager.cancelPeripheralConnection(peripheral); }); } } } catch (ex) { if (Trace.isEnabled()) { CLog(CLogTypes.error, methodName, '---- error:', ex); } throw new BluetoothError(ex.message, { stack: ex.stack, nativeException: ex.nativeException, method: methodName, arguments: args }); } } async isConnected(args) { const methodName = 'isConnected'; try { if (!args.UUID) { throw new BluetoothError(BluetoothCommon.msg_missing_parameter, { method: methodName, type: BluetoothCommon.UUIDKey, arguments: args }); } const peripheral = this.findPeripheral(args.UUID); if (peripheral === null) { return false; } else { if (Trace.isEnabled()) { CLog(CLogTypes.info, methodName, '---- checking connection with peripheral UUID:', args.UUID); } return peripheral.state === 2 /* CBPeripheralState.Connected */; } } catch (ex) { if (Trace.isEnabled()) { CLog(CLogTypes.error, methodName, '---- error:', ex); } throw new BluetoothError(ex.message, { stack: ex.stack, nativeException: ex.nativeException, method: methodName, arguments: args }); } } findPeripheral(UUID) { let result = this._connectedPeripherals[UUID] || this._discoverPeripherals[UUID]; if (!result) { const periphs = this.centralManager.retrievePeripheralsWithIdentifiers([NSUUID.alloc().initWithUUIDString(UUID)]); if (periphs.count > 0) { result = periphs.objectAtIndex(0); this.prepareConnectedPeripheralDelegate(result); } } return result; } adddDiscoverPeripheral(peripheral) { const UUID = NSUUIDToString(peripheral.identifier); if (!this._discoverPeripherals[UUID]) { this._discoverPeripherals[UUID] = peripheral; } } findDiscoverPeripheral(UUID) { let result = this._discoverPeripherals[UUID]; if (!result) { const periphs = this.centralManager.retrievePeripheralsWithIdentifiers([NSUUID.alloc().initWithUUIDString(UUID)]); if (periphs.count > 0) { result = periphs.objectAtIndex(0); } } return result; } read(args) { const methodName = 'read'; return this._getWrapper(args, 2 /* CBCharacteristicProperties.PropertyRead */).then((wrapper) => new Promise((resolve, reject) => { if (Trace.isEnabled()) { CLog(CLogTypes.info, methodName, `---- peripheralUUID:${args.peripheralUUID} serviceUUID:${args.serviceUUID} characteristicUUID:${args.characteristicUUID}`); } const pUUID = args.peripheralUUID; const p = wrapper.peripheral; let timeoutTimer; if (args.timeout > 0) { timeoutTimer = setTimeout(() => { // we need to try catch because the simple fact of creating a new Error actually throws. // so we will get an uncaughtException try { reject(new Error('timeout')); } catch { }