UNPKG

noble-uwp

Version:

Noble (Node.js Bluetooth LE) with Windows 10 UWP bindings

858 lines (727 loc) 31.2 kB
'use strict'; // Noble bindings for Windows UWP BLE APIs const events = require('events'); const util = require('util'); const debug = require('debug')('noble-uwp'); const rt = require('./rt-utils'); // Note the load order here is important for cross-namespace dependencies. rt.using('Windows.Foundation'); rt.using('Windows.Storage.Streams'); rt.using('Windows.Devices.Enumeration'); rt.using('Windows.Devices.Bluetooth.GenericAttributeProfile'); rt.using('Windows.Devices.Bluetooth'); rt.using('Windows.Devices.Bluetooth.Advertisement'); rt.using('Windows.Devices.Radios'); const BluetoothLEDevice = Windows.Devices.Bluetooth.BluetoothLEDevice; const BluetoothCacheMode = Windows.Devices.Bluetooth.BluetoothCacheMode; const BluetoothUuidHelper = Windows.Devices.Bluetooth.BluetoothUuidHelper; const BluetoothConnectionStatus = Windows.Devices.Bluetooth.BluetoothConnectionStatus; const BluetoothLEAdvertisementWatcher = Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementWatcher; const BluetoothLEScanningMode = Windows.Devices.Bluetooth.Advertisement.BluetoothLEScanningMode; const BluetoothLEAdvertisementType = Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementType; const BluetoothLEAdvertisementDataTypes = Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementDataTypes; const BluetoothLEAdvertisementWatcherStatus = Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementWatcherStatus; const GattCharacteristicProperties = Windows.Devices.Bluetooth.GenericAttributeProfile.GattCharacteristicProperties; const GattDeviceService = Windows.Devices.Bluetooth.GenericAttributeProfile.GattDeviceService; const GattServiceUuids = Windows.Devices.Bluetooth.GenericAttributeProfile.GattServiceUuids; const GattCommunicationStatus = Windows.Devices.Bluetooth.GenericAttributeProfile.GattCommunicationStatus; const GattClientCharacteristicConfigurationDescriptorValue = Windows.Devices.Bluetooth.GenericAttributeProfile.GattClientCharacteristicConfigurationDescriptorValue; const Radio = Windows.Devices.Radios.Radio; const RadioKind = Windows.Devices.Radios.RadioKind; const RadioState = Windows.Devices.Radios.RadioState; const DataReader = Windows.Storage.Streams.DataReader; let NobleBindings = function () { this._radio = null; this._radioState = 'unknown'; this._deviceMap = {}; this._devicesListeners = {}; this._acceptOnlyScanResponse = false; }; util.inherits(NobleBindings, events.EventEmitter); NobleBindings.prototype.init = function () { this._onAdvertisementWatcherReceived = this._onAdvertisementWatcherReceived.bind(this); this._onAdvertisementWatcherStopped = this._onAdvertisementWatcherStopped.bind(this); this._onConnectionStatusChanged = this._onConnectionStatusChanged.bind(this); this._advertisementWatcher = new BluetoothLEAdvertisementWatcher(); this._advertisementWatcher.scanningMode = BluetoothLEScanningMode.active; this._advertisementWatcher.on('received', this._onAdvertisementWatcherReceived); this._advertisementWatcher.on('stopped', this._onAdvertisementWatcherStopped); debug('initialized'); rt.promisify(Radio.getRadiosAsync)().then(radiosList => { radiosList = rt.toArray(radiosList); this._radio = radiosList.find(radio => radio.kind === RadioKind.bluetooth); if (this._radio) { debug('found bluetooth radio: %s', this._radio.name); this._radio.on('stateChanged', (sender, e) => { this._updateRadioState(); }); } else { debug('no bluetooth radio found'); } this._updateRadioState(); }).catch(ex => { debug('failed to get radios: %s', ex.stack); this._updateRadioState(); }); }; NobleBindings.prototype.startScanning = function (serviceUuids, allowDuplicates) { if (!(serviceUuids && serviceUuids.length > 0)) { serviceUuids = null; } allowDuplicates = !!allowDuplicates; debug('startScanning(%s, %s)', (serviceUuids ? serviceUuids.join() : ''), allowDuplicates); this._filterAdvertisementServiceUuids = serviceUuids; this._allowAdvertisementDuplicates = allowDuplicates; if (this._advertisementWatcher.status === BluetoothLEAdvertisementWatcherStatus.started) { return; } this._advertisementWatcher.start(); rt.keepAlive(true); } NobleBindings.prototype.stopScanning = function () { if (this._advertisementWatcher.status === BluetoothLEAdvertisementWatcherStatus.started) { debug('stopScanning()'); this._advertisementWatcher.stop(); rt.keepAlive(false); } }; NobleBindings.prototype.connect = function (deviceUuid) { debug('connect(%s)', deviceUuid); let deviceRecord = this._deviceMap[deviceUuid]; if (!deviceRecord) { throw new Error('Invalid or unknown device UUID: ' + deviceUuid); } if (!deviceRecord.connectable) { throw new Error("Device is not connectable: " + deviceRecord.formattedAddress); } rt.promisify(BluetoothLEDevice.fromBluetoothAddressAsync)(deviceRecord.address).then(device => { debug('got bluetooth device: %s (%s)', device.name, device.deviceInformation.kind); deviceRecord.device = rt.trackDisposable(deviceUuid, device); deviceRecord.device.on( 'connectionStatusChanged', this._onConnectionStatusChanged); this.emit('connect', deviceUuid, null); rt.keepAlive(true); }).catch(ex => { debug('failed to get device %s: %s', deviceRecord.formattedAddress, ex.stack); this.emit('connect', deviceUuid, ex); }); }; NobleBindings.prototype.disconnect = function (deviceUuid) { debug('disconnect(%s)', deviceUuid); let deviceRecord = this._deviceMap[deviceUuid]; if (!deviceRecord) { throw new Error('Invalid or unknown device UUID: ' + deviceUuid); } if (deviceRecord.device) { deviceRecord.device.removeListener( 'connectionStatusChanged', this._onConnectionStatusChanged); deviceRecord.device = null; deviceRecord.serviceMap = {}; deviceRecord.characteristicMap = {}; deviceRecord.descriptorMap = {}; delete this._devicesListeners[deviceUuid]; rt.disposeAll(deviceUuid); rt.keepAlive(false); this.emit('disconnect', deviceUuid); } }; NobleBindings.prototype.updateRssi = function (deviceUuid) { debug('updateRssi(%s)', deviceUuid); // TODO: Retrieve updated RSSI let rssi = 0; this.emit('rssiUpdate', deviceUuid, rssi); }; NobleBindings.prototype.discoverServices = function (deviceUuid, filterServiceUuids) { if (filterServiceUuids && filterServiceUuids.length === 0) { filterServiceUuids = null; } debug('discoverServices(%s, %s)', deviceUuid, (filterServiceUuids ? filterServiceUuids.join() : '(all)')); let deviceRecord = this._deviceMap[deviceUuid]; if (!deviceRecord) { throw new Error('Invalid or unknown device UUID: ' + deviceUuid); } let device = deviceRecord.device; if (!device) { throw new Error('Device is not connected. UUID: ' + deviceUuid); } rt.promisify(device.getGattServicesAsync, device)( BluetoothCacheMode.uncached).then(result => { checkCommunicationResult(deviceUuid, result); let services = rt.trackDisposables(deviceUuid, rt.toArray(result.services)); let serviceUuids = services.map(s => uuidToString(s.uuid)) .filter(filterUuids(filterServiceUuids)); debug(deviceUuid + ' services: %o', serviceUuids); this.emit('servicesDiscover', deviceUuid, serviceUuids); }).catch(ex => { debug('failed to get GATT services for device %s: %s', deviceUuid, ex.stack); this.emit('servicesDiscover', deviceUuid, ex); }); }; NobleBindings.prototype.discoverIncludedServices = function (deviceUuid, serviceUuid, filterServiceUuids) { if (filterServiceUuids && filterServiceUuids.length === 0) { filterServiceUuids = null; } debug('discoverIncludedServices(%s, %s, %s)', deviceUuid, serviceUuid, (filterServiceUuids ? filterServiceUuids.join() : '(all)')); this._getCachedServiceAsync(deviceUuid, serviceUuid).then(service => { rt.promisify(service.getIncludedServicesAsync, service)( BluetoothCacheMode.uncached).then(result => { checkCommunicationResult(deviceUuid, result); let includedServices = rt.trackDisposables(deviceUuid, rt.toArray(result.services)); let includedServiceUuids = includedServices.map(s => uuidToString(s.uuid)) .filter(filterUuids(filterServiceUuids)); debug(deviceUuid + ' ' + serviceUuid + ' included services: ' + includedServiceUuids); this.emit('includedServicesDiscover', deviceUuid, serviceUuid, includedServiceUuids); }); }).catch(ex => { debug('failed to get GATT included services for device %s: %s', deviceUuid, + ex.stack); this.emit('includedServicesDiscover', deviceUuid, serviceUuid, ex); }); }; NobleBindings.prototype.discoverCharacteristics = function (deviceUuid, serviceUuid, filterCharacteristicUuids) { if (filterCharacteristicUuids && filterCharacteristicUuids.length === 0) { filterCharacteristicUuids = null; } debug('discoverCharacteristics(%s, %s, %s', deviceUuid, serviceUuid, (filterCharacteristicUuids ? filterCharacteristicUuids.join() : '(all)')); this._getCachedServiceAsync(deviceUuid, serviceUuid).then(service => { return rt.promisify(service.getCharacteristicsAsync, service)( BluetoothCacheMode.uncached).then(result => { checkCommunicationResult(deviceUuid, result); let characteristics = rt.toArray(result.characteristics) .filter(c => { return filterUuids(filterCharacteristicUuids)(uuidToString(c.uuid)); }) .map(c => ({ uuid: uuidToString(c.uuid), properties: characteristicPropertiesToStrings(c.characteristicProperties), })); debug('%s %s characteristics: %o', deviceUuid, serviceUuid, characteristics.map(c => c.uuid)); this.emit('characteristicsDiscover', deviceUuid, serviceUuid, characteristics); }); }).catch(ex => { debug('failed to get GATT characteristics for device %s: %s', deviceUuid, ex.stack); this.emit('characteristicsDiscover', deviceUuid, serviceUuid, ex); }); }; NobleBindings.prototype.read = function (deviceUuid, serviceUuid, characteristicUuid) { debug('read(%s, %s, %s)', deviceUuid, serviceUuid, characteristicUuid); this._getCachedCharacteristicAsync( deviceUuid, serviceUuid, characteristicUuid).then(characteristic => { return rt.promisify(characteristic.readValueAsync, characteristic)().then(result => { checkCommunicationResult(deviceUuid, result); let data = rt.toBuffer(result.value); debug(' => [' + data.length + ']'); this.emit('read', deviceUuid, serviceUuid, characteristicUuid, data, false); }); }).catch(ex => { debug('failed to read characteristic for device %s: %s', deviceUuid, ex.stack); this.emit('read', deviceUuid, serviceUuid, characteristicUuid, ex, false); }); }; NobleBindings.prototype.write = function (deviceUuid, serviceUuid, characteristicUuid, data, withoutResponse) { debug('write(%s, %s, %s, (data), %s)', deviceUuid, serviceUuid, characteristicUuid, withoutResponse); this._getCachedCharacteristicAsync( deviceUuid, serviceUuid, characteristicUuid).then(characteristic => { let rtBuffer = rt.fromBuffer(data); return rt.promisify(characteristic.writeValueWithResultAsync, characteristic)( rtBuffer).then(result => { checkCommunicationResult(deviceUuid, result); this.emit('write', deviceUuid, serviceUuid, characteristicUuid); }); }).catch(ex => { debug('failed to write characteristic for device %s: %s', deviceUuid, ex.stack); if (!withoutResponse) { this.emit('write', deviceUuid, serviceUuid, characteristicUuid, ex); } }); }; NobleBindings.prototype.broadcast = function (deviceUuid, serviceUuid, characteristicUuid, broadcast) { debug('broadcast(%s, %s, %s, %s)', deviceUuid, serviceUuid, + characteristicUuid, broadcast); this.emit('broadcast', deviceUuid, serviceUuid, characteristicUuid, new Error('Not implemented')); }; NobleBindings.prototype.notify = function (deviceUuid, serviceUuid, characteristicUuid, notify) { debug('notify(%s, %s, %s, %s)', deviceUuid, serviceUuid, characteristicUuid, notify); this._getCachedCharacteristicAsync( deviceUuid, serviceUuid, characteristicUuid).then(characteristic => { let listenerKey = serviceUuid + '/' + characteristicUuid; let deviceListeners = this._devicesListeners[deviceUuid] || {}; let listener = deviceListeners[listenerKey]; let descriptorValue = characteristic.characteristicProperties & GattCharacteristicProperties.indicate ? GattClientCharacteristicConfigurationDescriptorValue.indicate : GattClientCharacteristicConfigurationDescriptorValue.notify; if (notify) { if (listener) { // Already listening. this.emit('notify', deviceUuid, serviceUuid, characteristicUuid, notify); return; } return rt.promisify( characteristic.writeClientCharacteristicConfigurationDescriptorWithResultAsync, characteristic)(descriptorValue) .then(result => { checkCommunicationResult(deviceUuid, result); listener = ((source, e) => { debug('notification: %s %s %s', deviceUuid, serviceUuid, characteristicUuid); let data = rt.toBuffer(e.characteristicValue); this.emit('read', deviceUuid, serviceUuid, characteristicUuid, data, true); }).bind(this); characteristic.addListener('valueChanged', listener); deviceListeners[listenerKey] = listener; this.emit('notify', deviceUuid, serviceUuid, characteristicUuid, notify); }); } else { if (!listener) { // Already not listening. this.emit('notify', deviceUuid, serviceUuid, characteristicUuid, notify); return; } characteristic.removeListener('valueChanged', listener); delete deviceListeners[listenerKey]; return rt.promisify( characteristic.writeClientCharacteristicConfigurationDescriptorWithResultAsync, characteristic)(GattClientCharacteristicConfigurationDescriptorValue.none) .then(result => { checkCommunicationResult(deviceUuid, result); this.emit('notify', deviceUuid, serviceUuid, characteristicUuid, notify); }); } }).catch(ex => { debug('failed to enable characteristic notify for device %s: %s', deviceUuid, ex.stack); this.emit('notify', deviceUuid, serviceUuid, characteristicUuid, ex); }); }; NobleBindings.prototype.discoverDescriptors = function (deviceUuid, serviceUuid, characteristicUuid) { debug('discoverDescriptors(%s, %s, %s)', deviceUuid, serviceUuid, characteristicUuid); this._getCachedCharacteristicAsync( deviceUuid, serviceUuid, characteristicUuid).then(characteristic => { return rt.promisify(characteristic.getDescriptorsAsync, characteristic)( BluetoothCacheMode.uncached).then(result => { checkCommunicationResult(deviceUuid, result); let descriptors = rt.toArray(result.descriptors).map(d => d.uuid); this.emit('descriptorsDiscover', deviceUuid, serviceUuid, characteristicUuid, descriptors); }); }).catch(ex => { debug('failed to get GATT characteristic descriptors for device %s: %s', deviceUuid, ex.stack); this.emit('descriptorsDiscover', deviceUuid, serviceUuid, characteristicUuid, ex); }); }; NobleBindings.prototype.readValue = function (deviceUuid, serviceUuid, characteristicUuid, descriptorUuid) { debug('readValue(%s, %s, %s, %s)', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid); return this._getCachedDescriptorAsync( deviceUuid, serviceUuid, characteristicUuid, descriptorUuid).then(descriptor => { return rt.promisify(descriptor.readValueAsync, descriptor)( BluetoothCacheMode.uncached).then(result => { checkCommunicationResult(deviceUuid, result); let data = rt.toBuffer(result.value); debug(' => [' + data.length + ']'); this.emit('readValue', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data); }); }).catch(ex => { debug('failed to read GATT characteristic descriptor values for device %s: %s', deviceUuid, ex.stack); this.emit('readValue', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, ex); }); }; NobleBindings.prototype.writeValue = function (deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { debug('writeValue(%s, %s, %s, %s, (data))', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid); this._getCachedDescriptorAsync( deviceUuid, serviceUuid, characteristicUuid, descriptorUuid).then(descriptor => { let rtBuffer = rt.fromBuffer(data); return rt.promisify(descriptor.writeValueWithResultAsync, descriptor)( rtBuffer).then(result => { checkCommunicationResult(deviceUuid, result); this.emit('writeValue', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid); }); }).catch(ex => { debug('failed to write characteristic descriptor for device %s: %s', deviceUuid, ex.stack); if (!withoutResponse) { this.emit('writeValue', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, ex); } }); }; NobleBindings.prototype.readHandle = function (deviceUuid, handle) { this.emit('readHandle', deviceUuid, handle, new Error('Not supported')); }; NobleBindings.prototype.writeHandle = function (deviceUuid, handle, data, withoutResponse) { if (!withoutResponse) { this.emit('writeHandle', deviceUuid, handle, new Error('Not supported')); } }; NobleBindings.prototype._updateRadioState = function () { let state; if (!this._radio) { state = 'unsupported'; } else switch (this._radio.state) { case RadioState.on: debug('bluetooth radio is on'); state = 'poweredOn'; break; case RadioState.off: debug('bluetooth radio is off'); state = 'poweredOff'; break; case RadioState.disabled: debug('bluetooth radio is disabled'); state = 'poweredOff'; break; default: debug('bluetooth radio is in unknown state: ' + this._bluetoothRadio.state); state = 'unknown'; break; } if (state != this._radioState) { this._radioState = state; this.emit('stateChange', state); } }; NobleBindings.prototype._onAdvertisementWatcherReceived = function (sender, e) { let address = formatBluetoothAddress(e.bluetoothAddress); debug('watcher received: %s %s %s', getEnumName(BluetoothLEAdvertisementType, e.advertisementType), address, e.advertisement.localName); let deviceUuid = address.replace(/:/g, ''); let serviceUuids = undefined; const isScanResponse = e.advertisementType === BluetoothLEAdvertisementType.scanResponse; if (isScanResponse) { if (!this._deviceMap[deviceUuid]) { debug(' Ignoring scan response for unknown device'); return; } } else { if (!this._allowAdvertisementDuplicates && this._deviceMap[deviceUuid]) { debug(' Ignoring duplicate advertisement'); return; } let serviceUuidMatched = !this._filterAdvertisementServiceUuids; serviceUuids = rt.toArray(e.advertisement.serviceUuids).map(serviceUuid => { if (debug.enabled) { debug(' service UUID: %s', (getEnumName(GattServiceUuids, serviceUuid) || uuidToString(serviceUuid))); } serviceUuid = uuidToString(serviceUuid); if (!serviceUuidMatched && this._filterAdvertisementServiceUuids.indexOf(serviceUuid) >= 0) { serviceUuidMatched = true; } return serviceUuid; }); if (!serviceUuidMatched) { debug(' Ignoring advertisement that did not pass service UUID filter'); return; } } let connectable; switch (e.advertisementType) { case BluetoothLEAdvertisementType.connectableUndirected: case BluetoothLEAdvertisementType.connectableDirected: connectable = true; break; case BluetoothLEAdvertisementType.nonConnectableUndirected: case BluetoothLEAdvertisementType.scannableUndirected: connectable = false; break; default: connectable = undefined; break; } // Random addresses have the two most-significant bits set of the 48-bit address. let addressType = (e.bluetoothAddress >= (3 * Math.pow(2, 46)) ? 'random' : 'public'); debug(' address type: %s', addressType); let dataSections = rt.toArray(e.advertisement.dataSections); let serviceDataEntry = {}; dataSections.forEach(dataSection => { debug(' data section: %s', (getEnumName(BluetoothLEAdvertisementDataTypes, dataSection.dataType) || dataSection.dataType)); // https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile switch (dataSection.dataType) { case BluetoothLEAdvertisementDataTypes.completeService16BitUuids: var id = Buffer.allocUnsafe(2); var buf = rt.toBuffer(dataSection.data); id[0] = buf[1]; id[1] = buf[0]; serviceDataEntry.uuid = id.toString('hex'); break; case BluetoothLEAdvertisementDataTypes.completeService128BitUuids: serviceDataEntry.uuid = rt.toBuffer(dataSection.data).toString('hex'); break; case BluetoothLEAdvertisementDataTypes.serviceData16BitUuids: serviceDataEntry.data = rt.toBuffer(dataSection.data).slice(2); break; default: break; } }); if (typeof e.advertisement.flags === 'number') { debug(' advertisement flags: %s', e.advertisement.flags); } let txPowerLevel = null; let txPowerDataSection = dataSections.find( ds => ds.dataType === BluetoothLEAdvertisementDataTypes.txPowerLevel); if (txPowerDataSection) { let dataReader = DataReader.fromBuffer(txPowerDataSection.data); txPowerLevel = dataReader.readByte(); if (txPowerLevel >= 128) txPowerLevel -= 256; dataReader.close(); } let deviceRecord = this._deviceMap[deviceUuid]; if (!deviceRecord) { deviceRecord = { name: null, address: e.bluetoothAddress, formattedAddress: address, addressType: addressType, connectable: connectable, serviceUuids: serviceUuids, txPowerLevel: null, device: null, serviceMap: {}, characteristicMap: {}, descriptorMap: {}, }; this._deviceMap[deviceUuid] = deviceRecord; } if (e.advertisement.localName) { deviceRecord.name = e.advertisement.localName; } var manufacturerSections = e.advertisement.manufacturerData; if (manufacturerSections.size > 0) { var manufacturerData = manufacturerSections[0]; deviceRecord.manufacturerData = rt.toBuffer(manufacturerData.data); let companyIdHex = manufacturerData.companyId.toString(16); let toAppend = Buffer.allocUnsafe(2); toAppend.writeUInt16LE(manufacturerData.companyId); deviceRecord.manufacturerData = Buffer.concat([toAppend, deviceRecord.manufacturerData]); debug(' manufacturer data: %s', deviceRecord.manufacturerData.toString('hex')); } if (txPowerLevel) { deviceRecord.txPowerLevel = txPowerLevel; } // If only responding to scan responses, wait until the response to the active query before emitting a 'discover' event. if (!this._acceptOnlyScanResponse || isScanResponse) { let advertisement = { localName: deviceRecord.name, txPowerLevel: deviceRecord.txPowerLevel, manufacturerData: deviceRecord.manufacturerData, // TODO: manufacturerData serviceUuids: deviceRecord.serviceUuids, serviceData: (serviceDataEntry ? [serviceDataEntry] : []), }; let rssi = e.rawSignalStrengthInDBm; debug('discover: %s [%s]', deviceUuid, advertisement.serviceUuids.join()); this.emit( 'discover', deviceUuid, address, deviceRecord.addressType, deviceRecord.connectable, advertisement, rssi); } }; NobleBindings.prototype._onAdvertisementWatcherStopped = function (sender, e) { if (this._advertisementWatcher.status === BluetoothLEAdvertisementWatcherStatus.aborted) { debug('watcher aborted'); } else if (this._advertisementWatcher.status === BluetoothLEAdvertisementWatcherStatus.stopped) { debug('watcher stopped'); } else { debug('watcher stopped with unexpected status: %s', this._advertisementWatcher.status); } this.emit('scanStop'); }; NobleBindings.prototype._onConnectionStatusChanged = function (sender, e) { const deviceUuid = sender.bluetoothAddress.toString(16); const deviceRecord = this._deviceMap[deviceUuid]; if (deviceRecord) { const connectionStatus = sender.connectionStatus; debug('connection status changed: ' + deviceUuid + ' ' + getEnumName(BluetoothConnectionStatus, connectionStatus)); if (connectionStatus === BluetoothConnectionStatus.connected) { // A 'connect' event was already emitted when the device object was obtained. // Windows does not provide an explicit connect API; it will automatically connect // whenever an operation is requested on the device that requires a connection. } else if (connectionStatus === BluetoothConnectionStatus.disconnected) { this.disconnect(deviceUuid); } } }; NobleBindings.prototype._getCachedServiceAsync = function (deviceUuid, serviceUuid) { let deviceRecord = this._deviceMap[deviceUuid]; if (!deviceRecord) { throw new Error('Invalid or unknown device UUID: ' + deviceUuid); } let service = deviceRecord.serviceMap[serviceUuid]; if (service) { return Promise.resolve(service); } let device = deviceRecord.device; if (!device) { throw new Error('Device is not connected. UUID: ' + deviceUuid); } return rt.promisify(device.getGattServicesAsync, device)( BluetoothCacheMode.cached).then(result => { checkCommunicationResult(deviceUuid, result); service = rt.trackDisposables(deviceUuid, rt.toArray(result.services)) .find(s => uuidToString(s.uuid) === serviceUuid); if (!service) { throw new Error('Service ' + serviceUuid + ' not found for device ' + deviceUuid); } deviceRecord.serviceMap[serviceUuid] = service; return service; }); }; NobleBindings.prototype._getCachedCharacteristicAsync = function (deviceUuid, serviceUuid, characteristicUuid) { let deviceRecord = this._deviceMap[deviceUuid]; if (!deviceRecord) { throw new Error('Invalid or unknown device UUID: ' + deviceUuid); } let characteristicKey = serviceUuid + '/' + characteristicUuid; let characteristic = deviceRecord.characteristicMap[characteristicKey]; if (characteristic) { return Promise.resolve(characteristic); } return this._getCachedServiceAsync(deviceUuid, serviceUuid).then(service => { return rt.promisify(service.getCharacteristicsAsync, service)( BluetoothCacheMode.cached).then(result => { checkCommunicationResult(deviceUuid, result); characteristic = rt.toArray(result.characteristics) .find(c => uuidToString(c.uuid) === characteristicUuid); if (!characteristic) { throw new Error('Service ' + serviceUuid + ' characteristic ' + characteristicUuid + ' not found for device ' + deviceUuid); } deviceRecord.characteristicMap[characteristicKey] = characteristic; return characteristic; }); }); }; NobleBindings.prototype._getCachedDescriptorAsync = function (deviceUuid, serviceUuid, characteristicUuid, descriptorUuid) { let deviceRecord = this._deviceMap[deviceUuid]; if (!deviceRecord) { throw new Error('Invalid or unknown device UUID: ' + deviceUuid); } let descriptorKey = serviceUuid + '/' + characteristicUuid + '/' + descriptorUuid; let descriptor = deviceRecord.descriptorMap[descriptorKey]; if (descriptor) { return Promise.resolve(descriptor); } return this._getCachedCharacteristicAsync( deviceUuid, serviceUuid, characteristicUuid).then(service => { return rt.promisify(characteristic.getDescriptorsAsync, characteristic)( BluetoothCacheMode.cached).then(result => { checkCommunicationResult(deviceUuid, result); descriptor = rt.toArray(result.descriptors) .find(d => uuidToString(d.uuid) === descriptorUuid); if (!descriptor) { throw new Error('Service ' + serviceUuid + ' characteristic ' + characteristicUuid + ' descriptor ' + descriptorUuid + ' not found for device ' + deviceUuid); } deviceRecord.descriptorMap[descriptorKey] = descriptor; return descriptor; }); }); }; function formatBluetoothAddress(address) { if (!address) { return 'null'; } let formattedAddress = address.toString(16); while (formattedAddress.length < 12) { formattedAddress = '0' + formattedAddress; } formattedAddress = formattedAddress.substr(0, 2) + ':' + formattedAddress.substr(2, 2) + ':' + formattedAddress.substr(4, 2) + ':' + formattedAddress.substr(6, 2) + ':' + formattedAddress.substr(8, 2) + ':' + formattedAddress.substr(10, 2); return formattedAddress; } function characteristicPropertiesToStrings(props) { let strings = []; if (props & GattCharacteristicProperties.broadcast) { strings.push('broadcast'); } if (props & GattCharacteristicProperties.read) { strings.push('read'); } if (props & GattCharacteristicProperties.writeWithoutResponse) { strings.push('writeWithoutResponse'); } if (props & GattCharacteristicProperties.write) { strings.push('write'); } if (props & GattCharacteristicProperties.notify) { strings.push('notify'); } if (props & GattCharacteristicProperties.indicate) { strings.push('indicate'); } if (props & GattCharacteristicProperties.broadcast) { strings.push('authenticatedSignedWrites'); } if (props & GattCharacteristicProperties.extendedProperties) { strings.push('extendedProperties'); } return strings; } function getEnumName(enumType, value) { return Object.keys(enumType).find(enumName => value === enumType[enumName]); } function stringToUuid(uuid) { if (typeof (uuid) === 'number') { return BluetoothUuidHelper.fromShortId(uuid).replace(/[{}]/g, ''); } else if (uuid && uuid.length === 4) { return BluetoothUuidHelper.fromShortId(parseInt(uuid, 16)).replace(/[{}]/g, ''); } else { return uuid; } } function uuidToString(uuid) { if (!uuid) { return uuid; } uuid = uuid.toString().replace(/[{}]/g, ''); let shortId = BluetoothUuidHelper.tryGetShortId(uuid); if (shortId) { return uint32ToHexString(shortId); } else { return uuid.replace(/-/g, '').toLowerCase(); } } function uint32ToHexString(n) { return (n + 0x10000).toString(16).substr(-4); } function filterUuids(filter) { return (uuid) => { return !filter || filter.indexOf(uuid) != -1; }; } function checkCommunicationResult(deviceUuid, result) { if (result.status === GattCommunicationStatus.unreachable) { throw new Error('Device unreachable: ' + deviceUuid); } else if (result.status === GattCommunicationStatus.protocolError) { throw new Error('Protocol error communicating with device: ' + deviceUuid); } } module.exports = new NobleBindings();