adapter.js

/* Copyright (c) 2010 - 2017, 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');

/** 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() {
        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: 247, // 247 is max att mtu size
            },
        };
    }

    /**
     * @summary Initialize the adapter.
     *
     * The serial port will be attempted to be opened with the configured serial port settings in
     * <code>adapterOptions</code>.
     *
     * @param {Object} options Options to initialize/open this adapter with.
     * Available adapter open options:
     * <ul>
     * <li>{number} [baudRate=115200]: The baud rate this adapter's serial port should be configured with.
     * <li>{string} [parity='none']: The parity this adapter's serial port should be configured with.
     * <li>{string} [flowControl='none']: Whether flow control should be configured with this adapter's serial port.
     * <li>{number} [eventInterval=0]: Interval to use for sending BLE driver events to JavaScript.
     *                                 If `0`, events will be sent as soon as they are received from the BLE driver.
     * <li>{string} [logLevel='info']: The verbosity of logging the developer wants with this adapter.
     * <li>{number} [retransmissionInterval=250]: The time interval to wait between retransmitted packets.
     * <li>{number} [responseTimeout=1500]: Response timeout of the data link layer.
     * <li>{boolean} [enableBLE=true]: Whether the BLE stack should be initialized and enabled.
     * </ul>
     * @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: 115200,
                parity: 'none',
                flowControl: 'none',
                eventInterval: 0,
                logLevel: 'info',
                retransmissionInterval: 250,
                responseTimeout: 1500,
                enableBLE: true,
            };
        } else {
            if (!options.baudRate) options.baudRate = 115200;
            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 = 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 <code>Adapter</code>.
             */
            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 <code>Adapter</code>.
                 */
                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:
     * <ul>
     * <li>{number} eventCallbackTotalTime
     * <li>{number} eventCallbackTotalCount
     * <li>{number} eventCallbackBatchMaxCount
     * <li>{number} eventCallbackBatchAvgCount
     * </ul>
     *
     * @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:
     * <ul>
     * <li>{Object} gap_enable_params: GAP init parameters
     *                <ul>
     *                <li>{number} periph_conn_count: Number of connections acting as a peripheral.
     *                <li>{number} central_conn_count: Number of connections acting as a central.
     *                <li>{number} central_sec_count: Number of SMP instances for all connections acting as a central.
     *                </ul>
     * <li>{Object} gatts_enable_params: GATTS init parameters
     *                <ul>
     *                <li>{boolean} service_changed: Include the Service Changed characteristic in the Attribute Table.
     *                <li>{number} attr_tab_size: Attribute Table size in bytes. The size must be a multiple of 4.
     *                </ul>
     * <li>{Object} common_enable_params: Common init parameters
     *                <ul>
     *                <li>{null|number} conn_bw_counts: Bandwidth configuration parameters or null for defaults.
     *                <li>{number} vs_uuid_count: Maximum number of 128-bit, Vendor Specific UUID bases to allocate.
     *                </ul>
     * <li>{Object} gatt_enable_params: GATT init parameters
     *                <ul>
     *                <li>{number} att_mtu: Maximum size of ATT packet the SoftDevice can send or receive.
     *                                      If it is 0 then @ref GATT_MTU_SIZE_DEFAULT will be used.
     *                                      Otherwise @ref GATT_MTU_SIZE_DEFAULT is the minimum value.
     *                </ul>
     * </ul>
     * @param {function(Error, Object, number)} [callback] Callback signature: (err, parameters, app_ram_base) => {}
     *                                                   where `parameters` is the BLE initialization parameters as
     *                                                   described above and `app_ram_base` is the minimum start address
     *                                                   of the application RAM region required by the SoftDevice for
     *                                                   this configuration.
     * @returns {void}
     */
    enableBLE(options, callback) {
        if (options === undefined || options === null) {
            options = this._getDefaultEnableBLEParams();
        }

        this._adapter.enableBLE(
            options,
            (err, parameters, app_ram_base) => {
                if (this._checkAndPropagateError(err, 'Enabling BLE failed.', callback)) { return; }
                this._changeState({ bleEnabled: true });
                if (callback) {
                    callback(err, parameters, app_ram_base);
                }
            });
    }

    _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_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:
                    this._parseTxCompleteEvent(event);
                    break;
                case this._bleDriver.BLE_EVT_DATA_LENGTH_CHANGED:
                    this._parseDataLengthChangedEvent(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._changeState({ connecting: false });

        if (deviceRole === 'central') {
            this._changeState({ advertising: false });
        }

        /**
         * Connection established.
         *
         * @event Adapter#deviceConnected
         * @type {Object}
         * @property {Device} device - The <code>Device</code> 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 <code>Device</code> 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 <code>Device</code> 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 <code>Device</code> 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 <code>Device</code> 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 <code>Device</code> 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 <code>Device</code> 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 <code>Device</code> instance representing the BLE peer we're connected to.
         * @property {number} event.match_request - If 1 requires the application to report the match using
                                                    <code>replyAuthKey</code>.
         * @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 <code>Device</code> 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 <code>Device</code> 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 <code>Device</code> 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 <code>replyLescDhkey</code> 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 <code>Device</code> 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 <code>Device</code> 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 <code>Device</code> instance representing the BLE peer we're connected to.
         * @property {Object} connectionParameters - GAP Connection Parameters.
         */
        this.emit('connParamUpdateRequest', device, connectionParameters);
    }

    _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 <code>Device</code> 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 <code>Device</code> 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 <code>Adapter</code>'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];

            const addVsUuidToDriver = uuid => {
                return new Promise((resolve, reject) => {
                    this._converter.uuidToDriver(uuid, (err, uuid) => {
                        if (err) {
                            reject(err);
                            return;
                        }

                        resolve();
                    });
                });
            };

            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);
                addVsUuidToDriver(attribute.uuid)
                .then(() => this.emit('serviceAdded', attribute))
                .catch(err => {
                    delete this._gattOperationsMap[device.instanceId];
                    this.emit('error', _makeError('addVsUuidToDriver error', err));
                    gattOperation.callback('Failed to add service uuid to driver');
                });

                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 <code>Adapter</code>'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));
                    addVsUuidToDriver(attribute.uuid)
                    .then(() => emitCharacteristicAdded())
                    .catch(err => {
                        delete this._gattOperationsMap[device.instanceId];
                        this.emit('error', _makeError('addVsUuidToDriver error', err));
                        gattOperation.callback('Failed to add characteristic uuid to driver');
                    });
                } 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 <code>Adapter</code>'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 <code>Adapter</code>'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 <code>Adapter</code>'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) {
            /**
             * Exchange MTU Response event.
             *
             * @event Adapter#attMtuChanged
             * @type {Object}
             * @property {Device} device - The <code>Device</code> 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 <code>Device</code> 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 remoteDevice = this._getDeviceByConnectionHandle(event.conn_handle);

        this._adapter.gattsExchangeMtuReply(event.conn_handle, event.client_rx_mtu, error => {
            if (error) {
                this.emit('error', _makeError('Failed to call gattsExchangeMtuReply', error));
                return;
            }

            const previousMtu = this._attMtuMap[remoteDevice.instanceId];
            const newMtu = event.client_rx_mtu;
            this._attMtuMap[remoteDevice.instanceId] = newMtu;

            if (newMtu !== previousMtu);
            this.emit('attMtuChanged', remoteDevice, 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 <code>Device</code> instance representing the BLE peer we've connected to.
         * @property {number} event.count - Number of packets transmitted.
         */
        this.emit('txComplete', remoteDevice, event.count);
    }

    _parseDataLengthChangedEvent(event) {
        const remoteDevice = this._getDeviceByConnectionHandle(event.conn_handle);
        /**
         * Link layer PDU length changed.
         *
         * @event Adapter#dataLengthChanged
         * @type {Object}
         * @property {Device} remoteDevice - The <code>Device</code> instance representing the BLE peer we've connected to.
         * @property {number} event.max_tx_octets - The maximum number of payload octets in a Link Layer Data Channel
         *                                          PDU that the local Controller will send. Range: 27-251
         */
        this.emit('dataLengthChanged', remoteDevice, event.max_tx_octets);
    }

    _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:
     * <ul>
     * <li>{boolean} active If 1, perform active scanning (scan requests).
     * <li>{number} interval Scan interval between 0x0004 and 0x4000 in 0.625ms units (2.5ms to 10.24s).
     * <li>{number} window Scan window between 0x0004 and 0x4000 in 0.625ms units (2.5ms to 10.24s).
     * <li>{number} timeout Scan timeout between 0x0001 and 0xFFFF in seconds, 0x0000 disables timeout.
     * <li>{number} use_whitelist If 1, filter advertisers using current active whitelist.
     * <li>{number} adv_dir_report If 1, also report directed advertisements where the initiator field is set to a
     *                             private resolvable address, even if the address did not resolve to an entry in the
     *                             device identity list. A report will be generated even if the peer is not in the whitelist.
     * </ul>
     * @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:
     * <ul>
     * <li>{Object} scanParams:
     *                <ul>
     *                <li>{boolean} active: If 1, perform active scanning (scan requests).
     *                <li>{number} interval: Scan interval between 0x0004 and 0x4000 in 0.625ms units (2.5ms to 10.24s).
     *                <li>{number} window: Scan window between 0x0004 and 0x4000 in 0.625ms units (2.5ms to 10.24s).
     *                <li>{number} timeout: Scan timeout between 0x0001 and 0xFFFF in seconds, 0x0000 disables timeout.
     *                <li>{number} use_whitelist: If 1, filter advertisers using current active whitelist.
     *                <li>{number} adv_dir_report: If 1, also report directed advertisements where the initiator field is set to a
     *                                             private resolvable address, even if the address did not resolve to an entry in the
     *                                             device identity list. A report will be generated even if the peer is not in the whitelist.
     *                </ul>
     * <li>{Object} connParams:
     *                <ul>
     *                <li>{number} min_conn_interval: Minimum Connection Interval in 1.25 ms units.
     *                <li>{number} max_conn_interval:  Maximum Connection Interval in 1.25 ms units.
     *                <li>{number} slave_latency: Slave Latency in number of connection events.
     *                <li>{number} conn_sup_timeout: Connection Supervision Timeout in 10 ms units.
     *                </ul>
     * </ul>
     * @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:
     * <ul>
     * <li>{number} channelMask: Channel mask for RF channels used in advertising. Default: all channels on.
     * <li>{number} interval: GAP Advertising interval. Required: an interval must be provided.
     * <li>{number} timeout: Maximum advertising time in limited discoverable mode. Required: a timeout must be provided.
     * <li>{boolean} connectable: GAP Advertising type connectable. Default: The device is connectable.
     * <li>{boolean} scannable: GAP Advertising type scannable. Default: The device is undirected.
     * </ul>
     * @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:
     * <ul>
     * <li>{number} min_conn_interval: Minimum Connection Interval in 1.25 ms units.
     * <li>{number} max_conn_interval:  Maximum Connection Interval in 1.25 ms units.
     * <li>{number} slave_latency: Slave Latency in number of connection events.
     * <li>{number} conn_sup_timeout: Connection Supervision Timeout in 10 ms units.
     * </ul>
     * @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:
     * <ul>
     *     <li>The Client RX MTU value, and
     *     <li>The Server RX MTU value from `BLE_GATTC_EVT_EXCHANGE_MTU_RSP`.
     * </ul>
     *
     *     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 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:
     * <ul>
     * <li>{boolean} bond Perform bonding.
     * <li>{boolean} lesc Enable LE Secure Connection pairing.
     * <li>{boolean} keypress Enable generation of keypress notifications.
     * <li>{Object} io_caps IO capabilities, see @ref BLE_GAP_IO_CAPS.
     * <li>{boolean} oob Out Of Band data available.
     * <li>{number} min_key_size Minimum encryption key size in octets between 7 and 16. If 0 then not applicable in this instance.
     * <li>{number} max_key_size Maximum encryption key size in octets between min_key_size and 16.
     * <li>{Object} kdist_own Key distribution bitmap: keys that the local device will distribute.
     *                <ul>
     *                <li>{boolean} enc Long Term Key and Master Identification.
     *                <li>{boolean} id Identity Resolving Key and Identity Address Information.
     *                <li>{boolean} sign Connection Signature Resolving Key.
     *                <li>{boolean} link Derive the Link Key from the LTK.
     *                </ul>
     * <li>{Object} kdist_peer Key distribution bitmap: keys that the remote device will distribute.
     *                <ul>
     *                <li>^^ Same as properties as `kdist_own` above. ^^
     *                </ul>
     * </ul>
     * @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.
     * <ul>
     *            <li>{Object} kdist_own Key distribution bitmap: keys that the local device will distribute.
     *                <ul>
     *                <li>{boolean} enc Long Term Key and Master Identification.
     *                <li>{boolean} id Identity Resolving Key and Identity Address Information.
     *                <li>{boolean} sign Connection Signature Resolving Key.
     *                <li>{boolean} link Derive the Link Key from the LTK.
     *                </ul>
     *            <li>{Object} kdist_peer Key distribution bitmap: keys that the remote device will distribute.
     *                <ul>
     *                <li>^^ Same as properties as `kdist_own` above. ^^
     *                </ul>
     * </ul>
     * @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._adapter.addVendorspecificUUID(
                                { uuid128: 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, callback) {
        // TODO: Do read on service? callback?
        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'));
                }, 2000);
            }),
        ]);
    }

    _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); }
        });
    }
}

module.exports = Adapter;