adapterFactory.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 os = require('os');

const _bleDriverV2 = require('bindings')('pc-ble-driver-js-sd_api_v2');
const _bleDriverV3 = require('bindings')('pc-ble-driver-js-sd_api_v3');

const Adapter = require('./adapter');
const logLevel = require('./util/logLevel');
const EventEmitter = require('events');

const _bleDrivers = { v2: _bleDriverV2, v3: _bleDriverV3 };
const _singleton = Symbol('Ensure that only one instance of AdapterFactory ever exists.');

/** @constant {number} Update interval, in milliseconds, at which PC shall be checked for new connected adapters. */
const UPDATE_INTERVAL_MS = 2000;

/**
 * Adapter added event. Fired when a new devkit is found.
 *
 * @event AdapterFactory#added
 * @param {object} adapter The adapter instance that was added.
 */

/**
 * Adapter removed event. Fired when a devkit is removed.
 *
 * @event AdapterFactory#removed
 * @param {object} adapter The adapter instance that was removed.
 */

/**
 * Adapter opened event.
 *
 * @event AdapterFactory#adapterOpened
 * @param {object} adapter The adapter instance that was opened.
 */

/**
 * Adapter closed event.
 *
 * @event AdapterFactory#adapterClosed
 * @param {object} adapter The adapter instance that was closed.
 */

/**
 * Error event.
 *
 * @event AdapterFactory#error
 * @param {Error} error Error object with a human-readable message.
 */

/**
 * Log message event.
 *
 * @event AdapterFactory#logMessage
 * @param {string} severity Severity of the log event.
 * @param {string} message Human-readable log message.
 */

/**
 * Class that provides Adapters through the use of the pc-ble-driver AddOn and the internal `Adapter` class.
 *
 * @fires AdapterFactory#added
 * @fires AdapterFactory#removed
 * @fires AdapterFactory#adapterOpened
 * @fires AdapterFactory#adapterClosed
 * @fires AdapterFactory#error
 * @fires AdapterFactory#logMessage
 */
class AdapterFactory extends EventEmitter {
    /**
     * Shall not be called by user. Called by the factory method `this.getInstance()`.
     *
     * @private
     * @constructor
     * @param {Symbol} singletonToken Symbol to ensure that only one instance of `AdapterFactory` ever exists.
     * @param {Object} bleDrivers Object mapping version to pc-ble-driver AddOn.
     * @param {Object} options Options for customizing the behavior of the adapter factory.
     */
    constructor(singletonToken, bleDrivers, options) {
        if (_singleton !== singletonToken) {
            throw new Error('Cannot instantiate AdapterFactory directly.');
        }

        super();
        this._bleDrivers = bleDrivers;
        this._adapters = {};

        if (options.enablePolling) {
            this.updateInterval = setInterval(this._updateAdapterList.bind(this), UPDATE_INTERVAL_MS);
        }
    }

    /**
     * Get the singleton `AdapterFactory` instance.
     *
     * The mapping of SoftDevice API version to pc-ble-driver AddOn can be overridden
     * by providing a custom `bleDrivers` argument. By default the AdapterFactory will
     * poll for added/removed adapters every 2 seconds and emit 'added' and 'removed'
     * events. This can be disabled by passing `enablePolling: false` as part of the
     * options object.
     *
     * @param {Object} [bleDrivers] Optional object mapping version to pc-ble-driver AddOn.
     * @param {Object} [options] Optional object for customizing the behavior of the adapter factory.
     * @returns {AdapterFactory} The singleton `AdapterFactory` instance.
     */
    static getInstance(bleDrivers, options) {
        const driversToUse = bleDrivers || _bleDrivers;
        const optionsToUse = options || {};
        if (optionsToUse.enablePolling === undefined) {
            optionsToUse.enablePolling = true;
        }

        if (!this[_singleton]) {
            this[_singleton] = new AdapterFactory(_singleton, driversToUse, optionsToUse);
        }

        return this[_singleton];
    }

    // TODO: Better idea for adapter's instanceId?
    _getInstanceId(adapter) {
        let instanceId;

        if (adapter.serialNumber) {
            instanceId = adapter.serialNumber;
        } else if (adapter.comName) {
            instanceId = adapter.comName;
        } else {
            this.emit('error', 'Failed to get adapter\'s instanceId.');
        }

        return instanceId;
    }

