device.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';

function _camelCaseFlag(flag) {
    const advFlagsPrefix = 'BLE_GAP_ADV_FLAG';

    if (flag.indexOf(advFlagsPrefix) === 0) {
        // remove unnecessary prefix and lowercase the string
        const flagCamelCase = flag.replace(
            /^BLE_GAP_ADV_FLAG[S]?_(.*)/g,
            ($1, $2) => {
                return $2.toLowerCase()
                    .replace(/(_[a-z])/g,
                        $3 => {
                            const camelCase = $3.toUpperCase().replace('_', '');
                            return camelCase[0].toUpperCase() + camelCase.slice(1);
                        });
            });

        return flagCamelCase[0].toUpperCase() + flagCamelCase.slice(1);
    }

    return `NOT_ABLE_TO_CAMELCASE_THIS_FLAG: ${flag}`;
}

/**
 * Class representing a Bluetooth device.
 */
class Device {
    /**
     * Create a Bluetooth device.
     *
     * @constructor
     * @param {string|Object} address The local Bluetooth identity address.
     * @param {string} role The BLE role of this device.
     */
    constructor(address, role) {
        this._instanceId = null;
        this._address = {};

        if (typeof address === 'string') {
            this._address = address;
            this._addressType = 'BLE_GAP_ADDR_TYPE_RANDOM_STATIC';
        } else {
            this._address = address.address;
            this._addressType = address.type;
        }

        this.adData = {};

        this.name = null;
        this._role = role;
        this.services = [];
        this.flags = [];
        this.scanResponse = null;

        this.connected = false;
        this.rssi = null;
        this.txPower = null;
        this._connectionHandle = null;

        this.minConnectionInterval = null;
        this.maxConnectionInterval = null;
        this.slaveLatency = null;
        this.connectionSupervisionTimeout = null;

        this.paired = false;

        // local adapter peripheral initiated a pairing procedure
        this.ownPeriphInitiatedPairingPending = false;
    }

    /**
     * Get the instanceId of this device.
     * @returns {null|string} Unique ID of this device. null if not connected.
     */
    get instanceId() {
        return this._instanceId;
    }

    /**
     * Get the bluetooth address of this device. 'local.server': local/adapter, non-'local.server': other device.
     * @returns {string} Bluetooth address of this device.
     */
    get address() {
        return this._address;
    }

    /**
     * Get the BLE address type. 'BLE_GAP_ADDR_TYPE_RANDOM_STATIC' (default) or `BLE_GAP_ADDR_TYPE_PUBLIC`.
     * @returns {string} BLE address type of this device.
     */
    get addressType() {
        return this._addressType;
    }

    /**
     * Get the BLE role of this device. `BLE_GAP_ROLE_CENTRAL` or `BLE_GAP_ROLE_PERIPH`.
     * @returns {string} BLE role of this device.
     */
    get role() {
        return this._role;
    }

    /**
     * Get the BLE connection handle of this device.
     * @returns {string|null} BLE connection handle of this device. null if not connected.
     */
    get connectionHandle() {
        return this._connectionHandle;
    }

    /**
     * Method that sets `_connectionHandle` and `_instanceId` upon establishing a BLE connection.
     *
     * Called on `BLE_GAP_EVT_CONNECTED`.
     *
     * @param {string} connectionHandle The BLE connection handle of the device.
     */
    set connectionHandle(connectionHandle) {
        // TODO: possible to set connectionHandle to undefined? will instanceID be correct?
        this._connectionHandle = connectionHandle;

        // TODO: should instanceId involve role or is that handled by connectionHandle?
        this._instanceId = `${this._address}.${connectionHandle}`;
    }

