Source: tinkamo.js

import { TinkaCore } from './tinkacore.js';

/**
* **Class for all Tinkamo**
* Essentially a bucket containing all Tinkamo and functions handling when
* TinkaCores are connected and disconnected to the browser.
*
* There should only be one instance of Tinkamo per application.
*/
class Tinkamo {

    /**
     * Creates an instance of the Tinkamo class.
     */
    constructor() {
        // Static Variables
        Tinkamo.eventTypes = ['*', 'connect', 'disconnect'];

        // Instance Variables
        this.tinkacores = {};
        this.serviceName = 0xfffa;

        this.events = [];

        // Tracking of TinkaCores
        TinkaCore.core_ids = TinkaCore.core_ids || {
            connected: new Set([]),
            disconnected: new Set([])
        }
    }

    /**
     * Primary method for connecting a new TinkaCore to the browser using
     * the Chrome bluetooth api.
     *
     * Due to browser security, this function cannot be called directly,
     * and instead must be a called from user action like pressing a button.
     *
     * @example
     *
     * let tinkamo = new Tinkamo();
     * let connectionButton = document.getElementById('connectionButton');
     * connectionButton.onclick = function() { tinkamo.connect(); }
     */
    connect(){
        let self = this;
        console.log('Requesting Bluetooth Device...');
        let newDeviceID; // Hold on to the device ID for later
        navigator.bluetooth.requestDevice({
    	filters : [{
    	    name: 'Tinka',
    	}],
    	optionalServices: [self.serviceName]
        })
    	.then(device => {
    	    console.log('> Found ' + device.name);
    	    console.log('> Id: ' + device.id);
    	    console.log('> Connected: ' + device.gatt.connected);

            // In order to maintain Tinkamo as the parent when this is called
            // within the event listener, we must explicitly bind it
            let bound_disconnect = (function(event) {
                self._on_disconnected(event)
            }).bind(self);
    	    device.addEventListener('gattserverdisconnected', bound_disconnect);

            newDeviceID = device.id;
            return device.gatt.connect()
    	})
    	.then(server => {
            console.log(server);
    	    return server.getPrimaryService(self.serviceName);
    	})
    	.then(service => {
                console.log('Tinka services...');
                console.log(service);
                return service.getCharacteristics();
    	})
    	.then(characteristics => {
    	    console.log('Tinka characteristics...');
            return self._add_tinkacore(newDeviceID, characteristics);
    	})
        .then(tinkacore => {
            this._callEventListeners({type: 'connect', tinkacore: tinkacore, tinkamo: this});
            console.log('Optional user callback')
        })
    	.catch(error => {
    	    console.log('Error', error);
    	});
    }

    // ----------- Events -----------

    /**
     * Function allowing the user to define custom event listeners to be called
     * when a Tinkamo instance triggers an event. Events are called in the
     * order with which they were added.
     *
     * eventType must be one of the following:
     * - '*' - connect or disconnect
     * - 'connect'
     * - 'disconnect'
     *
     * @param {string} eventType
     * @param {function} func
     * @param {...*} args
     * @returns {boolean}
     */
    addEventListener(eventType, func, ...args) {
        if (typeof func !== "function") {
            throw "second argument must be a valid function";
            return false;
        }
        if (!Tinkamo.eventTypes.includes(eventType)) {
            throw "event type must be valid"; // list event types
            return false;
        }

        let newEvent = {'eventType': eventType, 'func': func, 'args': args};
        this.events.push(newEvent);
        return true;
    }

    /**
     * Removes a callback function from the events list preventing further calls.
     *
     * @param {string} eventType
     * @param {function} func
     * @returns {boolean}
     */
    removeEventListener(eventType, func) {
        this.events = this.events.filter(ev => (ev.eventType != eventType || ev.func != func));
        return true;
    }

    /**
     * Iterates through the events list and upon ANY TinkaCore event, calls
     * the relevent functions.
     *
     * @param {string} event
     * @returns {boolean}
     * @private
     */
    _callEventListeners(event) {
        for (let evObj of this.events) {
            if (evObj.eventType == '*' || evObj.eventType == event.type)
                evObj.func(event, ...evObj.args);
        }
    }

    // ----------- Getters -----------

    /**
     * Get a list of TinkaCores. By default it returns tinkacores that have
     * been disconnected as well.
     *
     * TinkaCores are listed in the order with which they were originally
     * connected.
     *
     * @param {boolean} [include_disconnected=true]
     * @returns {Tinkacore[]}
     */
    getTinkamoList(include_disconnected=true) {
        let tinkaList = Object.values(this.tinkacores);
        tinkaList.sort((a, b) => a.number - b.number);
        let fList = tinkaList.filter(t => (include_disconnected || t.connected));
        return fList;
    }

    /**
     * Gets a tinkacore based on its built-in ID
     * @returns {Tinkacore}
     */
    getByID(id) {
        return this.tinkacores[id];
    }

    /**
     * Returns a list of tinkacores with included name.
     * Empty list if the name is not found
     * @returns {Tinkacore}
     */
    getByName(name) {
        // We need to decide if names are guaranteed to be unique
        let tinkaList = Object.values(this.tinkacores);
        let tinkaWithName = tinkaList.filter(t => t.name == name);
        return tinkaWithName;
    }

    /**
     * Returns a list of tinkacores with currently attached top.
     * Empty list if none have that top.
     * @returns {Tinkacore}
     */
    getBySensor(sensorName) {
        // May want to change the word sensor to top or tinkaTop
        let tinkaList = Object.values(this.tinkacores);
        let tinkaWithSensor = tinkaList.filter(t => t.sensor.name == sensorName);
        return tinkaWithSensor;
    }

    // ----------- Setters -----------

    // Get and set name?

    /**
     * Creates a new TinkaCore instance and uses TinkaCore functions
     * to ensure it is tracked correctly.
     *
     * @returns {Tinkacore}
     * @private
     */
    _add_tinkacore(id, characteristics) {
        if (TinkaCore.core_ids.disconnected.has(id)) {
            this.tinkacores[id].reconnect(characteristics);
        }
        else {
            let newTinkaCore = new TinkaCore(id, characteristics);
            newTinkaCore.connect();
            this.tinkacores[id] = newTinkaCore;
        }

        return this.tinkacores[id];
    }

    // Should only be used as a callback function
    /**
     * Callback function triggered when a TinkaCore is turned off or becomes
     * otherwise disconnectd.
     *
     * @private
     */
    _on_disconnected(event) {
        let device = event.target;
        let disconnected_id = device.id;

        this.tinkacores[disconnected_id].disconnect();

        this._callEventListeners({type: 'disconnect',
                                  tinkacore: this.tinkacores[disconnected_id],
                                  tinkamo: this});
    }
}

export default Tinkamo;