    _parseAndCreateAdapter(adapter) {
        // TODO: How about moving id generation and equality check within adapter class?
        const instanceId = this._getInstanceId(adapter);

        let addOnAdapter;
        let selectedDriver;

        // TODO: get adapters from one driver
        // TODO: Figure out what device it is (nRF51 or nRF52). Perhaps serial number can be used?

        const seggerSerialNumber = /^.*68([0-3]{1})[0-9]{6}$/;

        if (seggerSerialNumber.test(instanceId)) {
            const developmentKit = parseInt(seggerSerialNumber.exec(instanceId)[1], 10);
            let sdVersion;

            switch (developmentKit) {
                case 0:
                case 1:
                    sdVersion = 'v2';
                    break;
                case 2:
                    sdVersion = 'v3';
                    break;
                case 3:
                    sdVersion = 'v3';
                    break;
                default:
                    throw new Error(`Unsupported nRF5 development kit. [${instanceId}]`);
            }

            selectedDriver = this._bleDrivers[sdVersion];
            addOnAdapter = new selectedDriver.Adapter();
        } else {
            throw new Error(`Not able to determine version of pc-ble-driver to use. [${instanceId}]`);
        }

        if (addOnAdapter === undefined) {
            throw new Error(`Missing argument adapter. [${instanceId}]`);
        }

        let notSupportedMessage;

        if ((os.platform() === 'darwin') && (adapter.manufacturer === 'MBED')) {
            notSupportedMessage = 'This adapter with mbed CMSIS firmware is currently not supported on OS X. Please visit www.nordicsemi.com/nRFConnectOSXfix for further instructions.';
        } else if ((os.platform() === 'darwin') && ((adapter.manufacturer === 'SEGGER'))) {
            notSupportedMessage = 'Note: Adapters with Segger JLink debug probe requires MSD to be disabled to function properly on OSX. Please visit www.nordicsemi.com/nRFConnectOSXfix for further instructions.';
        }

        return new Adapter(selectedDriver, addOnAdapter, instanceId, adapter.comName, adapter.serialNumber, notSupportedMessage);
    }

    _setUpListenersForAdapterOpenAndClose(adapter) {
        adapter.on('opened', _adapter => {
            this.emit('adapterOpened', _adapter);
        });
        adapter.on('closed', _adapter => {
            this.emit('adapterClosed', _adapter);
        });
    }

    // TODO: create a separate npm module that gets connected adapters and information about them
    _updateAdapterList(callback) {
        // for getting the adapters we just use pc-ble-driver AddOn v2
        this._bleDrivers.v2.getAdapters((err, adapters) => {
            const isCallback = callback && (typeof callback === 'function');

            if (err) {
                this.emit('error', err);
                if (isCallback) {
                    callback(err);
                }

                return;
            }

            const removedAdapters = Object.assign({}, this._adapters);
            for (const adapter of adapters) {
                const adapterInstanceId = this._getInstanceId(adapter);

                if (this._adapters[adapterInstanceId]) {
                    delete removedAdapters[adapterInstanceId];
                }

                try {
                    const newAdapter = this._parseAndCreateAdapter(adapter);

                    if (this._adapters[adapterInstanceId] === undefined) {
                        this._adapters[adapterInstanceId] = newAdapter;
                        this._setUpListenersForAdapterOpenAndClose(newAdapter);
                        this.emit('added', newAdapter);
                    }
                } catch (error) {
                    this.emit('logMessage', logLevel.DEBUG, `Unable to create adapter: ${error.message}`);
                }
            }

            Object.keys(removedAdapters).forEach(adapterId => {
                const removedAdapter = this._adapters[adapterId];
                removedAdapter.removeAllListeners('opened');
                delete this._adapters[adapterId];
                this.emit('removed', removedAdapter);
            });

            if (isCallback) {
                callback(undefined, this._adapters);
            }
        });
    }

    /**
     * Get connected adapters.
     *
     * @param {null|function} callback Optional callback signature: (err, adapters) => {}.
     * @returns {void}
     */
    getAdapters(callback) {
        this._updateAdapterList((err, adapters) => {
            const isCallback = callback && (typeof callback === 'function');

            if (err) {
                this.emit('error', err);
                if (isCallback) {
                    callback(err);
                }

                return;
            }

            if (isCallback) {
                callback(undefined, adapters);
            }
        });
    }

    /**
     * Create Adapter with custom serialport
     *
     * @param sdVersion {string} Softdevice API version: 'v2' or 'v3'.
     * @param comName {string} Serialport name (eg. 'COM7' on windows).
     * @param instanceId {string} The unique Id that identifies this Adapter instance.
     * @returns {Adapter} Created adapter.
     */
    createAdapter(sdVersion, comName, instanceId) {
        if (sdVersion !== 'v2' && sdVersion !== 'v3') {
            throw new Error('Unsupported soft-device version!');
        }
        if (typeof comName === 'undefined') {
            throw new Error('Missing parameter: comName!');
        }
        if (typeof instanceId === 'undefined') {
            throw new Error('Missing parameter: instanceId!');
        }

        const selectedDriver = this._bleDrivers[sdVersion];
        const addOnAdapter = new selectedDriver.Adapter();

        return new Adapter(selectedDriver, addOnAdapter, instanceId, comName);
    }
}

module.exports = AdapterFactory;