    /**
     * Method that initializes `Device` as a discovered BLE peripheral.
     *
     * Called on `BLE_GAP_EVT_ADV_REPORT`.
     *
     * @param {Object} event Advertising report event received from SoftDevice.
     * @returns {void}
     */
    processEventData(event) {
        this.adData = event.data;
        this.time = new Date(event.time);
        this.scanResponse = event.scan_rsp;
        this.rssi = event.rssi;
        this.advType = event.adv_type;
        this.txPower = event.data ? event.data.BLE_GAP_AD_TYPE_TX_POWER_LEVEL : undefined;
        this._findAndSetNameFromAdvertisingData(event.data);
        this._processAndSetServiceUuidsFromAdvertisingData(event.data);
        this._processFlagsFromAdvertisingData(event.data);
        this._setRssiLevel();
    }

    _findAndSetNameFromAdvertisingData(advertisingData) {
        if (advertisingData) {
            if (advertisingData.BLE_GAP_AD_TYPE_LONG_LOCAL_NAME) {
                this.name = advertisingData.BLE_GAP_AD_TYPE_LONG_LOCAL_NAME;
            } else if (advertisingData.BLE_GAP_AD_TYPE_SHORT_LOCAL_NAME) {
                this.name = advertisingData.BLE_GAP_AD_TYPE_SHORT_LOCAL_NAME;
            } else if (advertisingData.BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME) {
                this.name = advertisingData.BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME;
            }
        } else {
            this.name = '';
        }
    }

    _processFlagsFromAdvertisingData(advertisingData) {
        if (advertisingData && advertisingData.BLE_GAP_AD_TYPE_FLAGS) {
            this.flags = advertisingData.BLE_GAP_AD_TYPE_FLAGS.map(_camelCaseFlag);
        }
    }

    _processAndSetServiceUuidsFromAdvertisingData(advertisingData) {
        if (!advertisingData) {
            return;
        }

        let uuids = [];

        if (advertisingData.BLE_GAP_AD_TYPE_16BIT_SERVICE_UUID_MORE_AVAILABLE) {
            uuids = uuids.concat(advertisingData.BLE_GAP_AD_TYPE_16BIT_SERVICE_UUID_MORE_AVAILABLE);
        }

        if (advertisingData.BLE_GAP_AD_TYPE_16BIT_SERVICE_UUID_COMPLETE) {
            uuids = uuids.concat(advertisingData.BLE_GAP_AD_TYPE_16BIT_SERVICE_UUID_COMPLETE);
        }

        /*
        if (event.data.BLE_GAP_AD_TYPE_32BIT_SERVICE_UUID_MORE_AVAILABLE) {
            uuids = uuids.concat(event.data.BLE_GAP_AD_TYPE_32BIT_SERVICE_UUID_MORE_AVAILABLE);
        }

        if (event.data.BLE_GAP_AD_TYPE_32BIT_SERVICE_UUID_COMPLETE) {
            uuids = uuids.concat(event.data.BLE_GAP_AD_TYPE_32BIT_SERVICE_UUID_COMPLETE);
        }
        */

        if (advertisingData.BLE_GAP_AD_TYPE_128BIT_SERVICE_UUID_MORE_AVAILABLE) {
            uuids = uuids.concat(advertisingData.BLE_GAP_AD_TYPE_128BIT_SERVICE_UUID_MORE_AVAILABLE);
        }

        if (advertisingData.BLE_GAP_AD_TYPE_128BIT_SERVICE_UUID_COMPLETE) {
            uuids = uuids.concat(advertisingData.BLE_GAP_AD_TYPE_128BIT_SERVICE_UUID_COMPLETE);
        }

        this.services = uuids;
    }

    _setRssiLevel() {
        const MIN_RSSI = -100;
        const MAX_RSSI = -45;

        function mapRange(n, fromMin, fromMax, toMin, toMax) {
            // scale number n from the range [fromMin, fromMax] to [toMin, toMax]
            let scaledN = toMin + (((toMax - toMin) / (fromMax - fromMin)) * (n - fromMin));
            scaledN = Math.round(scaledN);
            return Math.max(toMin, Math.min(toMax, scaledN));
        }

        this.rssi_level = mapRange(this.rssi, MIN_RSSI, MAX_RSSI, 4, 20);
    }
}

module.exports = Device;