/* 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:
*
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:
* 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:
*