/* Copyright (c) 2010 - 2019, Nordic Semiconductor ASA * * All rights reserved. * * Use in source and binary forms, redistribution in binary form only, with * or without modification, are permitted provided that the following conditions * are met: * * 1. Redistributions in binary form, except as embedded into a Nordic * Semiconductor ASA integrated circuit in a product or a software update for * such product, must reproduce the above copyright notice, this list of * conditions and the following disclaimer in the documentation and/or other * materials provided with the distribution. * * 2. Neither the name of Nordic Semiconductor ASA nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * 3. This software, with or without modification, must only be used with a Nordic * Semiconductor ASA integrated circuit. * * 4. Any software provided in binary form under this license must not be reverse * engineered, decompiled, modified and/or disassembled. * * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 'use strict'; const EventEmitter = require('events'); const _ = require('underscore'); const AdapterState = require('./adapterState'); const Device = require('./device'); const Service = require('./service'); const Characteristic = require('./characteristic'); const Descriptor = require('./descriptor'); const AdType = require('./util/adType'); const Converter = require('./util/sdConv'); const ToText = require('./util/toText'); const logLevel = require('./util/logLevel'); const Security = require('./security'); const HexConv = require('./util/hexConv'); const MAX_SUPPORTED_ATT_MTU = 247; /** Class to mediate error conditions. */ class Error { /** * Create an error object. * * @constructor * @param {string} userMessage The message to display to the user. * @param {string} description A detailed description of the error. */ constructor(userMessage, description) { this.message = userMessage; this.description = description; } } const _makeError = function (userMessage, description) { return new Error(userMessage, description); }; /** * Class representing a transport adapter (SoftDevice RPC module). * * @fires Adapter#advertiseTimedOut * @fires Adapter#attMtuChanged * @fires Adapter#authKeyRequest * @fires Adapter#authStatus * @fires Adapter#characteristicAdded * @fires Adapter#characteristicValueChanged * @fires Adapter#closed * @fires Adapter#connectTimedOut * @fires Adapter#connParamUpdate * @fires Adapter#connParamUpdateRequest * @fires Adapter#connSecUpdate * @fires Adapter#dataLengthChanged * @fires Adapter#descriptorAdded * @fires Adapter#descriptorValueChanged * @fires Adapter#deviceConnected * @fires Adapter#deviceDisconnected * @fires Adapter#deviceDiscovered * @fires Adapter#deviceNotifiedOrIndicated * @fires Adapter#error * @fires Adapter#keyPressed * @fires Adapter#lescDhkeyRequest * @fires Adapter#logMessage * @fires Adapter#opened * @fires Adapter#passkeyDisplay * @fires Adapter#scanTimedOut * @fires Adapter#secInfoRequest * @fires Adapter#secParamsRequest * @fires Adapter#securityChanged * @fires Adapter#securityRequest * @fires Adapter#securityRequestTimedOut * @fires Adapter#serviceAdded * @fires Adapter#stateChanged * @fires Adapter#status * @fires Adapter#txComplete * @fires Adapter#warning */ class Adapter extends EventEmitter { /** * @summary Create an object representing an adapter. * * This constructor is called by `AdapterFactory` and it should not be necessary for the developer to call directly. * * @constructor * @param {Object} bleDriver The driver to use for getting constants from the pc-ble-driver-js AddOn. * @param {Object} adapter The adapter to use. The adapter is an object received from the pc-ble-driver-js AddOn. * @param {string} instanceId The unique Id that identifies this Adapter instance. * @param {string} port The port this adapter uses. For example it can be 'COM1', '/dev/ttyUSB0' or similar. * @param {string} [serialNumber] The serial number of hardware device this adapter is controlling via serial. * @param {string} [notSupportedMessage] Message displayed to developer if this adapter is not supported on platform. * */ constructor(bleDriver, adapter, instanceId, port, serialNumber, notSupportedMessage) { super(); if (bleDriver === undefined) throw new Error('Missing argument bleDriver.'); if (adapter === undefined) throw new Error('Missing argument adapter.'); if (instanceId === undefined) throw new Error('Missing argument instanceId.'); if (port === undefined) throw new Error('Missing argument port.'); this._bleDriver = bleDriver; this._adapter = adapter; this._instanceId = instanceId; this._state = new AdapterState(instanceId, port, serialNumber); this._security = new Security(this._bleDriver); this._notSupportedMessage = notSupportedMessage; this._keys = null; this._attMtuMap = {}; this._init(); } _init() { this._devices = {}; this._services = {}; this._characteristics = {}; this._descriptors = {}; this._converter = new Converter(this._bleDriver, this._adapter); this._gapOperationsMap = {}; this._gattOperationsMap = {}; this._preparedWritesMap = {}; this._pendingNotificationsAndIndications = {}; } _getServiceType(service) { let type; if (service.type) { if (service.type === 'primary') { type = this._bleDriver.BLE_GATTS_SRVC_TYPE_PRIMARY; } else if (service.type === 'secondary') { type = this._bleDriver.BLE_GATTS_SRVC_TYPE_SECONDARY; } else { throw new Error(`Service type ${service.type} is unknown to me. Must be 'primary' or 'secondary'.`); } } else { throw new Error('Service type is not specified. Must be \'primary\' or \'secondary\'.'); } return type; } /** * Get the instanceId of this adapter. * @returns {string} Unique Id of this adapter. */ get instanceId() { return this._instanceId; } /** * Get the state of this adapter. @ref: ./adapterState.js * @returns {AdapterState} `AdapterState` store object of this adapter. */ get state() { return this._state; } /** * Get the driver of this adapter. * @returns {Object} The pc-ble-driver to use for this adapter, from the pc-ble-driver-js add-on. */ get driver() { return this._bleDriver; } /** * Get the `notSupportedMessage` of this adapter. * @returns {string} The error message thrown if this adapter is not supported on the platform/hardware. */ get notSupportedMessage() { return this._notSupportedMessage; } _maxReadPayloadSize(deviceInstanceId) { return this.getCurrentAttMtu(deviceInstanceId) - 1; } _maxShortWritePayloadSize(deviceInstanceId) { return this.getCurrentAttMtu(deviceInstanceId) - 3; } _maxLongWritePayloadSize(deviceInstanceId) { return this.getCurrentAttMtu(deviceInstanceId) - 5; } _generateKeyPair() { if (this._keys === null) { this._keys = this._security.generateKeyPair(); } } /** * Compute shared secret. * * @param {string} [peerPublicKey] Peer public key. * @returns {string} The computed shared secret generated from this adapter's key-pair. */ computeSharedSecret(peerPublicKey) { this._generateKeyPair(); let publicKey = peerPublicKey; if (publicKey === null || publicKey === undefined) { publicKey = this._keys; } return this._security.generateSharedSecret(this._keys.sk, publicKey.pk).ss; } /** * Compute public key. * * @returns {string} The public key generated from this adapter's key-pair. */ computePublicKey() { this._generateKeyPair(); return this._security.generatePublicKey(this._keys.sk).pk; } /** * Deletes any previously generated key-pair. * * The next time `computeSharedSecret` or `computePublicKey` is invoked, a new key-pair will be generated and used. * @returns {void} */ deleteKeys() { this._keys = null; } _checkAndPropagateError(err, userMessage, callback) { if (err) { this._emitError(err, userMessage); if (callback) callback(err); return true; } return false; } _emitError(err, userMessage) { const error = new Error(userMessage, err); /** * Error event. * * @event Adapter#error * @type {Object} * @property {Error} error - Provides information related to an error that occurred. */ this.emit('error', error); } _changeState(changingStates, swallowEmit) { let changed = false; for (const state in changingStates) { const newValue = changingStates[state]; const previousValue = this._state[state]; // Use isEqual to compare objects if (!_.isEqual(previousValue, newValue)) { this._state[state] = newValue; changed = true; } } if (swallowEmit) { return; } if (changed) { /** * Adapter state changed event. * * @event Adapter#stateChanged * @type {Object} * @property {AdapterState} this._state - The updated adapter's state store. */ this.emit('stateChanged', this._state); } } _getDefaultEnableBLEParams() { if (this._bleDriver.NRF_SD_BLE_API_VERSION === 2) { return { gap_enable_params: { periph_conn_count: 1, central_conn_count: 7, central_sec_count: 1, }, gatts_enable_params: { service_changed: false, attr_tab_size: this._bleDriver.BLE_GATTS_ATTR_TAB_SIZE_DEFAULT, }, common_enable_params: { conn_bw_counts: null, // tell SD to use default vs_uuid_count: 10, }, gatt_enable_params: { att_mtu: MAX_SUPPORTED_ATT_MTU, }, }; } return { conn_cfg: { gap_conn_cfg: { conn_count: 8, // event_length, }, gatt_conn_cfg: { att_mtu: MAX_SUPPORTED_ATT_MTU, }, // gattc_conn_cfg: { // write_cmd_tx_queue_size, // }, // gatts_conn_cfg: { // hvn_tx_queue_size, // }, // l2cap_conn_cfg: { // rx_mps, // tx_mps, // rx_queue_size, // tx_queue_size, // ch_count, // }, }, common_cfg: { vs_uuid_cfg: { vs_uuid_count: 10, }, }, gap_cfg: { role_count_cfg: { periph_role_count: 1, central_role_count: 7, central_sec_count: 1, }, }, gatts_cfg: { service_changed: { service_changed: false, }, attr_tab_size: { attr_tab_size: this._bleDriver.BLE_GATTS_ATTR_TAB_SIZE_DEFAULT, }, }, }; } /** * @summary Initialize the adapter. * * The serial port will be attempted to be opened with the configured serial port settings in * adapterOptions. * * @param {Object} options Options to initialize/open this adapter with. * Available adapter open options: * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ open(options, callback) { if (this.state.opening || this.state.available) { callback(_makeError('Adapter is already open.')); return; } if (this.notSupportedMessage !== undefined) { const error = new Error(this.notSupportedMessage); /** * Warning event. * * @event Adapter#warning * @type {Object} * @property {Error} error - A non fatal Error. */ this.emit('warning', error); } if (!options) { options = { baudRate: 1000000, parity: 'none', flowControl: 'none', eventInterval: 0, logLevel: 'info', retransmissionInterval: 250, responseTimeout: 1500, enableBLE: true, }; } else { if (!options.baudRate) options.baudRate = 1000000; if (!options.parity) options.parity = 'none'; if (!options.flowControl) options.flowControl = 'none'; if (!options.eventInterval) options.eventInterval = 0; if (!options.logLevel) options.logLevel = 'info'; if (!options.retransmissionInterval) options.retransmissionInterval = 250; if (!options.responseTimeout) options.responseTimeout = 1500; if (options.enableBLE === undefined) options.enableBLE = true; } this._changeState({ opening: true, baudRate: options.baudRate, parity: options.parity, flowControl: options.flowControl, }); options.logCallback = this._logCallback.bind(this); options.eventCallback = this._eventCallback.bind(this); options.statusCallback = this._statusCallback.bind(this); options.enableBLEParams = options.enableBLEParams || this._getDefaultEnableBLEParams(); this._adapter.open(this._state.port, options, err => { this._changeState({ opening: false }); if (this._checkAndPropagateError(err, 'Error occurred opening serial port.', callback)) { return; } this._changeState({ available: true }); /** * Adapter opened event. * * @event Adapter#opened * @type {Object} * @property {Adapter} this - An instance of the opened Adapter. */ this.emit('opened', this); if (options.enableBLE) { this._changeState({ bleEnabled: true }); this.getState(getStateError => { this._checkAndPropagateError(getStateError, 'Error retrieving adapter state.', callback); }); } if (callback) { callback(); } }); } /** * @summary Close the adapter. * * This function will close the serial port, release allocated resources and remove event listeners. * Before closing, a reset command is issued to set the connectivity device to idle state. * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ close(callback) { if (!this._state.available) { if (callback) callback(); return; } this.connReset(err => { if (err) { this.emit('logMessage', logLevel.DEBUG, `Failed to issue connectivity reset: ${err.message}. Proceeding with close.`); } this._changeState({ available: false, bleEnabled: false, }); this._adapter.close(error => { /** * Adapter closed event. * * @event Adapter#closed * @type {Object} * @property {Adapter} this - An instance of the closed Adapter. */ this.emit('closed', this); if (callback) callback(error); }); }); } /** * @summary Reset the connectivity device * * This function will issue a reset command to the connectivity device. * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ connReset(callback) { if (!this.state.available) { if (callback) callback(_makeError('The adapter is not available.')); return; } this._adapter.connReset(error => { if (callback) callback(error); }); } /** * This function is for debugging purposes. It will return an object with these members: * * * @returns {Object} This adapters stats. */ getStats() { return this._adapter.getStats(); } /** * @summary Enable the BLE stack. * * This call initializes the BLE stack, no other BLE related function can be called before this one. * * @param {Object} [options] BLE Initialization parameters. If `undefined` or `null` the BLE stack will be * initialized with default options (see code for `enableBLE()` below for default values). * Available BLE enable parameters: * * @param {function(Error)} [callback] Callback signature: (err) => {} * @returns {void} */ enableBLE(options, callback) { if (options === undefined || options === null) { options = this._getDefaultEnableBLEParams(); } this._adapter.enableBLE( options, err => { if (this._checkAndPropagateError(err, 'Enabling BLE failed.', callback)) { return; } this._changeState({ bleEnabled: true }); if (callback) { callback(); } }); } setBleConfig(ble_cfg, callback) { const { conn_cfg, common_cfg, gap_cfg, gatts_cfg } = ble_cfg; (async () => { const setOneCfg = (configId, cfg) => new Promise((resolve, reject) => { this._adapter.setBleConfig(this._bleDriver[configId], cfg, err => ( this._checkAndPropagateError(err, `Set BLE config ${configId} failed.`, reject) || resolve() )); }); if (conn_cfg !== undefined) { const { gap_conn_cfg, gattc_conn_cfg, gatts_conn_cfg, gatt_conn_cfg, l2cap_conn_cfg } = conn_cfg; if (gap_conn_cfg !== undefined) { await setOneCfg('BLE_CONN_CFG_GAP', { conn_cfg: { gap_conn_cfg } }); } if (gattc_conn_cfg !== undefined) { await setOneCfg('BLE_CONN_CFG_GATTC', { conn_cfg: { gattc_conn_cfg } }); } if (gatts_conn_cfg !== undefined) { await setOneCfg('BLE_CONN_CFG_GATTS', { conn_cfg: { gatts_conn_cfg } }); } if (gatt_conn_cfg !== undefined) { await setOneCfg('BLE_CONN_CFG_GATT', { conn_cfg: { gatt_conn_cfg } }); } if (l2cap_conn_cfg !== undefined) { await setOneCfg('BLE_CONN_CFG_L2CAP', { conn_cfg: { l2cap_conn_cfg } }); } } if (common_cfg !== undefined) { const { vs_uuid_cfg } = common_cfg; if (vs_uuid_cfg) { await setOneCfg('BLE_COMMON_CFG_VS_UUID', { common_cfg: { vs_uuid_cfg } }); } } if (gap_cfg !== undefined) { const { role_count_cfg, device_name } = gap_cfg; if (role_count_cfg !== undefined) { await setOneCfg('BLE_GAP_CFG_ROLE_COUNT', { gap_cfg: { role_count_cfg } }); } if (device_name !== undefined) { await setOneCfg('BLE_GAP_CFG_DEVICE_NAME', { gap_cfg: { device_name } }); } } if (gatts_cfg !== undefined) { const { service_changed, attr_tab_size } = gatts_cfg; if (service_changed !== undefined) { await setOneCfg('BLE_GATTS_CFG_SERVICE_CHANGED', { gatts_cfg: { service_changed } }); } if (attr_tab_size !== undefined) { await setOneCfg('BLE_GATTS_CFG_ATTR_TAB_SIZE', { gatts_cfg: { attr_tab_size } }); } } })().then(callback); } _statusCallback(status) { switch (status.id) { case this._bleDriver.RESET_PERFORMED: this._init(); this._changeState( { available: false, bleEnabled: false, connecting: false, scanning: false, advertising: false, } ); break; case this._bleDriver.CONNECTION_ACTIVE: this._changeState( { available: true, } ); break; } /** * Status event. * * @event Adapter#status * @type {Object} * @property {string} status - Human-readable status message. */ this.emit('status', status); } _logCallback(severity, message) { /** * Log message event. * * @event Adapter#logMessage * @type {Object} * @property {string} severity - Severity of the log event. * @property {string} message - Human-readable log message. */ this.emit('logMessage', severity, message); } _eventCallback(eventArray) { eventArray.forEach(event => { const text = new ToText(event); // TODO: set the correct level for different types of events: this.emit('logMessage', logLevel.DEBUG, text.toString()); switch (event.id) { case this._bleDriver.BLE_GAP_EVT_CONNECTED: this._parseConnectedEvent(event); break; case this._bleDriver.BLE_GAP_EVT_DISCONNECTED: this._parseDisconnectedEvent(event); break; case this._bleDriver.BLE_GAP_EVT_CONN_PARAM_UPDATE: this._parseConnectionParameterUpdateEvent(event); break; case this._bleDriver.BLE_GAP_EVT_SEC_REQUEST: this._parseGapSecurityRequestEvent(event); break; case this._bleDriver.BLE_GAP_EVT_SEC_PARAMS_REQUEST: this._parseSecParamsRequestEvent(event); break; case this._bleDriver.BLE_GAP_EVT_CONN_SEC_UPDATE: this._parseConnSecUpdateEvent(event); break; case this._bleDriver.BLE_GAP_EVT_AUTH_STATUS: this._parseAuthStatusEvent(event); break; case this._bleDriver.BLE_GAP_EVT_PASSKEY_DISPLAY: this._parsePasskeyDisplayEvent(event); break; case this._bleDriver.BLE_GAP_EVT_AUTH_KEY_REQUEST: this._parseAuthKeyRequest(event); break; case this._bleDriver.BLE_GAP_EVT_KEY_PRESSED: this._parseGapKeyPressedEvent(event); break; case this._bleDriver.BLE_GAP_EVT_LESC_DHKEY_REQUEST: this._parseLescDhkeyRequest(event); break; case this._bleDriver.BLE_GAP_EVT_SEC_INFO_REQUEST: this._parseSecInfoRequest(event); break; case this._bleDriver.BLE_GAP_EVT_TIMEOUT: this._parseGapTimeoutEvent(event); break; case this._bleDriver.BLE_GAP_EVT_RSSI_CHANGED: this._parseGapRssiChangedEvent(event); break; case this._bleDriver.BLE_GAP_EVT_ADV_REPORT: this._parseGapAdvertismentReportEvent(event); break; case this._bleDriver.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST: this._parseGapConnectionParameterUpdateRequestEvent(event); break; case this._bleDriver.BLE_GAP_EVT_SCAN_REQ_REPORT: // Not needed. Received when a scan request is received. break; case this._bleDriver.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST: this._parseGapDataLengthUpdateRequestEvent(event); break; case this._bleDriver.BLE_GAP_EVT_DATA_LENGTH_UPDATE: this._parseGapDataLengthUpdateEvent(event); break; case this._bleDriver.BLE_GAP_EVT_PHY_UPDATE_REQUEST: this._parseGapPhyUpdateRequestEvent(event); break; case this._bleDriver.BLE_GAP_EVT_PHY_UPDATE: this._parseGapPhyUpdateEvent(event); break; case this._bleDriver.BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP: this._parseGattcPrimaryServiceDiscoveryResponseEvent(event); break; case this._bleDriver.BLE_GATTC_EVT_REL_DISC_RSP: // Not needed. Used for included services discovery. break; case this._bleDriver.BLE_GATTC_EVT_CHAR_DISC_RSP: this._parseGattcCharacteristicDiscoveryResponseEvent(event); break; case this._bleDriver.BLE_GATTC_EVT_DESC_DISC_RSP: this._parseGattcDescriptorDiscoveryResponseEvent(event); break; case this._bleDriver.BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP: // Not needed, service discovery is not using the related function. break; case this._bleDriver.BLE_GATTC_EVT_READ_RSP: this._parseGattcReadResponseEvent(event); break; case this._bleDriver.BLE_GATTC_EVT_CHAR_VALS_READ_RSP: // Not needed, characteristic discovery is not using the related function. break; case this._bleDriver.BLE_GATTC_EVT_WRITE_RSP: this._parseGattcWriteResponseEvent(event); break; case this._bleDriver.BLE_GATTC_EVT_HVX: this._parseGattcHvxEvent(event); break; case this._bleDriver.BLE_GATTC_EVT_TIMEOUT: this._parseGattTimeoutEvent(event); break; case this._bleDriver.BLE_GATTC_EVT_EXCHANGE_MTU_RSP: this._parseGattcExchangeMtuResponseEvent(event); break; case this._bleDriver.BLE_GATTS_EVT_WRITE: this._parseGattsWriteEvent(event); break; case this._bleDriver.BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST: this._parseGattsRWAutorizeRequestEvent(event); break; case this._bleDriver.BLE_GATTS_EVT_SYS_ATTR_MISSING: this._parseGattsSysAttrMissingEvent(event); break; case this._bleDriver.BLE_GATTS_EVT_HVC: this._parseGattsHvcEvent(event); break; case this._bleDriver.BLE_GATTS_EVT_SC_CONFIRM: // Not needed, service changed is not supported currently. break; case this._bleDriver.BLE_GATTS_EVT_TIMEOUT: this._parseGattTimeoutEvent(event); break; case this._bleDriver.BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST: this._parseGattsExchangeMtuRequestEvent(event); break; case this._bleDriver.BLE_EVT_USER_MEM_REQUEST: this._parseMemoryRequestEvent(event); break; case this._bleDriver.BLE_EVT_TX_COMPLETE: case this._bleDriver.BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE: case this._bleDriver.BLE_GATTS_EVT_HVN_TX_COMPLETE: this._parseTxCompleteEvent(event); break; default: this.emit('logMessage', logLevel.INFO, `Unsupported event received from SoftDevice: ${event.id} - ${event.name}`); break; } }); } _parseConnectedEvent(event) { // TODO: Update device with connection handle // TODO: Should 'deviceConnected' event emit the updated device? const deviceAddress = event.peer_addr; const connectionParameters = event.conn_params; let deviceRole; // If our role is central set the device role to be peripheral. if (event.role === 'BLE_GAP_ROLE_CENTRAL') { deviceRole = 'peripheral'; } else if (event.role === 'BLE_GAP_ROLE_PERIPH') { deviceRole = 'central'; } const device = new Device(deviceAddress, deviceRole); device.connectionHandle = event.conn_handle; device.minConnectionInterval = connectionParameters.min_conn_interval; device.maxConnectionInterval = connectionParameters.max_conn_interval; device.slaveLatency = connectionParameters.slave_latency; device.connectionSupervisionTimeout = connectionParameters.conn_sup_timeout; device.connected = true; this._devices[device.instanceId] = device; this._attMtuMap[device.instanceId] = this.driver.GATT_MTU_SIZE_DEFAULT || this.driver.BLE_GATT_ATT_MTU_DEFAULT; this._changeState({ connecting: false }); if (deviceRole === 'central') { this._changeState({ advertising: false }); } /** * Connection established. * * @event Adapter#deviceConnected * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we've connected to. */ this.emit('deviceConnected', device); this._addDeviceToAllPerConnectionValues(device.instanceId); if (deviceRole === 'peripheral') { const callback = this._gapOperationsMap.connecting.callback; delete this._gapOperationsMap.connecting; if (callback) { callback(undefined, device); } } } _parseDisconnectedEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); if (!device) { this._emitError('Internal inconsistency: Could not find device with connection handle ' + event.conn_handle, 'Disconnect failed'); const errorObject = _makeError('Disconnect failed', 'Internal inconsistency: Could not find device with connection handle ' + event.conn_handle); // cannot reach callback when there is no device. The best we can do is emit error and return. this.emit('error', errorObject); return; } device.connected = false; if (device.instanceId in this._attMtuMap) delete this._attMtuMap[device.instanceId]; // TODO: Delete all operations for this device. if (this._gapOperationsMap[device.instanceId]) { // TODO: How do we know what the callback expects? Check disconnected event reason? const callback = this._gapOperationsMap[device.instanceId].callback; delete this._gapOperationsMap[device.instanceId]; if (callback) { callback(undefined, device); } } if (this._gattOperationsMap[device.instanceId]) { const callback = this._gattOperationsMap[device.instanceId].callback; delete this._gattOperationsMap[device.instanceId]; if (callback) { callback(_makeError('Device disconnected', 'Device with address ' + device.address + ' disconnected')); } } delete this._devices[device.instanceId]; /** * Disconnected from peer. * * @event Adapter#deviceDisconnected * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we've disconnected * from. * @property {string} event.reason_name - Human-readable reason for disconnection. * @property {string} event.reason - HCI status code. */ this.emit('deviceDisconnected', device, event.reason_name, event.reason); this._clearDeviceFromAllPerConnectionValues(device.instanceId); this._clearDeviceFromDiscoveredServices(device.instanceId); } _parseConnectionParameterUpdateEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); if (!device) { this.emit('error', 'Internal inconsistency: Could not find device with connection handle ' + event.conn_handle); return; } device.minConnectionInterval = event.conn_params.min_conn_interval; device.maxConnectionInterval = event.conn_params.max_conn_interval; device.slaveLatency = event.conn_params.slave_latency; device.connectionSupervisionTimeout = event.conn_params.conn_sup_timeout; if (this._gapOperationsMap[device.instanceId]) { const callback = this._gapOperationsMap[device.instanceId].callback; delete this._gapOperationsMap[device.instanceId]; if (callback) { callback(undefined, device); } } const connectionParameters = { minConnectionInterval: event.conn_params.min_conn_interval, maxConnectionInterval: event.conn_params.max_conn_interval, slaveLatency: event.conn_params.slave_latency, connectionSupervisionTimeout: event.conn_params.conn_sup_timeout, }; /** * Connection parameter update event. * * @event Adapter#connParamUpdate * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} connectionParameters - The updated connection parameters. */ this.emit('connParamUpdate', device, connectionParameters); } _parseSecParamsRequestEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * Request to provide security parameters. * * @event Adapter#secParamsRequest * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} event.peer_params - Initiator Security Parameters. */ this.emit('secParamsRequest', device, event.peer_params); } _parseConnSecUpdateEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * Connection security updated. * * @event Adapter#connSecUpdate * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} event.conn_sec - Connection security level. */ this.emit('connSecUpdate', device, event.conn_sec); const authParamters = { securityMode: event.conn_sec.sec_mode.sm, securityLevel: event.conn_sec.sec_mode.lv, }; /** * Connection security updated. * * @event Adapter#securityChanged * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} authParamters - Connection security level. */ this.emit('securityChanged', device, authParamters); } _parseAuthStatusEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); device.ownPeriphInitiatedPairingPending = false; /** * Authentication procedure completed with status. * * @event Adapter#authStatus * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} _ - Authentication status and corresponding parameters. */ this.emit('authStatus', device, { auth_status: event.auth_status, auth_status_name: event.auth_status_name, error_src: event.error_src, error_src_name: event.error_src_name, bonded: event.bonded, sm1_levels: event.sm1_levels, sm2_levels: event.sm2_levels, kdist_own: event.kdist_own, kdist_peer: event.kdist_peer, keyset: event.keyset, } ); } _parsePasskeyDisplayEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * Request to display a passkey to the user. * * @event Adapter#passkeyDisplay * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {number} event.match_request - If 1 requires the application to report the match using replyAuthKey. * @property {string} event.passkey - 6 digit passkey in ASCII ('0'-'9' digits only). */ this.emit('passkeyDisplay', device, event.match_request, event.passkey); } _parseAuthKeyRequest(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * Request to provide an authentication key. * * @event Adapter#authKeyRequest * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {string} event.key_type - The GAP Authentication Key Types. */ this.emit('authKeyRequest', device, event.key_type); } _parseGapKeyPressedEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * Notify of a key press during an authentication procedure. * * @event Adapter#keyPressed * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {string} event.kp_not - The Key press notification type. */ this.emit('keyPressed', device, event.kp_not); } _parseLescDhkeyRequest(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * Request to calculate an LE Secure Connections DHKey. * * @event Adapter#lescDhkeyRequest * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} event.pk_peer - LE Secure Connections remote P-256 Public Key. * @property {Object} event.oobd_req - LESC OOB data required. A call to replyLescDhkey is * required to complete the procedure. */ this.emit('lescDhkeyRequest', device, event.pk_peer, event.oobd_req); } _parseSecInfoRequest(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * Request to provide security information. * * @event Adapter#secInfoRequest * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} event - Security Information Request Event Parameters */ this.emit('secInfoRequest', device, event); } _parseGapSecurityRequestEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * Security Request. * * @event Adapter#securityRequest * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} event - Security Request Event Parameters. */ this.emit('securityRequest', device, event); } _parseGapConnectionParameterUpdateRequestEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); const connectionParameters = { minConnectionInterval: event.conn_params.min_conn_interval, maxConnectionInterval: event.conn_params.max_conn_interval, slaveLatency: event.conn_params.slave_latency, connectionSupervisionTimeout: event.conn_params.conn_sup_timeout, }; /** * Connection Parameter Update Request. * * @event Adapter#connParamUpdateRequest * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} connectionParameters - GAP Connection Parameters. */ this.emit('connParamUpdateRequest', device, connectionParameters); } _parseGapDataLengthUpdateRequestEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * DataLength Update Request. * * @event Adapter#dataLengthUpdateRequest * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} event - DataLength Update Request Event Parameters. */ this.emit('dataLengthUpdateRequest', device, { max_rx_octets: event.peer_params.max_tx_octets, max_tx_octets: event.peer_params.max_rx_octets, }); } _parseGapDataLengthUpdateEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); const { effective_params: { max_rx_octets: rx, max_tx_octets: tx, }, } = event; device.dataLength = Math.min(rx, tx); /** * DataLength Update. * * @event Adapter#dataLengthUpdated * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} event - DataLength Update Event Parameters. */ this.emit('dataLengthUpdated', device, event); } _parseGapPhyUpdateRequestEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * PHY Update Request. * * @event Adapter#phyUpdateRequest * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} event - PHY Update Request Event Parameters. */ this.emit('phyUpdateRequest', device, { tx_phys: event.peer_preferred_phys.rx_phys, rx_phys: event.peer_preferred_phys.tx_phys, }); } _parseGapPhyUpdateEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); device.rxPhy = event.rx_phy; device.txPhy = event.tx_phy; /** * PHY Update. * * @event Adapter#phyUpdated * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} event - PHY Update Event Parameters. */ this.emit('phyUpdated', device, event); } _parseGapAdvertismentReportEvent(event) { const address = event.peer_addr; const discoveredDevice = new Device(address, 'peripheral'); discoveredDevice.processEventData(event); /** * Discovered a peripheral BLE device. * * @event Adapter#deviceDiscovered * @type {Object} * @property {Device} discoveredDevice - The Device instance representing the BLE peer we've * discovered. */ this.emit('deviceDiscovered', discoveredDevice); } _parseGapTimeoutEvent(event) { switch (event.src) { case this._bleDriver.BLE_GAP_TIMEOUT_SRC_ADVERTISING: this._changeState({ advertising: false }); /** * BLE peripheral timed out advertising. * * @event Adapter#advertiseTimedOut * @type {Object} */ this.emit('advertiseTimedOut'); break; case this._bleDriver.BLE_GAP_TIMEOUT_SRC_SCAN: this._changeState({ scanning: false }); /** * BLE central timed out scanning. * * @event Adapter#scanTimedOut * @type {Object} */ this.emit('scanTimedOut'); break; case this._bleDriver.BLE_GAP_TIMEOUT_SRC_CONN: const deviceAddress = this._gapOperationsMap.connecting.deviceAddress; const errorObject = _makeError('Connect timed out.', deviceAddress); const connectingCallback = this._gapOperationsMap.connecting.callback; if (connectingCallback) connectingCallback(errorObject); delete this._gapOperationsMap.connecting; this._changeState({ connecting: false }); /** * BLE peer timed out in connection. * * @event Adapter#connectTimedOut * @type {Object} * @property {Device} deviceAddress - The device address of the BLE peer our connection timed-out with. */ this.emit('connectTimedOut', deviceAddress); break; case this._bleDriver.BLE_GAP_TIMEOUT_SRC_SECURITY_REQUEST: const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * BLE peer timed out while waiting for a security request response. * * @event Adapter#securityRequestTimedOut * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. */ this.emit('securityRequestTimedOut', device); this.emit('error', _makeError('Security request timed out.')); break; default: this.emit('logMessage', logLevel.DEBUG, `GAP operation timed out: ${event.src_name} (${event.src}).`); } } _parseGapRssiChangedEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); device.rssi = event.rssi; // TODO: How do we notify the application of a changed rssi? //emit('rssiChanged', device); } _parseGattcPrimaryServiceDiscoveryResponseEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); const services = event.services; const gattOperation = this._gattOperationsMap[device.instanceId]; const finishServiceDiscovery = () => { if (_.isEmpty(gattOperation.pendingHandleReads)) { // No pending reads to wait for. const callbackServices = []; for (let serviceInstanceId in this._services) { const service = this._services[serviceInstanceId]; if (service.deviceInstanceId === gattOperation.parent.instanceId) { callbackServices.push(this._services[serviceInstanceId]); } } delete this._gattOperationsMap[device.instanceId]; gattOperation.callback(undefined, callbackServices); } else { for (let handle in gattOperation.pendingHandleReads) { // Just take the first found handle and start the read process. const handleAsNumber = parseInt(handle, 10); this._adapter.gattcRead(device.connectionHandle, handleAsNumber, 0, err => { if (err) { this.emit('error', err); gattOperation.callback(_makeError('Error reading attributes', err)); } }); break; } } }; if (event.count === 0) { finishServiceDiscovery(); return; } services.forEach(service => { const handle = service.handle_range.start_handle; let uuid = HexConv.numberTo16BitUuid(service.uuid.uuid); if (service.uuid.type >= this._bleDriver.BLE_UUID_TYPE_VENDOR_BEGIN) { uuid = this._converter.lookupVsUuid(service.uuid); } else if (service.uuid.type === this._bleDriver.BLE_UUID_TYPE_UNKNOWN) { uuid = null; } const newService = new Service(device.instanceId, uuid); newService.startHandle = service.handle_range.start_handle; newService.endHandle = service.handle_range.end_handle; this._services[newService.instanceId] = newService; if (uuid === null) { gattOperation.pendingHandleReads[handle] = newService; } else { /** * Service was successfully added to the Adapter's GATT attribute table. * * @event Adapter#serviceAdded * @type {Object} * @property {Service} newService - The new added service. */ this.emit('serviceAdded', newService); } }); const nextStartHandle = services[services.length - 1].handle_range.end_handle + 1; if (nextStartHandle > 0xFFFF) { finishServiceDiscovery(); return; } this._adapter.gattcDiscoverPrimaryServices(device.connectionHandle, nextStartHandle, null, err => { this._checkAndPropagateError(err, 'Failed to get services', gattOperation.callback); }); } _parseGattcCharacteristicDiscoveryResponseEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); const characteristics = event.chars; const gattOperation = this._gattOperationsMap[device.instanceId]; const finishCharacteristicDiscovery = () => { if (_.isEmpty(gattOperation.pendingHandleReads)) { // No pending reads to wait for. const callbackCharacteristics = []; for (let characteristicInstanceId in this._characteristics) { const characteristic = this._characteristics[characteristicInstanceId]; if (characteristic.serviceInstanceId === gattOperation.parent.instanceId) { callbackCharacteristics.push(characteristic); } } delete this._gattOperationsMap[device.instanceId]; gattOperation.callback(undefined, callbackCharacteristics); } else { for (let handle in gattOperation.pendingHandleReads) { // Only take the first found handle and start the read process. const handleAsNumber = parseInt(handle, 10); this._adapter.gattcRead(device.connectionHandle, handleAsNumber, 0, err => { if (this._checkAndPropagateError(err, `Failed to get characteristic with handle ${handleAsNumber}`, gattOperation.callback)) { return; } }); break; } } }; if (event.count === 0) { finishCharacteristicDiscovery(); return; } // We should only receive characteristics under one service. const service = this._getServiceByHandle(device.instanceId, characteristics[0].handle_decl); characteristics.forEach(characteristic => { const declarationHandle = characteristic.handle_decl; const valueHandle = characteristic.handle_value; let uuid = HexConv.numberTo16BitUuid(characteristic.uuid.uuid); if (characteristic.uuid.type >= this._bleDriver.BLE_UUID_TYPE_VENDOR_BEGIN) { uuid = this._converter.lookupVsUuid(characteristic.uuid); } else if (characteristic.uuid.type === this._bleDriver.BLE_UUID_TYPE_UNKNOWN) { uuid = null; } const properties = characteristic.char_props; const newCharacteristic = new Characteristic(service.instanceId, uuid, [], properties); newCharacteristic.declarationHandle = characteristic.handle_decl; newCharacteristic.valueHandle = characteristic.handle_value; this._characteristics[newCharacteristic.instanceId] = newCharacteristic; if (uuid === null) { gattOperation.pendingHandleReads[declarationHandle] = newCharacteristic; } // Add pending reads to get characteristics values. if (properties.read) { gattOperation.pendingHandleReads[valueHandle] = newCharacteristic; } }); const nextStartHandle = characteristics[characteristics.length - 1].handle_decl + 1; const handleRange = { start_handle: nextStartHandle, end_handle: service.endHandle }; if (service.endHandle <= nextStartHandle) { finishCharacteristicDiscovery(); return; } // Do one more round with discovery of characteristics this._adapter.gattcDiscoverCharacteristics(device.connectionHandle, handleRange, err => { if (err) { this.emit('error', 'Failed to get Characteristics'); // Call getCharacteristics callback?? } }); } _parseGattcDescriptorDiscoveryResponseEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); const descriptors = event.descs; const gattOperation = this._gattOperationsMap[device.instanceId]; const finishDescriptorDiscovery = () => { if (_.isEmpty(gattOperation.pendingHandleReads)) { // No pending reads to wait for. const callbackDescriptors = []; for (let descriptorInstanceId in this._descriptors) { const descriptor = this._descriptors[descriptorInstanceId]; if (descriptor.characteristicInstanceId === gattOperation.parent.instanceId) { callbackDescriptors.push(descriptor); } } delete this._gattOperationsMap[device.instanceId]; gattOperation.callback(undefined, callbackDescriptors); } else { for (let handle in gattOperation.pendingHandleReads) { const handleAsNumber = parseInt(handle, 10); // Just take the first found handle and start the read process. this._adapter.gattcRead(device.connectionHandle, handleAsNumber, 0, err => { if (err) { this.emit('error', err); // Call getDescriptors callback?? } }); break; } } }; if (event.count === 0) { finishDescriptorDiscovery(); return; } // We should only receive descriptors under one characteristic. const characteristic = gattOperation.parent; let foundNextServiceOrCharacteristic = false; descriptors.forEach(descriptor => { if (foundNextServiceOrCharacteristic) { return; } const handle = descriptor.handle; let uuid = HexConv.numberTo16BitUuid(descriptor.uuid.uuid); if (descriptor.uuid.type >= this._bleDriver.BLE_UUID_TYPE_VENDOR_BEGIN) { uuid = this._converter.lookupVsUuid(descriptor.uuid); } else if (descriptor.uuid.type === this._bleDriver.BLE_UUID_TYPE_UNKNOWN) { uuid = 'Unknown 128 bit descriptor uuid '; } // TODO: Fix magic number? Primary Service and Characteristic Declaration uuids if (uuid === '2800' || uuid === '2803') { // Found a service or characteristic declaration foundNextServiceOrCharacteristic = true; return; } const newDescriptor = new Descriptor(characteristic.instanceId, uuid, null); newDescriptor.handle = handle; this._descriptors[newDescriptor.instanceId] = newDescriptor; // TODO: We cannot read descriptor 128bit uuid. gattOperation.pendingHandleReads[handle] = newDescriptor; }); if (foundNextServiceOrCharacteristic) { finishDescriptorDiscovery(); return; } const service = this._services[gattOperation.parent.serviceInstanceId]; const nextStartHandle = descriptors[descriptors.length - 1].handle + 1; if (service.endHandle < nextStartHandle) { finishDescriptorDiscovery(); return; } const handleRange = { start_handle: nextStartHandle, end_handle: service.endHandle }; this._adapter.gattcDiscoverDescriptors(device.connectionHandle, handleRange, err => { this._checkAndPropagateError(err, 'Failed to get Descriptors'); }); } _parseGattcReadResponseEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); const handle = event.handle; const data = event.data; const gattOperation = this._gattOperationsMap[device.instanceId]; if(!gattOperation) { return; } if (gattOperation && gattOperation.pendingHandleReads && !_.isEmpty(gattOperation.pendingHandleReads)) { const pendingHandleReads = gattOperation.pendingHandleReads; const attribute = pendingHandleReads[handle]; if (!attribute) { this.emit('logMessage', logLevel.DEBUG, `Unable to find attribute with handle ${event.handle} ` + 'when parsing GATTC read response event.'); return; } delete pendingHandleReads[handle]; if (attribute instanceof Service) { // TODO: Translate from uuid to name? attribute.uuid = HexConv.arrayTo128BitUuid(data); this.emit('serviceAdded', attribute); if (_.isEmpty(pendingHandleReads)) { const callbackServices = []; for (let serviceInstanceId in this._services) { if (this._services[serviceInstanceId].deviceInstanceId === device.instanceId) { callbackServices.push(this._services[serviceInstanceId]); } } delete this._gattOperationsMap[device.instanceId]; gattOperation.callback(undefined, callbackServices); } } else if (attribute instanceof Characteristic) { // TODO: Translate from uuid to name? const emitCharacteristicAdded = () => { /** * Characteristic was successfully added to the Adapter's GATT attribute table. * * @event Adapter#characteristicAdded * @type {Object} * @property {Service} attribute - The new added characteristic. */ if (attribute.uuid && attribute.value) { this.emit('characteristicAdded', attribute); } }; if (handle === attribute.declarationHandle) { attribute.uuid = HexConv.arrayTo128BitUuid(data.slice(3)); emitCharacteristicAdded(); } else if (handle === attribute.valueHandle) { attribute.value = data; emitCharacteristicAdded(); } if (_.isEmpty(pendingHandleReads)) { const callbackCharacteristics = []; for (let characteristicInstanceId in this._characteristics) { if (this._characteristics[characteristicInstanceId].serviceInstanceId === attribute.serviceInstanceId) { callbackCharacteristics.push(this._characteristics[characteristicInstanceId]); } } delete this._gattOperationsMap[device.instanceId]; gattOperation.callback(undefined, callbackCharacteristics); } } else if (attribute instanceof Descriptor) { attribute.value = data; if (attribute.uuid && attribute.value) { /** * Descriptor was successfully added to the Adapter's GATT attribute table. * * @event Adapter#descriptorAdded * @type {Object} * @property {Service} attribute - The new added descriptor. */ this.emit('descriptorAdded', attribute); } if (_.isEmpty(pendingHandleReads)) { const callbackDescriptors = []; for (let descriptorInstanceId in this._descriptors) { if (this._descriptors[descriptorInstanceId].characteristicInstanceId === attribute.characteristicInstanceId) { callbackDescriptors.push(this._descriptors[descriptorInstanceId]); } } delete this._gattOperationsMap[device.instanceId]; gattOperation.callback(undefined, callbackDescriptors); } } for (let newReadHandle in pendingHandleReads) { const newReadHandleAsNumber = parseInt(newReadHandle, 10); // Just take the first found handle and start the read process. this._adapter.gattcRead(device.connectionHandle, newReadHandleAsNumber, 0, err => { if (err) { this.emit('error', err); // Call getAttributecallback callback?? } }); break; } } else { if (event.gatt_status !== this._bleDriver.BLE_GATT_STATUS_SUCCESS) { delete this._gattOperationsMap[device.instanceId]; gattOperation.callback(_makeError(`Read operation failed: ${event.gatt_status_name} (0x${HexConv.numberToHexString(event.gatt_status)})`)); return; } gattOperation.readBytes = gattOperation.readBytes ? gattOperation.readBytes.concat(event.data) : event.data; if (event.data.length < this._maxReadPayloadSize(device.instanceId)) { delete this._gattOperationsMap[device.instanceId]; gattOperation.callback(undefined, gattOperation.readBytes); } else if (event.data.length === this._maxReadPayloadSize(device.instanceId)) { // We need to read more: this._adapter.gattcRead(event.conn_handle, event.handle, gattOperation.readBytes.length, err => { if (err) { delete this._gattOperationsMap[device.instanceId]; this.emit('error', _makeError('Read value failed', err)); gattOperation.callback('Failed reading at byte #' + gattOperation.readBytes.length); } }); } else { delete this._gattOperationsMap[device.instanceId]; this.emit('error', 'Length of Read response is > mtu'); gattOperation.callback('Invalid read response length. (> mtu)'); } } } _parseGattcWriteResponseEvent(event) { // 1. Check if there is a long write in progress for this device // 2a. If there is check if it is done after next write // 2ai. If it is done after next write // Perform the last write and if success, exec write on fail, cancel write // callback, delete callback, delete pending write, emit // 2aii. if not done, issue one more PREPARED_WRITE, update pendingwrite // TODO: Do more checking of write response? const device = this._getDeviceByConnectionHandle(event.conn_handle); const handle = event.handle; const gattOperation = this._gattOperationsMap[device.instanceId]; if (!device) { delete this._gattOperationsMap[device.instanceId]; this.emit('error', 'Failed to handle write event, no device with handle ' + device.instanceId + 'found.'); gattOperation.callback(_makeError('Failed to handle write event, no device with connection handle ' + event.conn_handle + 'found')); return; } if (event.write_op === this._bleDriver.BLE_GATT_OP_WRITE_CMD) { gattOperation.attribute.value = gattOperation.value; } else if (event.write_op === this._bleDriver.BLE_GATT_OP_PREP_WRITE_REQ) { const writeParameters = { write_op: 0, flags: 0, handle: handle, offset: 0, len: 0, value: [], }; if (gattOperation.bytesWritten < gattOperation.value.length) { const value = gattOperation.value.slice(gattOperation.bytesWritten, gattOperation.bytesWritten + this._maxLongWritePayloadSize(device.instanceId)); writeParameters.write_op = this._bleDriver.BLE_GATT_OP_PREP_WRITE_REQ; writeParameters.handle = handle; writeParameters.offset = gattOperation.bytesWritten; writeParameters.len = value.length; writeParameters.value = value; gattOperation.bytesWritten += value.length; this._adapter.gattcWrite(device.connectionHandle, writeParameters, err => { if (err) { this._longWriteCancel(device, gattOperation.attribute); this.emit('error', _makeError('Failed to write value to device/handle ' + device.instanceId + '/' + handle, err)); return; } }); } else { writeParameters.write_op = this._bleDriver.BLE_GATT_OP_EXEC_WRITE_REQ; writeParameters.flags = this._bleDriver.BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE; this._adapter.gattcWrite(device.connectionHandle, writeParameters, err => { if (err) { this._longWriteCancel(device, gattOperation.attribute); this.emit('error', _makeError('Failed to write value to device/handle ' + device.instanceId + '/' + handle, err)); return; } }); } return; } else if (event.write_op === this._bleDriver.BLE_GATT_OP_WRITE_REQ || event.write_op === this._bleDriver.BLE_GATT_OP_EXEC_WRITE_REQ) { gattOperation.attribute.value = gattOperation.value; delete this._gattOperationsMap[device.instanceId]; if (event.gatt_status !== this._bleDriver.BLE_GATT_STATUS_SUCCESS) { gattOperation.callback(_makeError(`Write operation failed: ${event.gatt_status_name} (0x${HexConv.numberToHexString(event.gatt_status)})`)); return; } } this._emitAttributeValueChanged(gattOperation.attribute); gattOperation.callback(undefined, gattOperation.attribute); } _getServiceByHandle(deviceInstanceId, handle) { let foundService = null; for (let serviceInstanceId in this._services) { const service = this._services[serviceInstanceId]; if (!_.isEqual(service.deviceInstanceId, deviceInstanceId)) { continue; } if (service.startHandle <= handle && (!foundService || foundService.startHandle <= service.startHandle)) { foundService = service; } } return foundService; } _getCharacteristicByHandle(deviceInstanceId, handle) { const service = this._getServiceByHandle(deviceInstanceId, handle); let foundCharacteristic = null; for (let characteristicInstanceId in this._characteristics) { const characteristic = this._characteristics[characteristicInstanceId]; if (characteristic.serviceInstanceId !== service.instanceId) { continue; } if (characteristic.declarationHandle <= handle && (!foundCharacteristic || foundCharacteristic.declarationHandle < characteristic.declarationHandle)) { foundCharacteristic = characteristic; } } return foundCharacteristic; } _getCharacteristicByValueHandle(devinceInstanceId, valueHandle) { return _.find(this._characteristics, characteristic => this._services[characteristic.serviceInstanceId].deviceInstanceId === devinceInstanceId && characteristic.valueHandle === valueHandle); } _getDescriptorByHandle(deviceInstanceId, handle) { const characteristic = this._getCharacteristicByHandle(deviceInstanceId, handle); for (let descriptorInstanceId in this._descriptors) { const descriptor = this._descriptors[descriptorInstanceId]; if (descriptor.characteristicInstanceId !== characteristic.instanceId) { continue; } if (descriptor.handle === handle) { return descriptor; } } return null; } _getAttributeByHandle(deviceInstanceId, handle) { return this._getDescriptorByHandle(deviceInstanceId, handle) || this._getCharacteristicByValueHandle(deviceInstanceId, handle) || this._getCharacteristicByHandle(deviceInstanceId, handle) || this._getServiceByHandle(deviceInstanceId, handle); } _emitAttributeValueChanged(attribute) { if (attribute instanceof Characteristic) { /** * The value of a characteristic in the Adapter's GATT attribute table changed. * * @event Adapter#characteristicValueChanged * @type {Object} * @property {Characteristic} attribute - The changed characteristic. */ this.emit('characteristicValueChanged', attribute); } else if (attribute instanceof Descriptor) { /** * The value of a descriptor in the Adapter's GATT attribute table changed. * * @event Adapter#descriptorValueChanged * @type {Object} * @property {Descriptor} attribute - The changed descriptor. */ this.emit('descriptorValueChanged', attribute); } } _parseGattcHvxEvent(event) { if (event.type === this._bleDriver.BLE_GATT_HVX_INDICATION) { this._adapter.gattcConfirmHandleValue(event.conn_handle, event.handle, error => { if (error) { this.emit('error', _makeError('Failed to call gattcConfirmHandleValue', error)); } }); } const device = this._getDeviceByConnectionHandle(event.conn_handle); const characteristic = this._getCharacteristicByValueHandle(device.instanceId, event.handle); if (!characteristic) { this.emit('logMessage', logLevel.DEBUG, `Cannot handle HVX event. No characteristic value with handle ${event.handle} found.`); return; } characteristic.value = event.data; this.emit('characteristicValueChanged', characteristic); } _parseGattcExchangeMtuResponseEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); const gattOperation = this._gattOperationsMap[device.instanceId]; const previousMtu = this._attMtuMap[device.instanceId]; const newMtu = Math.min(event.server_rx_mtu, gattOperation.clientRxMtu); this._attMtuMap[device.instanceId] = newMtu; if (newMtu !== previousMtu) { device.mtu = newMtu; /** * Exchange MTU Response event. * * @event Adapter#attMtuChanged * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we've connected to. * @property {number} newMtu - Server RX MTU size. */ this.emit('attMtuChanged', device, newMtu); } if (gattOperation && gattOperation.callback) { gattOperation.callback(null, newMtu); delete this._gattOperationsMap[device.instanceId]; } } _parseGattTimeoutEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); const gattOperation = this._gattOperationsMap[device.instanceId]; const error = _makeError('Received a Gatt timeout'); this.emit('error', error); if (gattOperation) { if (gattOperation.callback) { gattOperation.callback(error); } delete this._gattOperationsMap[device.instanceId]; } } _parseGattsWriteEvent(event) { // TODO: BLE_GATTS_OP_SIGN_WRITE_CMD not supported? // TODO: Support auth_required flag const device = this._getDeviceByConnectionHandle(event.conn_handle); const attribute = this._getAttributeByHandle('local.server', event.handle); if (event.op === this._bleDriver.BLE_GATTS_OP_WRITE_REQ || event.op === this._bleDriver.BLE_GATTS_OP_WRITE_CMD) { if (this._instanceIdIsOnLocalDevice(attribute.instanceId) && this._isCCCDDescriptor(attribute.instanceId)) { this._setDescriptorValue(attribute, event.data, device.instanceId); this._emitAttributeValueChanged(attribute); } else { this._setAttributeValueWithOffset(attribute, event.data, event.offset); this._emitAttributeValueChanged(attribute); } } } _parseGattsRWAutorizeRequestEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); let promiseChain = new Promise(resolve => resolve()); let authorizeReplyParams; const createWritePromise = (handle, data, offset) => { return new Promise((resolve, reject) => { const attribute = this._getAttributeByHandle('local.server', handle); this._writeLocalValue(attribute, data, offset, error => { if (error) { this.emit('error', _makeError('Failed to set local attribute value from rwAuthorizeRequest', error)); reject(_makeError('Failed to set local attribute value from rwAuthorizeRequest', error)); } else { this._emitAttributeValueChanged(attribute); resolve(); } }); }); }; if (event.type === this._bleDriver.BLE_GATTS_AUTHORIZE_TYPE_WRITE) { if (event.write.op === this._bleDriver.BLE_GATTS_OP_WRITE_REQ) { promiseChain = promiseChain.then(() => { createWritePromise(event.write.handle, event.write.data, event.write.offset); }); authorizeReplyParams = { type: event.type, write: { gatt_status: this._bleDriver.BLE_GATT_STATUS_SUCCESS, update: 1, offset: event.write.offset, len: event.write.len, data: event.write.data, }, }; } else if (event.write.op === this._bleDriver.BLE_GATTS_OP_PREP_WRITE_REQ) { if (!this._preparedWritesMap[device.instanceId]) { this._preparedWritesMap[device.instanceId] = []; } let preparedWrites = this._preparedWritesMap[device.instanceId]; preparedWrites = preparedWrites.concat({ handle: event.write.handle, value: event.write.data, offset: event.write.offset }); this._preparedWritesMap[device.instanceId] = preparedWrites; authorizeReplyParams = { type: event.type, write: { gatt_status: this._bleDriver.BLE_GATT_STATUS_SUCCESS, update: 1, offset: event.write.offset, len: event.write.len, data: event.write.data, }, }; } else if (event.write.op === this._bleDriver.BLE_GATTS_OP_EXEC_WRITE_REQ_CANCEL) { delete this._preparedWritesMap[device.instanceId]; authorizeReplyParams = { type: event.type, write: { gatt_status: this._bleDriver.BLE_GATT_STATUS_SUCCESS, update: 0, offset: 0, len: 0, data: [], }, }; } else if (event.write.op === this._bleDriver.BLE_GATTS_OP_EXEC_WRITE_REQ_NOW) { for (let preparedWrite of this._preparedWritesMap[device.instanceId]) { promiseChain = promiseChain.then(() => { createWritePromise(preparedWrite.handle, preparedWrite.value, preparedWrite.offset); }); } delete this._preparedWritesMap[device.instanceId]; authorizeReplyParams = { type: event.type, write: { gatt_status: this._bleDriver.BLE_GATT_STATUS_SUCCESS, update: 0, offset: 0, len: 0, data: [], }, }; } } else if (event.type === this._bleDriver.BLE_GATTS_AUTHORIZE_TYPE_READ) { authorizeReplyParams = { type: event.type, read: { gatt_status: this._bleDriver.BLE_GATT_STATUS_SUCCESS, update: 0, // 0 = Don't provide data here, read from server. offset: 0, len: 0, data: [], }, }; } promiseChain.then(() => { this._adapter.gattsReplyReadWriteAuthorize(event.conn_handle, authorizeReplyParams, error => { if (error) { this.emit('error', _makeError('Failed to call gattsReplyReadWriteAuthorize', error)); } }); }); } _parseGattsSysAttrMissingEvent(event) { this._adapter.gattsSystemAttributeSet(event.conn_handle, null, 0, 0, error => { if (error) { this.emit('error', _makeError('Failed to call gattsSystemAttributeSet', error)); } }); } _parseGattsHvcEvent(event) { const remoteDevice = this._getDeviceByConnectionHandle(event.conn_handle); const characteristic = this._getCharacteristicByHandle('local.server', event.handle); if (this._pendingNotificationsAndIndications.deviceNotifiedOrIndicated) { this._pendingNotificationsAndIndications.deviceNotifiedOrIndicated(remoteDevice, characteristic); } /** * Handle Value Notification or Indication event. * * @event Adapter#deviceNotifiedOrIndicated * @type {Object} * @property {Device} remoteDevice - The Device instance representing the BLE peer we've connected to. * @property {Characteristic} characteristic - Characteristic to which the HVx operation applies. */ this.emit('deviceNotifiedOrIndicated', remoteDevice, characteristic); this._pendingNotificationsAndIndications.remainingIndicationConfirmations--; if (this._sendingNotificationsAndIndicationsComplete()) { this._pendingNotificationsAndIndications.completeCallback(undefined, characteristic); this._pendingNotificationsAndIndications = {}; } } _parseGattsExchangeMtuRequestEvent(event) { const device = this._getDeviceByConnectionHandle(event.conn_handle); /** * ATT MTU Request. * * @event Adapter#attMtuRequest * @type {Object} * @property {Device} device - The Device instance representing the BLE peer we're connected to. * @property {Object} mtu - requested ATT MTU. */ this.emit('attMtuRequest', device, event.client_rx_mtu); } _parseMemoryRequestEvent(event) { if (event.type === this._bleDriver.BLE_USER_MEM_TYPE_GATTS_QUEUED_WRITES) { this._adapter.replyUserMemory(event.conn_handle, null, error => { if (error) { this.emit('error', _makeError('Failed to call replyUserMemory', error)); } }); } } _parseTxCompleteEvent(event) { const remoteDevice = this._getDeviceByConnectionHandle(event.conn_handle); /** * Transmission Complete. * * @event Adapter#txComplete * @type {Object} * @property {Device} remoteDevice - The Device instance representing the BLE peer we've connected to. * @property {number} event.count - Number of packets transmitted. */ this.emit('txComplete', remoteDevice, event.count); } _setAttributeValueWithOffset(attribute, value, offset) { attribute.value = attribute.value.slice(0, offset).concat(value); } /** * Gets and updates this adapter's state. * * @param {function(Error, AdapterState)} [callback] Callback signature: (err, state) => {} where `state` is an * instance of `AdapterState` corresponding to this adapter's * stored state. * @returns {void} */ getState(callback) { const changedStates = {}; this._adapter.getVersion((version, err) => { if (this._checkAndPropagateError( err, 'Failed to retrieve softdevice firmwareVersion.', callback)) return; changedStates.firmwareVersion = version; this._adapter.gapGetDeviceName((name, err) => { if (this._checkAndPropagateError( err, 'Failed to retrieve driver version.', callback)) return; changedStates.name = name; this._adapter.gapGetAddress((address, err) => { if (this._checkAndPropagateError( err, 'Failed to retrieve device address.', callback)) return; changedStates.address = address; changedStates.available = true; changedStates.bleEnabled = true; this._changeState(changedStates); if (callback) { callback(undefined, this._state); } }); }); }); } /** * Sets this adapter's BLE device's GAP name. * * @param {string} name GAP device name. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ setName(name, callback) { let _name = name.split(); this._adapter.gapSetDeviceName({ sm: 0, lv: 0 }, _name, err => { if (err) { this.emit('error', _makeError('Failed to set name to adapter', err)); } else if (this._state.name !== name) { this._state.name = name; this._changeState({ name: name }); } if (callback) { callback(err); } }); } _getAddressStruct(address, type) { return { address: address, type: type }; } /** * @summary Sets this adapter's BLE device's local Bluetooth identity address. * * The local Bluetooth identity address is the address that identifies this device to other peers. * The address type must be either `BLE_GAP_ADDR_TYPE_PUBLIC` or 'BLE_GAP_ADDR_TYPE_RANDOM_STATIC'. * The identity address cannot be changed while roles are running. * * @param {string} address The local Bluetooth identity address. * @param {string} type The address type. 'BLE_GAP_ADDR_TYPE_RANDOM_STATIC' or `BLE_GAP_ADDR_TYPE_PUBLIC`. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ setAddress(address, type, callback) { // TODO: if privacy is active use this._bleDriver.BLE_GAP_ADDR_CYCLE_MODE_AUTO? const cycleMode = this._bleDriver.BLE_GAP_ADDR_CYCLE_MODE_NONE; const addressStruct = this._getAddressStruct(address, type); this._adapter.gapSetAddress(cycleMode, addressStruct, err => { if (err) { this.emit('error', _makeError('Failed to set address', err)); } else if (this._state.address !== address) { this._changeState({ address: address }); } if (callback) { callback(err); } }); } _setDeviceName(deviceName, security, callback) { const convertedSecurity = Converter.securityModeToDriver(security); this._adapter.gapSetDeviceName(convertedSecurity, deviceName, err => { if (err) { this.emit('error', _makeError('Failed to set device name', err)); } if (callback) { callback(err); } }); } _setDeviceNameFromArray(valueArray, writePerm, callback) { const nameArray = valueArray.concat(0); this._setDeviceName(nameArray, writePerm, callback); } _setAppearance(appearance, callback) { this._adapter.gapSetAppearance(appearance, err => { if (err) { this.emit('error', _makeError('Failed to set appearance', err)); } if (callback) { callback(err); } }); } _setAppearanceFromArray(valueArray, callback) { const appearanceValue = valueArray[0] + (valueArray[1] << 8); this._setAppearance(appearanceValue, callback); } _setPPCP(ppcp, callback) { this._adapter.gapSetPPCP(ppcp, err => { if (err) { this.emit('error', _makeError('Failed to set PPCP', err)); } if (callback) { callback(err); } }); } _setPPCPFromArray(valueArray, callback) { // TODO: Fix addon parameter check to also accept arrays? Atleast avoid converting twice const ppcpParameter = { min_conn_interval: (valueArray[0] + (valueArray[1] << 8)) * (1250 / 1000), max_conn_interval: (valueArray[2] + (valueArray[3] << 8)) * (1250 / 1000), slave_latency: (valueArray[4] + (valueArray[5] << 8)), conn_sup_timeout: (valueArray[6] + (valueArray[7] << 8)) * (10000 / 1000), }; this._setPPCP(ppcpParameter, callback); } /** * Get this adapter's connected device/devices. * @returns {Device[]} An array of this adapter's connected device/devices. */ getDevices() { return this._devices; } /** * Get a device connected to this adapter by its instanceId. * * @param {string} deviceInstanceId The device's unique Id. * @returns {null|Device} The device connected to this adapter corresponding to `deviceInstanceId`. */ getDevice(deviceInstanceId) { return this._devices[deviceInstanceId]; } _getDeviceByConnectionHandle(connectionHandle) { const foundDeviceId = Object.keys(this._devices).find(deviceId => { return this._devices[deviceId].connectionHandle === connectionHandle; }); return this._devices[foundDeviceId]; } _getDeviceByAddress(address) { const foundDeviceId = Object.keys(this._devices).find(deviceId => { return this._devices[deviceId].address === address; }); return this._devices[foundDeviceId]; } /** * @summary Start scanning (GAP Discovery procedure, Observer Procedure). * * @param {Object} options The GAP scanning parameters. * Available scan parameters: * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ startScan(options, callback) { this._adapter.gapStartScan(options, err => { if (err) { this.emit('error', _makeError('Error occured when starting scan', err)); } else { this._changeState({ scanning: true }); } if (callback) { callback(err); } }); } /** * Stop scanning (GAP Discovery procedure, Observer Procedure). * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ stopScan(callback) { this._adapter.gapStopScan(err => { if (err) { // TODO: probably is state already set to false, but should we make sure? if yes, emit stateChanged? this.emit('error', _makeError('Error occured when stopping scanning', err)); } else { this._changeState({ scanning: false }); } if (callback) { callback(err); } }); } /** * @summary Create a connection (GAP Link Establishment). * * If a scanning procedure is currently in progress it will be automatically stopped when calling this function. * * The application will be informed of a connection being established with a event:DeviceConnectedEvent. * * @param {string|Object} deviceAddress The peer address. If the use_whitelist bit is set in scanParams, * then this is ignored. If given as a string, * `address.type='BLE_GAP_ADDR_TYPE_RANDOM_STATIC'` by default. Else, * an Object with members: { address: {string}, type: {string} } must be given. * @param {Object} options The scan and connection parameters. * Available options: * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ connect(deviceAddress, options, callback) { if (!_.isEmpty(this._gapOperationsMap)) { const errorObject = _makeError('Could not connect. Another connect is in progress.'); this.emit('error', errorObject); if (callback) callback(errorObject); return; } var address = {}; if (typeof deviceAddress === 'string') { address.address = deviceAddress; address.type = 'BLE_GAP_ADDR_TYPE_RANDOM_STATIC'; } else { address = deviceAddress; } this._changeState({ scanning: false, connecting: true }); this._adapter.gapConnect(address, options.scanParams, options.connParams, err => { if (err) { this._changeState({ connecting: false }); const errorMsg = (err.errcode === 'NRF_ERROR_CONN_COUNT') ? _makeError(`Could not connect. Max number of connections reached.`, err) : _makeError(`Could not connect to ${deviceAddress.address}`, err); this.emit('error', errorMsg); if (callback) { callback(errorMsg); } } else { this._gapOperationsMap.connecting = { deviceAddress: address, callback: callback }; } }); } /** * Cancel a connection establishment. * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ cancelConnect(callback) { this._adapter.gapCancelConnect(err => { if (err) { // TODO: log more const newError = _makeError('Error occured when canceling connection', err); this.emit('error', newError); if (callback) { callback(newError); } } else { const errorObject = _makeError('Connection canceled.'); const connectingCallback = this._gapOperationsMap.connecting.callback; if (connectingCallback) connectingCallback(errorObject); delete this._gapOperationsMap.connecting; this._changeState({ connecting: false }); if (callback) { callback(); } } }); } // Enable the client role and starts advertising _getAdvertisementParams(params) { var retval = {}; retval.channel_mask = {}; retval.channel_mask.ch_37_off = false; retval.channel_mask.ch_38_off = false; retval.channel_mask.ch_39_off = false; if (params.channelMask) { for (let channel in params.channelMask) { switch (params.channelMask[channel]) { case 'ch37off': retval.channel_mask.ch_37_off = true; break; case 'ch38off': retval.channel_mask.ch_38_off = true; break; case 'ch39off': retval.channel_mask.ch_39_off = true; break; default: throw new Error(`Channel ${channel} is not possible to switch off during advertising.`); } } } if (params.interval) { retval.interval = params.interval; } else { throw new Error('You have to provide an interval.'); } if (params.timeout || params.timeout === 0) { retval.timeout = params.timeout; } else { throw new Error('You have to provide a timeout.'); } // TOOD: fix fp logic later retval.fp = this._bleDriver.BLE_GAP_ADV_FP_ANY; // Default value is that device is connectable undirected. retval.type = this._bleDriver.BLE_GAP_ADV_TYPE_ADV_IND; // TODO: we do not support directed connectable mode yet if (params.connectable !== undefined) { if (!params.connectable) { retval.type |= this._bleDriver.BLE_GAP_ADV_TYPE_NONCONN_IND; } } if (params.scannable !== undefined) { if (params.scannable) { retval.type |= this._bleDriver.BLE_GAP_ADV_TYPE_ADV_SCAN_IND; } } return retval; } /** * @summary Start advertising (GAP Discoverable, Connectable modes, Broadcast Procedure). * * An application can start an advertising procedure for broadcasting purposes while a connection * is active. After a BLE_GAP_EVT_CONNECTED event is received, this function may therefore * be called to start a broadcast advertising procedure. The advertising procedure * cannot however be connectable (it must be of type BLE_GAP_ADV_TYPE_ADV_SCAN_IND or * BLE_GAP_ADV_TYPE_ADV_NONCONN_IND). * * Only one advertiser may be active at any time. * @param {Object} options GAP advertising parameters. * Available GAP advertising parameters: * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ startAdvertising(options, callback) { const advParams = this._getAdvertisementParams(options); this._adapter.gapStartAdvertising(advParams, err => { if (this._checkAndPropagateError(err, 'Failed to start advertising.', callback)) return; this._changeState({ advertising: true }); if (callback) { callback(); } }); } /** * @summary Set, clear or update advertising and scan response data. * The format of the advertising data will be checked by this call to ensure interoperability. * Limitations imposed by this API call to the data provided include having a flags data type in the scan response data and * duplicating the local name in the advertising data and scan response data. * * To clear the advertising data and set it to a 0-length packet, simply provide a null `advData`/`scanRespData` parameter. * * The call will fail if `advData` and `scanRespData` are both null since this would have no effect. * * See @ref: ./util/adType.js for possible advertisement object parameters. * Note: should multiple custom properties be required in the advData or scanRespData, * it is possible to append 'custom' key with colon plus anything, like 'custom:1'. * * @param {Object} advData Advertising packet * @param {Object} scanRespData Scan response packet. * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ setAdvertisingData(advData, scanRespData, callback) { const advDataStruct = Array.from(AdType.convertToBuffer(advData)); const scanRespDataStruct = Array.from(AdType.convertToBuffer(scanRespData)); this._adapter.gapSetAdvertisingData( advDataStruct, scanRespDataStruct, err => { if (this._checkAndPropagateError(err, 'Failed to set advertising data.', callback)) return; if (callback) { callback(); } } ); } /** * Stop advertising (GAP Discoverable, Connectable modes, Broadcast Procedure). * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ stopAdvertising(callback) { this._adapter.gapStopAdvertising(err => { if (this._checkAndPropagateError(err, 'Failed to stop advertising.', callback)) return; this._changeState({ advertising: false }); if (callback) { callback(); } }); } /** * @summary Disconnect (GAP Link Termination). * This call initiates the disconnection procedure, and its completion will be communicated to the application * with a `BLE_GAP_EVT_DISCONNECTED` event upon which `callback` will be called. * * @param {string} deviceInstanceId The device's unique Id. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ disconnect(deviceInstanceId, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to disconnect', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) callback(errorObject); return; } const hciStatusCode = this._bleDriver.BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION; this._gapOperationsMap[deviceInstanceId] = { callback: callback, }; this._adapter.gapDisconnect(device.connectionHandle, hciStatusCode, err => { if (err) { const errorObject = _makeError('Failed to disconnect', err); delete this._gapOperationsMap[deviceInstanceId]; this.emit('error', errorObject); if (callback) { callback(errorObject); } } else { // Expect a disconnect event down the road } }); } _getConnectionUpdateParams(options) { return { min_conn_interval: options.minConnectionInterval, max_conn_interval: options.maxConnectionInterval, slave_latency: options.slaveLatency, conn_sup_timeout: options.connectionSupervisionTimeout, }; } /** * @summary Update connection parameters. * * In the central role this will initiate a Link Layer connection parameter update procedure, * otherwise in the peripheral role, this will send the corresponding L2CAP request and wait for * the central to perform the procedure. In both cases, and regardless of success or failure, the application * will be informed of the result with a event:connParamUpdateEvent. * * @param {string} deviceInstanceId The device's unique Id. * @param {Object} options GAP Connection Parameters. * Available GAP Connection Parameters: * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ updateConnectionParameters(deviceInstanceId, options, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { throw new Error('No device with instance id: ' + deviceInstanceId); } const connectionParamsStruct = this._getConnectionUpdateParams(options); this._adapter.gapUpdateConnectionParameters(device.connectionHandle, connectionParamsStruct, err => { if (err) { const errorObject = _makeError('Failed to update connection parameters', err); this.emit('error', errorObject); if (callback) { callback(errorObject); } } else { this._gapOperationsMap[deviceInstanceId] = { callback, }; } }); } /** * Reject a GAP connection parameters update request. * * @param {string} deviceInstanceId The device's unique Id. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ rejectConnParams(deviceInstanceId, callback) { const connectionHandle = this.getDevice(deviceInstanceId).connectionHandle; // TODO: Does the AddOn support undefined second parameter? this._adapter.gapUpdateConnectionParameters(connectionHandle, null, err => { if (this._checkAndPropagateError(err, 'Failed to reject connection parameters', callback)) { return; } if (callback) { callback(err); } }); } /** * Get the current ATT_MTU size. * * @param {string} deviceInstanceId The device's unique Id. * @returns {undefined|number} The current ATT_MTU size. */ getCurrentAttMtu(deviceInstanceId) { if (!(deviceInstanceId in this._attMtuMap)) { return; } return this._attMtuMap[deviceInstanceId]; } /** * @summary Start an ATT_MTU exchange by sending an Exchange MTU Request to the server. * * The SoftDevice sets ATT_MTU to the minimum of: * * * However, the SoftDevice never sets ATT_MTU lower than `GATT_MTU_SIZE_DEFAULT` == 23. * * @param {string} deviceInstanceId The device's unique Id. * @param {number} mtu Requested ATT_MTU. Default ATT_MTU is 23. Valid range is between 24 and 247. * @param {function(Error, number)} [callback] Callback signature: (err, mtu) => {} where `mtu` is the updated * ATT_MTU value. * @returns {void} */ requestAttMtu(deviceInstanceId, mtu, callback) { if (this._bleDriver.NRF_SD_BLE_API_VERSION <= 2) { if (callback) callback(null, this._bleDriver.GATT_MTU_SIZE_DEFAULT); return; } const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError(`Failed to request att mtu. Failed to find device with id ${deviceInstanceId}`); if (callback) callback(errorObject); return; } if (this._gattOperationsMap[device.instanceId]) { this.emit('error', _makeError('Failed to request att mtu. A GATT operation already in progress.')); return; } this._adapter.gattcExchangeMtuRequest(device.connectionHandle, mtu, err => { if (err) { const errorObject = _makeError(`Failed to request att mtu: ${err.message}`); if (callback) callback(errorObject); return; } this._gattOperationsMap[device.instanceId] = { callback, clientRxMtu: mtu }; }); } /** * @summary Reply to ATT_MTU exchange request * * @param {string} deviceInstanceId The device's unique Id. * @param {number} mtu Requested ATT_MTU. Default ATT_MTU is 23. Valid range is between 24 and 247. * @param {function(Error, number)} [callback] Callback signature: (err, mtu) => {} where `mtu` is the updated * ATT_MTU value. * @returns {void} */ attMtuReply(deviceInstanceId, mtu, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { throw new Error('No device with instance id: ' + deviceInstanceId); } /* Make sure the requested mtu does not exceed the max supported size */ const newMtu = Math.min(mtu, MAX_SUPPORTED_ATT_MTU); this._adapter.gattsExchangeMtuReply(device.connectionHandle, newMtu, error => { if (error) { const errorObject = _makeError('Failed to call gattsExchangeMtuReply', error); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } this._attMtuMap[deviceInstanceId] = newMtu; if (callback) { callback(); } }); } /** * @summary Initiate the GAP Authentication procedure. * * In the central role, this function will send an SMP Pairing Request (or an SMP Pairing Failed if rejected), * otherwise in the peripheral role, an SMP Security Request will be sent. * * @param {string} deviceInstanceId The device's unique Id. * @param {object} secParams The security parameters to be used during the pairing or bonding procedure. * In the peripheral role, only the bond, mitm, lesc and keypress fields of this Object are used. * In the central role, this pointer may be NULL to reject a Security Request. * Available GAP security parameters: * * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ authenticate(deviceInstanceId, secParams, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to authenticate', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } if (device.role === 'central') { device.ownPeriphInitiatedPairingPending = true; } this._adapter.gapAuthenticate(device.connectionHandle, secParams, err => { if (this._checkAndPropagateError(err, 'Failed to authenticate', callback)) { if (device.role === 'central') { device.ownPeriphInitiatedPairingPending = false; } return; } if (callback) { callback(); } }); } /** * @summary Reply with GAP security parameters. * * This function is only used to reply to a `BLE_GAP_EVT_SEC_PARAMS_REQUEST`, calling it at other times will result in an `NRF_ERROR_INVALID_STATE`. * If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected parameters. * * @param {string} deviceInstanceId The device's unique Id. * @param {string} secStatus Security status, see `BLE_GAP_SEC_STATUS`. * @param {Object} secParams Security parameters object. In the central role this must be set to null, as the parameters have * already been provided during a previous call to `this.authenticate()`. * @param {Object} secKeyset security key set object. * * @param {function(Error, Object)} [callback] Callback signature: (err, secKeyset) => {} where `secKeyset` is a * security key set object as described above. * @returns {void} */ replySecParams(deviceInstanceId, secStatus, secParams, secKeyset, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to reply security parameters', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } this._adapter.gapReplySecurityParameters(device.connectionHandle, secStatus, secParams, secKeyset, (err, secKeyset) => { if (this._checkAndPropagateError(err, 'Failed to reply security parameters.', callback)) { return; } if (callback) { callback(err, secKeyset); } }); } /** * @summary Reply with an authentication key. * * This function is only used to reply to a `BLE_GAP_EVT_AUTH_KEY_REQUEST `or a `BLE_GAP_EVT_PASSKEY_DISPLAY`, calling it at other times will result in an `NRF_ERROR_INVALID_STATE`. * If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected parameters. * * @param {string} deviceInstanceId The device's unique Id. * @param {Object} keyType No key, 6-digit Passkey or Out Of Band data. * @param {null|Array|string} key If key type is `BLE_GAP_AUTH_KEY_TYPE_NONE`, then null. * If key type is `BLE_GAP_AUTH_KEY_TYPE_PASSKEY`, then a 6-byte array (digit 0..9 only) * or null when confirming LE Secure Connections Numeric Comparison. * If key type is `BLE_GAP_AUTH_KEY_TYPE_OOB`, then a 16-byte OOB key value in Little Endian format. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ replyAuthKey(deviceInstanceId, keyType, key, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to reply authenticate key', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } // If the key is a string we split it into an array before we call gapReplyAuthKey if (key && key.constructor === String) { key = Array.from(key); } this._adapter.gapReplyAuthKey(device.connectionHandle, keyType, key, err => { if (this._checkAndPropagateError(err, 'Failed to reply authenticate key.', callback)) { return; } if (callback) { callback(); } }); } /** * @summary Reply with an LE Secure connections DHKey. * * This function is only used to reply to a `BLE_GAP_EVT_LESC_DHKEY_REQUEST`, calling it at other times will result in an `NRF_ERROR_INVALID_STATE`. * If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected parameters. * * @param {string} deviceInstanceId The device's unique Id. * @param {Object} dhkey LE Secure Connections DHKey. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ replyLescDhkey(deviceInstanceId, dhkey, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to reply lesc dh key', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } this._adapter.gapReplyLescDhKey(device.connectionHandle, dhkey, err => { if (this._checkAndPropagateError(err, 'Failed to reply lesc dh key.', callback)) { return; } if (callback) { callback(); } }); } /** * @summary Notify the peer of a local keypress. * * This function can only be used when an authentication procedure using LE Secure Connection is in progress. Calling it at other times will result in an `NRF_ERROR_INVALID_STATE`. * * @param {string} deviceInstanceId The device's unique Id. * @param {number} notificationType See `adapter.driver.BLE_GAP_KP_NOT_TYPES`. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ notifyKeypress(deviceInstanceId, notificationType, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to notify keypress', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } this._adapter.gapNotifyKeypress(device.connectionHandle, notificationType, err => { if (this._checkAndPropagateError(err, 'Failed to notify keypress.', callback)) { return; } if (callback) { callback(); } }); } /** * @summary Generate a set of OOB data to send to a peer out of band. * * The `ble_gap_addr_t` included in the OOB data returned will be the currently active one (or, if a connection has already been established, * the one used during connection setup). The application may manually overwrite it with an updated value. * * @param {string} deviceInstanceId The device's unique Id. * @param {string} ownPublicKey LE Secure Connections local P-256 Public Key. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ getLescOobData(deviceInstanceId, ownPublicKey, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to get lesc oob data', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } this._adapter.gapGetLescOobData(device.connectionHandle, ownPublicKey, (err, ownOobData) => { let errorObject; if (err) { errorObject = _makeError('Failed to get lesc oob data'); this.emit('error', errorObject); } if (callback) { callback(errorObject, ownOobData); } }); } /** * @summary Provide the OOB data sent/received out of band. * * At least one of the 2 data objects provided must not be null. * An authentication procedure with OOB selected as an algorithm must be in progress when calling this function. * A `BLE_GAP_EVT_LESC_DHKEY_REQUEST` event with the oobd_req set to 1 must have been received prior to calling this function. * * @param {string} deviceInstanceId The device's unique Id. * @param {string} ownOobData The OOB data sent out of band to a peer or NULL if none sent. * @param {string} peerOobData The OOB data received out of band from a peer or NULL if none received. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ setLescOobData(deviceInstanceId, ownOobData, peerOobData, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to set lesc oob data', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } this._adapter.gapSetLescOobData(device.connectionHandle, ownOobData, peerOobData, err => { this._checkAndPropagateError(err, 'Failed to set lesc oob data.', callback); }); if (callback) { callback(); } } /** * @summary Initiate GAP Encryption procedure. * * In the central role, this function will initiate the encryption procedure using the encryption information provided. * * @param {string} deviceInstanceId The device's unique Id. * @param {Object} masterId Master identification structure. TODO * @param {Object} encInfo Encryption information structure. TODO * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ encrypt(deviceInstanceId, masterId, encInfo, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to encrypt', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } this._adapter.gapEncrypt(device.connectionHandle, masterId, encInfo, err => { let errorObject; if (err) { errorObject = _makeError('Failed to encrypt'); this.emit('error', errorObject); } if (callback) { callback(errorObject); } }); } /** * @summary Reply with GAP security information. * * This function is only used to reply to a `BLE_GAP_EVT_SEC_INFO_REQUEST`, calling it at other times will result in `NRF_ERROR_INVALID_STATE`. * If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected parameters. * Data signing is not yet supported, and signInfo must therefore be null. * * @param {string} deviceInstanceId The device's unique Id. * @param {Object} encInfo Encryption information structure. May be null to signal none is available. * @param {Object} idInfo Identity information structure. May be null to signal none is available. * @param {Object} signInfo Pointer to a signing information structure. May be null to signal none is available. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ secInfoReply(deviceInstanceId, encInfo, idInfo, signInfo, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { const errorObject = _makeError('Failed to encrypt', 'Failed to find device with id ' + deviceInstanceId); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } this._adapter.gapReplySecurityInfo(device.connectionHandle, encInfo, idInfo, signInfo, err => { let errorObject; if (err) { errorObject = _makeError('Failed to encrypt'); this.emit('error', errorObject); } if (callback) { callback(errorObject); } }); } /** * Set the services in the BLE peripheral device's GATT attribute table. * * @param {Service[]} services An array of `Service` objects to be set. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ setServices(services, callback) { let decodeUUID = (uuid, data) => { return new Promise((resolve, reject) => { const length = uuid.length === 32 ? 16 : 2; this._adapter.decodeUUID(length, uuid, (err, _uuid) => { if (err) { // If the UUID is not found it is a 128-bit UUID // so we have to add it to the SD and try again if (err.errno === this._bleDriver.NRF_ERROR_NOT_FOUND && length === 16) { this._converter.uuidToDriver(uuid, (err, type) => { if (err) { reject(_makeError(`Unable to add UUID ${uuid} to SoftDevice`, err)); } else { this._adapter.decodeUUID(length, uuid, (err, _uuid) => { if (err) { reject(_makeError(`Unable to decode UUID ${uuid}`, err)); } else { data.decoded_uuid = _uuid; resolve(data); } }); } }); } else { reject(_makeError(`Unable to decode UUID ${uuid}`, err)); } } else { data.decoded_uuid = _uuid; resolve(data); } }); }); }; let addService = (service, type, data) => { return new Promise((resolve, reject) => { var p = Promise.resolve(data); var decode = decodeUUID.bind(undefined, service.uuid); p.then(decode).then(data => { this._adapter.gattsAddService(type, data.decoded_uuid, (err, serviceHandle) => { if (err) { reject(_makeError('Error occurred adding service.', err)); } else { data.serviceHandle = serviceHandle; service.startHandle = serviceHandle; this._services[service.instanceId] = service; // TODO: what if we fail later on this service ? resolve(data); } }); }).catch(err => { reject(err); }); }); }; let addCharacteristic = (characteristic, data) => { return new Promise((resolve, reject) => { this._converter.characteristicToDriver(characteristic, (err, characteristicForDriver) => { if (err) { reject(_makeError('Error converting characteristic to driver.', err)); } else { this._adapter.gattsAddCharacteristic( data.serviceHandle, characteristicForDriver.metadata, characteristicForDriver.attribute, (err, handles) => { if (err) { reject(_makeError('Error occurred adding characteristic.', err)); } else { characteristic.valueHandle = data.characteristicHandle = handles.value_handle; characteristic.declarationHandle = characteristic.valueHandle - 1; // valueHandle is always directly after declarationHandle this._characteristics[characteristic.instanceId] = characteristic; // TODO: what if we fail later on this ? resolve(data); if (!characteristic._factory_descriptors) { return; } const findDescriptor = uuid => { return characteristic._factory_descriptors.find(descriptor => { return descriptor.uuid === uuid; }); }; if (handles.user_desc_handle) { const userDescriptionDescriptor = findDescriptor('2901'); this._descriptors[userDescriptionDescriptor.instanceId] = userDescriptionDescriptor; userDescriptionDescriptor.handle = handles.user_desc_handle; } if (handles.cccd_handle) { const cccdDescriptor = findDescriptor('2902'); this._descriptors[cccdDescriptor.instanceId] = cccdDescriptor; cccdDescriptor.handle = handles.cccd_handle; cccdDescriptor.value = {}; for (let deviceInstanceId in this._devices) { this._setDescriptorValue(cccdDescriptor, [0, 0], deviceInstanceId); } } if (handles.sccd_handle) { const sccdDescriptor = findDescriptor('2903'); this._descriptors[sccdDescriptor.instanceId] = sccdDescriptor; sccdDescriptor.handle = handles.sccd_handle; } } } ); } }); }); }; let addDescriptor = (descriptor, data) => { return new Promise((resolve, reject) => { this._converter.descriptorToDriver(descriptor, (err, descriptorForDriver) => { if (err) { reject(_makeError('Error converting descriptor.', err)); } else if (descriptorForDriver) { this._adapter.gattsAddDescriptor( data.characteristicHandle, descriptorForDriver, (err, handle) => { if (err) { reject(_makeError(err, 'Error adding descriptor.')); } else { descriptor.handle = data.descriptorHandle = handle; this._descriptors[descriptor.instanceId] = descriptor; // TODO: what if we fail later on this ? resolve(data); } } ); } }); }); }; let promiseSequencer = (list, data) => { var p = Promise.resolve(data); return list.reduce((previousP, nextP) => { return previousP.then(nextP); }, p); }; let applyGapServiceCharacteristics = gapService => { for (let characteristic of gapService._factory_characteristics) { // TODO: Fix Device Name uuid magic number if (characteristic.uuid === '2A00') { // TODO: At some point addon should accept string. this._setDeviceNameFromArray(characteristic.value, characteristic.writePerm, err => { if (!err) { characteristic.declarationHandle = 2; characteristic.valueHandle = 3; this._characteristics[characteristic.instanceId] = characteristic; } }); } // TODO: Fix Appearance uuid magic number if (characteristic.uuid === '2A01') { this._setAppearanceFromArray(characteristic.value, err => { if (!err) { characteristic.declarationHandle = 4; characteristic.valueHandle = 5; this._characteristics[characteristic.instanceId] = characteristic; } }); } // TODO: Fix Peripheral Preferred Connection Parameters uuid magic number if (characteristic.uuid === '2A04') { this._setPPCPFromArray(characteristic.value, err => { if (!err) { characteristic.declarationHandle = 6; characteristic.valueHandle = 7; this._characteristics[characteristic.instanceId] = characteristic; } }); } } }; // Create array of function objects to call in sequence. var promises = []; for (let service of services) { var p; if (service.uuid === '1800') { service.startHandle = 1; service.endHandle = 7; applyGapServiceCharacteristics(service); this._services[service.instanceId] = service; continue; } else if (service.uuid === '1801') { service.startHandle = 8; service.endHandle = 8; this._services[service.instanceId] = service; continue; } p = addService.bind(undefined, service, this._getServiceType(service)); promises.push(p); if (service._factory_characteristics) { for (let characteristic of service._factory_characteristics) { p = addCharacteristic.bind(undefined, characteristic); promises.push(p); if (characteristic._factory_descriptors) { for (let descriptor of characteristic._factory_descriptors) { if (!this._converter.isSpecialUUID(descriptor.uuid)) { p = addDescriptor.bind(undefined, descriptor); promises.push(p); } } } } } } // Execute the promises in sequence, start with an empty object that // is propagated to all promises. promiseSequencer(promises, {}).then(data => { // TODO: Ierate over all servicses, descriptors, characterstics from parameter services if (callback) { callback(); } }).catch(err => { this.emit('error', err); if (callback) { callback(err); } }); } /** * Get a `Service` instance by its instanceId. * * @param {string} serviceInstanceId The unique Id of this service. * @returns {Service} The service. */ getService(serviceInstanceId) { return this._services[serviceInstanceId]; } /** * Initiate or continue a GATT Primary Service Discovery procedure. * * @param {string} deviceInstanceId The device's unique Id. * @param {function(Error, Service[])} [callback] Callback signature: (err, services) => {} where `services` is an * array of `Service` instances corresponding to the discovered GATT * primary services. * @returns {void} */ getServices(deviceInstanceId, callback) { // TODO: Implement something for when device is local const device = this.getDevice(deviceInstanceId); if (this._gattOperationsMap[device.instanceId]) { this.emit('error', _makeError('Failed to get services, a GATT operation already in progress')); return; } // TODO: Should we remove old services and do new discovery? const alreadyFoundServices = _.filter(this._services, service => { return deviceInstanceId === service.deviceInstanceId; }); if (!_.isEmpty(alreadyFoundServices)) { if (callback) { callback(undefined, alreadyFoundServices); } return; } this._gattOperationsMap[device.instanceId] = { callback: callback, pendingHandleReads: {}, parent: device }; this._adapter.gattcDiscoverPrimaryServices(device.connectionHandle, 1, null, (err, services) => { if (err) { this.emit('error', _makeError('Failed to get services', err)); if (callback) { callback(err); } return; } }); } /** * Get a `Characteristic` instance by its instanceId. * * @param {string} characteristicId The unique Id of this characteristic. * @returns {Characteristic} The characteristic. */ getCharacteristic(characteristicId) { return this._characteristics[characteristicId]; } /** * Initiate or continue a GATT Characteristic Discovery procedure. * * * @param {string} serviceId Unique ID of of the GATT service. * @param {function(Error, Characteristic[])} [callback] Callback signature: (err, characteristics) => {} where * `characteristics` is an array of `Characteristic` instances * corresponding to the discovered GATT characteristics attached to * the service. * @returns {void} */ getCharacteristics(serviceId, callback) { // TODO: Implement something for when device is local const service = this.getService(serviceId); if (!service) { throw new Error(_makeError('Failed to get characteristics.', 'Could not find service with id: ' + serviceId)); } const device = this.getDevice(service.deviceInstanceId); if (this._gattOperationsMap[device.instanceId]) { this._checkAndPropagateError(undefined, 'Failed to get characteristics, a gatt operation already in progress', callback); return; } const alreadyFoundCharacteristics = _.filter(this._characteristics, characteristic => { return serviceId === characteristic.serviceInstanceId; }); if (!_.isEmpty(alreadyFoundCharacteristics)) { if (callback) { callback(undefined, alreadyFoundCharacteristics); } return; } const handleRange = { start_handle: service.startHandle, end_handle: service.endHandle }; this._gattOperationsMap[device.instanceId] = { callback: callback, pendingHandleReads: {}, parent: service }; this._adapter.gattcDiscoverCharacteristics(device.connectionHandle, handleRange, err => { if (this._checkAndPropagateError(err, 'Failed to get Characteristics', callback)) { return; } }); } /** * Get a `Descriptor` instance by its instanceId. * * @param {string} descriptorId The unique Id of this descriptor. * @returns {Descriptor} The descriptor. */ getDescriptor(descriptorId) { return this._descriptors[descriptorId]; } _isDescriptorPerConnectionBased(descriptor) { return this._isCCCDDescriptor(descriptor.instanceId); } _setDescriptorValue(descriptor, value, deviceInstanceId) { if (this._isDescriptorPerConnectionBased(descriptor)) { descriptor.value[deviceInstanceId] = value; } else { descriptor.value = value; } } _getDescriptorValue(descriptor, deviceInstanceId) { if (this._isDescriptorPerConnectionBased(descriptor)) { return descriptor.value[deviceInstanceId]; } return descriptor.value; } _addDeviceToAllPerConnectionValues(deviceId) { for (const descriptorInstanceId in this._descriptors) { const descriptor = this._descriptors[descriptorInstanceId]; if (this._instanceIdIsOnLocalDevice(descriptorInstanceId) && this._isDescriptorPerConnectionBased(descriptor)) { this._setDescriptorValue(descriptor, [0, 0], deviceId); this.emit('descriptorValueChanged', descriptor); } } } _clearDeviceFromAllPerConnectionValues(deviceId) { for (const descriptorInstanceId in this._descriptors) { const descriptor = this._descriptors[descriptorInstanceId]; if (this._instanceIdIsOnLocalDevice(descriptorInstanceId) && this._isDescriptorPerConnectionBased(descriptor)) { delete descriptor.value[deviceId]; this.emit('descriptorValueChanged', descriptor); } } } _clearDeviceFromDiscoveredServices(deviceId) { this._services = this._filterObject(this._services, value => value.indexOf(deviceId) < 0); this._characteristics = this._filterObject(this._characteristics, value => value.indexOf(deviceId) < 0); this._descriptors = this._filterObject(this._descriptors, value => value.indexOf(deviceId) < 0); } _filterObject(collection, predicate) { const newCollection = {}; for (let key in collection) { if (predicate(key)) { newCollection[key] = collection[key]; } } return newCollection; } /** * Initiate or continue a GATT Characteristic Descriptor Discovery procedure. * * @param {string} characteristicId Unique ID of of the GATT characteristic. * @param {function(Error, Descriptor[])} [callback] Callback signature: (err, descriptors) => {} where * `descriptors` is an array of `Descriptor` instances corresponding to the * discovered GATT descriptors attached to the characteristic. * @returns {void} */ getDescriptors(characteristicId, callback) { const characteristic = this.getCharacteristic(characteristicId); const service = this.getService(characteristic.serviceInstanceId); const device = this.getDevice(service.deviceInstanceId); if (this._gattOperationsMap[device.instanceId]) { this.emit('error', _makeError('Failed to get descriptors, a gatt operation already in progress', undefined)); return; } const alreadyFoundDescriptor = _.filter(this._descriptors, descriptor => { return characteristicId === descriptor.characteristicInstanceId; }); if (!_.isEmpty(alreadyFoundDescriptor)) { if (callback) { callback(undefined, alreadyFoundDescriptor); } return; } const handleRange = { start_handle: characteristic.valueHandle + 1, end_handle: service.endHandle }; this._gattOperationsMap[device.instanceId] = { callback, pendingHandleReads: {}, parent: characteristic }; this._adapter.gattcDiscoverDescriptors(device.connectionHandle, handleRange, err => { //this._checkAndPropagateError('Failed to get descriptors', err, callback); }); } _getDescriptorsPromise() { return (data, serviceId, characteristicId) => { return new Promise((resolve, reject) => { this.getDescriptors( characteristicId, (error, descriptors) => { if (error) { reject(error); return; } data.services[serviceId].characteristics[characteristicId].descriptors = descriptors; resolve(data); } ); }); }; } _getCharacteristicsPromise() { return (data, service) => { return new Promise((resolve, reject) => { this.getCharacteristics(service.instanceId, (error, characteristics) => { if (error) { reject(error); return; } data.services[service.instanceId].characteristics = {}; let promise = Promise.resolve(data); for (let characteristic of characteristics) { data.services[service.instanceId].characteristics[characteristic.instanceId] = characteristic; promise = promise.then(data => { return this._getDescriptorsPromise()( data, service.instanceId, characteristic.instanceId); }); } promise.then(data => { resolve(data); }).catch(error => { reject(error); }); }); }); }; } _getServicesPromise(deviceInstanceId) { return new Promise((resolve, reject) => { this.getServices( deviceInstanceId, (error, services) => { if (error) { reject(error); return; } resolve(services); }); }); } /** * Discovers information about a range of attributes on a GATT server. * * @param {string} deviceInstanceId The device's unique Id. * @param {function(Error, Object)} [callback] Callback signature: (err, attributes) => {} where `attributes` contains * the device's GATT attributes (services, characteristics and * descriptors). * @returns {void} */ getAttributes(deviceInstanceId, callback) { let data = { 'services': {} }; this._getServicesPromise(deviceInstanceId).then(services => { let p = Promise.resolve(data); for (let service of services) { data.services[service.instanceId] = service; p = p.then(data => { return this._getCharacteristicsPromise()(data, service); }); } return p; }) .then(data => { if (callback) callback(undefined, data); }) .catch(error => { if (callback) callback(error); }); } /** * Reads the value of a GATT characteristic. * * @param {string} characteristicId Unique ID of of the GATT characteristic. * @param {function(Error, number[])} [callback] Callback signature: (err, readBytes) => {} where `readBytes` is an * array of numbers corresponding to the value of the GATT * characteristic. * @returns {void} */ readCharacteristicValue(characteristicId, callback) { const characteristic = this.getCharacteristic(characteristicId); if (!characteristic) { throw new Error('Characteristic value read failed: Could not get characteristic with id ' + characteristicId); } if (this._instanceIdIsOnLocalDevice(characteristicId)) { this._readLocalValue(characteristic, callback); return; } const device = this._getDeviceByCharacteristicId(characteristicId); if (!device) { throw new Error('Characteristic value read failed: Could not get device'); } if (this._gattOperationsMap[device.instanceId]) { throw new Error('Characteristic value read failed: A gatt operation already in progress with device id ' + device.instanceId); } this._gattOperationsMap[device.instanceId] = { callback: callback, readBytes: [] }; this._adapter.gattcRead(device.connectionHandle, characteristic.valueHandle, 0, err => { if (err) { this.emit('error', _makeError('Read characteristic value failed', err)); } }); } /** * Writes the value of a GATT characteristic. * * @param {string} characteristicId Unique ID of the GATT characteristic. * @param {array} value The value (array of bytes) to be written. * @param {boolean} ack Require acknowledge from device, irrelevant in GATTS role. * @param {function(Error)} completeCallback Callback signature: err => {} * @param {function} deviceNotifiedOrIndicated TODO * @returns {void} */ writeCharacteristicValue(characteristicId, value, ack, completeCallback, deviceNotifiedOrIndicated) { const characteristic = this.getCharacteristic(characteristicId); if (!characteristic) { throw new Error('Characteristic value write failed: Could not get characteristic with id ' + characteristicId); } if (this._instanceIdIsOnLocalDevice(characteristicId)) { this._writeLocalValue(characteristic, value, 0, completeCallback, deviceNotifiedOrIndicated); return; } const device = this._getDeviceByCharacteristicId(characteristicId); if (!device) { throw new Error('Characteristic value write failed: Could not get device'); } if (this._gattOperationsMap[device.instanceId]) { throw new Error('Characteristic value write failed: A gatt operation already in progress with device id ' + device.instanceId); } this._gattOperationsMap[device.instanceId] = { callback: completeCallback, bytesWritten: 0, value: value.slice(), attribute: characteristic }; if (value.length > this._maxShortWritePayloadSize(device.instanceId)) { if (!ack) { delete this._gattOperationsMap[device.instanceId]; throw new Error('Long writes do not support BLE_GATT_OP_WRITE_CMD'); } this._gattOperationsMap[device.instanceId].bytesWritten = this._maxLongWritePayloadSize(device.instanceId); this._longWrite(device, characteristic, value, completeCallback); } else { this._gattOperationsMap[device.instanceId].bytesWritten = value.length; this._shortWrite(device, characteristic, value, ack, completeCallback); } } _getDeviceByDescriptorId(descriptorId) { const descriptor = this._descriptors[descriptorId]; if (!descriptor) { throw new Error('No descriptor found with descriptor id: ' + descriptorId); } return this._getDeviceByCharacteristicId(descriptor.characteristicInstanceId); } _getDeviceByCharacteristicId(characteristicId) { const characteristic = this._characteristics[characteristicId]; if (!characteristic) { throw new Error('No characteristic found with id: ' + characteristicId); } const service = this._services[characteristic.serviceInstanceId]; if (!service) { throw new Error('No service found with id: ' + characteristic.serviceInstanceId); } const device = this._devices[service.deviceInstanceId]; if (!device) { throw new Error('No device found with id: ' + service.deviceInstanceId); } return device; } _isCCCDDescriptor(descriptorId) { const descriptor = this._descriptors[descriptorId]; return descriptor && ((descriptor.uuid === '0000290200001000800000805F9B34FB') || (descriptor.uuid === '2902')); } _getCCCDOfCharacteristic(characteristicId) { return _.find(this._descriptors, descriptor => { return (descriptor.characteristicInstanceId === characteristicId) && (this._isCCCDDescriptor(descriptor.instanceId)); }); } _instanceIdIsOnLocalDevice(instanceId) { return instanceId.split('.')[0] === 'local'; } /** * Reads the value of a GATT descriptor. * * @param {string} descriptorId Unique ID of of the GATT descriptor. * @param {function(Error, number[])} [callback] Callback signature: (err, readBytes) => {} where `readBytes` is an * array of numbers corresponding to the value of the GATT * descriptor. * @returns {void} */ readDescriptorValue(descriptorId, callback) { const descriptor = this.getDescriptor(descriptorId); if (!descriptor) { throw new Error('Descriptor read failed: could not get descriptor with id ' + descriptorId); } if (this._instanceIdIsOnLocalDevice(descriptorId)) { this._readLocalValue(descriptor, callback); return; } const device = this._getDeviceByDescriptorId(descriptorId); if (!device) { throw new Error('Descriptor read failed: Could not get device'); } if (this._gattOperationsMap[device.instanceId]) { throw new Error('Descriptor read failed: A gatt operation already in progress with device with id ' + device.instanceId); } this._gattOperationsMap[device.instanceId] = { callback: callback, readBytes: [] }; this._adapter.gattcRead(device.connectionHandle, descriptor.handle, 0, err => { if (err) { this.emit('error', _makeError('Read descriptor value failed', err)); } }); } /** * Writes the value of a GATT descriptor. * * @param {string} descriptorId Unique ID of the GATT descriptor. * @param {array} value The value (array of bytes) to be written. * @param {boolean} ack Require acknowledge from device, irrelevant in GATTS role. * @param {function(Error)} [callback] Callback signature: err => {}. * (not called until ack is received if `requireAck`). * options: {ack, long, offset} * @returns {void} */ writeDescriptorValue(descriptorId, value, ack, callback) { // Does not support reliable write const descriptor = this.getDescriptor(descriptorId); if (!descriptor) { throw new Error('Descriptor write failed: could not get descriptor with id ' + descriptorId); } if (this._instanceIdIsOnLocalDevice(descriptorId)) { this._writeLocalValue(descriptor, value, 0, callback); return; } const device = this._getDeviceByDescriptorId(descriptorId); if (!device) { throw new Error('Descriptor write failed: Could not get device'); } if (this._gattOperationsMap[device.instanceId]) { throw new Error('Descriptor write failed: A gatt operation already in progress with device with id ' + device.instanceId); } this._gattOperationsMap[device.instanceId] = { callback: callback, bytesWritten: 0, value: value.slice(), attribute: descriptor }; if (value.length > this._maxShortWritePayloadSize(device.instanceId)) { if (!ack) { delete this._gattOperationsMap[device.instanceId]; throw new Error('Long writes do not support BLE_GATT_OP_WRITE_CMD'); } this._gattOperationsMap[device.instanceId].bytesWritten = this._maxLongWritePayloadSize(device.instanceId); this._longWrite(device, descriptor, value, callback); } else { this._gattOperationsMap[device.instanceId].bytesWritten = value.length; this._shortWrite(device, descriptor, value, ack, callback); } } _shortWrite(device, attribute, value, ack, callback) { const writeParameters = { write_op: ack ? this._bleDriver.BLE_GATT_OP_WRITE_REQ : this._bleDriver.BLE_GATT_OP_WRITE_CMD, flags: 0, // don't care for WRITE_REQ / WRITE_CMD handle: attribute.handle, offset: 0, len: value.length, value, }; Promise.resolve() .then(() => { if (ack) { return this._shortWriteWithResponse(device, writeParameters); } return this._shortWriteWithoutResponse(device, writeParameters) .then(() => { delete this._gattOperationsMap[device.instanceId]; attribute.value = value; if (callback) { callback(undefined, attribute); } }); }) .catch(err => { delete this._gattOperationsMap[device.instanceId]; const error = _makeError(`Failed to write to attribute with handle: ${attribute.handle}: ${err.message}`); this.emit('error', error); if (callback) callback(error); }); } _shortWriteWithResponse(device, writeParameters) { return new Promise((resolve, reject) => { this._adapter.gattcWrite(device.connectionHandle, writeParameters, err => { if (err) { reject(err); } else { resolve(); } }); }); } _shortWriteWithoutResponse(device, writeParameters) { let timeoutId; return Promise.race([ new Promise((resolve, reject) => { const txCompleteHandler = txCompleteDevice => { if (device.connectionHandle === txCompleteDevice.connectionHandle) { this.removeListener('txComplete', txCompleteHandler); clearTimeout(timeoutId); resolve(); } }; this.on('txComplete', txCompleteHandler); this._adapter.gattcWrite(device.connectionHandle, writeParameters, err => { if (err) reject(err); }); }), new Promise((resolve, reject) => { timeoutId = setTimeout(() => { reject(_makeError('Timed out while waiting for BLE_EVT_TX_COMPLETE')); }, 4000); }), ]); } _longWrite(device, attribute, value, callback) { if (value.length < this._maxShortWritePayloadSize(device.instanceId)) { throw new Error('Wrong write method. Use regular write for payload sizes < ' + this._maxShortWritePayloadSize(device.instanceId)); } const writeParameters = { write_op: this._bleDriver.BLE_GATT_OP_PREP_WRITE_REQ, flags: this._bleDriver.BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE, handle: attribute.handle, offset: 0, len: this._maxLongWritePayloadSize(device.instanceId), value: value.slice(0, this._maxLongWritePayloadSize(device.instanceId)), }; this._adapter.gattcWrite(device.connectionHandle, writeParameters, err => { if (err) { this._longWriteCancel(device, attribute); this.emit('error', _makeError('Failed to write value to device/handle ' + device.instanceId + '/' + attribute.handle, err)); return; } }); } _longWriteCancel(device, attribute) { const gattOperation = this._gattOperationsMap[device.instanceId]; const writeParameters = { write_op: this._bleDriver.BLE_GATT_OP_EXEC_WRITE_REQ, flags: this._bleDriver.BLE_GATT_EXEC_WRITE_FLAG_PREPARED_CANCEL, handle: attribute.handle, offset: 0, len: 0, value: [], }; this._adapter.gattcWrite(device.connectionHandle, writeParameters, err => { delete this._gattOperationsMap[device.instanceId]; if (err) { this.emit('error', _makeError('Failed to cancel failed long write', err)); gattOperation.callback('Failed to write and failed to cancel write'); } else { gattOperation.callback('Failed to write value to device/handle ' + device.instanceId + '/' + attribute.handle); } }); } _sendingNotificationsAndIndicationsComplete() { return this._pendingNotificationsAndIndications.sentAllNotificationsAndIndications && this._pendingNotificationsAndIndications.remainingNotificationCallbacks === 0 && this._pendingNotificationsAndIndications.remainingIndicationConfirmations === 0; } _writeLocalValue(attribute, value, offset, completeCallback, deviceNotifiedOrIndicated) { const writeParameters = { len: value.length, offset: offset, value: value, }; if (!this._instanceIdIsOnLocalDevice(attribute.instanceId)) { this.emit('error', _makeError('Attribute was not a local attribute')); return; } // TODO: Do we know that the attributes are the correct attributes? if (attribute.uuid === '2A00') { // TODO: At some point addon should accept string. // TODO: Fix write perm to be same as set at server setup. this._setDeviceNameFromArray(value, ['open'], err => { if (err) { completeCallback(err); } attribute.value = value; completeCallback(undefined, attribute); }); return; } // TODO: Fix Appearance uuid magic number if (attribute.uuid === '2A01') { this._setAppearanceFromArray(value, err => { if (err) { completeCallback(err); } attribute.value = value; completeCallback(undefined, attribute); }); return; } // TODO: Fix Peripheral Preferred Connection Parameters uuid magic number if (attribute.uuid === '2A04') { this._setPPCPFromArray(value, err => { if (err) { completeCallback(err); } attribute.value = value; completeCallback(undefined, attribute); }); return; } // TODO: Figure out if we should use hvx? const cccdDescriptor = this._getCCCDOfCharacteristic(attribute.instanceId); let sentHvx = false; if (cccdDescriptor) { // TODO: This is probably way to simple, do we need a map of devices indication is sent to? this._pendingNotificationsAndIndications = { completeCallback, deviceNotifiedOrIndicated, sentAllNotificationsAndIndications: false, remainingNotificationCallbacks: 0, remainingIndicationConfirmations: 0, }; for (let deviceInstanceId in this._devices) { const cccdValue = cccdDescriptor.value[deviceInstanceId][0]; const sendIndication = cccdValue & 2; const sendNotification = !sendIndication && (cccdValue & 1); if (sendNotification || sendIndication) { const device = this._devices[deviceInstanceId]; const hvxParams = { handle: attribute.valueHandle, type: sendIndication || sendNotification, offset: offset, len: value.length, data: value, }; sentHvx = true; if (sendNotification) { this._pendingNotificationsAndIndications.remainingNotificationCallbacks++; } else if (sendIndication) { this._pendingNotificationsAndIndications.remainingIndicationConfirmations++; } this._adapter.gattsHVX(device.connectionHandle, hvxParams, err => { if (err) { if (sendNotification) { this._pendingNotificationsAndIndications.remainingNotificationCallbacks--; } else if (sendIndication) { this._pendingNotificationsAndIndications.remainingIndicationConfirmations--; } this.emit('error', _makeError('Failed to send notification', err)); if (this._sendingNotificationsAndIndicationsComplete()) { completeCallback(_makeError('Failed to send notification or indication', err)); this._pendingNotificationsAndIndications = {}; } return; } this._setAttributeValueWithOffset(attribute, value, offset); if (sendNotification) { if (deviceNotifiedOrIndicated) { deviceNotifiedOrIndicated(device, attribute); } this.emit('deviceNotifiedOrIndicated', device, attribute); this._pendingNotificationsAndIndications.remainingNotificationCallbacks--; if (this._sendingNotificationsAndIndicationsComplete()) { completeCallback(undefined); this._pendingNotificationsAndIndications = {}; } } else if (sendIndication) { return; } }); } } this._pendingNotificationsAndIndications.sentAllNotificationsAndIndications = true; } if (sentHvx) { if (this._sendingNotificationsAndIndicationsComplete()) { completeCallback(undefined); this._pendingNotificationsAndIndications = {}; } return; } this._adapter.gattsSetValue(this._bleDriver.BLE_CONN_HANDLE_INVALID, attribute.handle, writeParameters, (err, writeResult) => { if (err) { this.emit('error', _makeError('Failed to write local value', err)); completeCallback(err, undefined); return; } this._setAttributeValueWithOffset(attribute, value, offset); completeCallback(undefined, attribute); }); } _readLocalValue(attribute, callback) { const readParameters = { len: 512, offset: 0, value: [], }; this._adapter.gattsGetValue(this._bleDriver.BLE_CONN_HANDLE_INVALID, attribute, readParameters, (err, readResults) => { if (err) { this.emit('error', _makeError('Failed to write local value', err)); if (callback) callback(err, undefined); return; } attribute.value = readResults.value; if (callback) { callback(undefined, attribute); } }); } /** * Starts notifications on a GATT characteristic. * * Only for GATT central role. * * @param {string} characteristicId Unique ID of the GATT characteristic. * @param {boolean} requireAck Require all notifications to ack. * @param {function(Error)} [callback] Callback signature: err => {}. * (not called until ack is received if `requireAck`). * @returns {void} */ startCharacteristicsNotifications(characteristicId, requireAck, callback) { // TODO: If CCCD not discovered do a decriptor discovery const enableNotificationBitfield = requireAck ? 2 : 1; const characteristic = this._characteristics[characteristicId]; if (!characteristic) { throw new Error('Start characteristic notifications failed: Could not get characteristic with id ' + characteristicId); } const cccdDescriptor = this._getCCCDOfCharacteristic(characteristicId); if (!cccdDescriptor) { throw new Error('Start characteristic notifications failed: Could not find CCCD descriptor with parent characteristic id: ' + characteristicId); } this.writeDescriptorValue(cccdDescriptor.instanceId, [enableNotificationBitfield, 0], true, err => { if (err) { this.emit('error', 'Failed to start characteristics notifications'); } if (callback) { callback(err); } }); } /** * Disables notifications on a GATT characteristic. * * @param {string} characteristicId Unique ID of the GATT characteristic. * @param {function(Error)} [callback] Callback signature: err => {}. * @returns {void} */ stopCharacteristicsNotifications(characteristicId, callback) { // TODO: If CCCD not discovered how did we start it? const disableNotificationBitfield = 0; const characteristic = this._characteristics[characteristicId]; if (!characteristic) { throw new Error('Stop characteristic notifications failed: Could not get characteristic with id ' + characteristicId); } const cccdDescriptor = this._getCCCDOfCharacteristic(characteristicId); if (!cccdDescriptor) { throw new Error('Stop characteristic notifications failed: Could not find CCCD descriptor with parent characteristic id: ' + characteristicId); } this.writeDescriptorValue(cccdDescriptor.instanceId, [disableNotificationBitfield, 0], true, err => { if (err) { this.emit('error', 'Failed to stop characteristics notifications'); } if (callback) { callback(err); } }); } phyUpdate(deviceInstanceId, phys, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { throw new Error('No device with instance id: ' + deviceInstanceId); } this._adapter.gapPhyUpdate(device.connectionHandle, phys, err => { if (err) { const errorObject = _makeError('Failed to update phys', err); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } if (callback) { callback(); } }); } dataLengthUpdate(deviceInstanceId, params, callback) { const device = this.getDevice(deviceInstanceId); if (!device) { throw new Error('No device with instance id: ' + deviceInstanceId); } this._adapter.gapDataLengthUpdate(device.connectionHandle, params, err => { if (err) { const errorObject = _makeError('Failed to update data length', err); this.emit('error', errorObject); if (callback) { callback(errorObject); } return; } if (callback) { callback(); } }); } } module.exports = Adapter;