Memristor-Robotics/mep-master

View on GitHub
src/drivers/DriverManager.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict';

/** @namespace drivers */
const DriverChecker = require('./DriverChecker');

const TAG = 'DriverManager';

/**
 * <p>Class manipulate drivers</p>
 *
 * DriverManager provides mechanisms to:
 * <ul>
 *  <li>initialize and configure drivers based on config files,</li>
 *  <li>return instance of driver by driver unique identifier,</li>
 *  <li>filter drivers by groups,</li>
 *  <li>check if driver is available,</li>
 *  <li>resolve dependencies and</li>
 *  <li>put driver out of order or recover driver if it is possible.</li>
 * </ul>
 *
 * Each driver can be part of one or more of following groups: control, terrain & position.
 *
 * @memberOf drivers
 * @author Darko Lukic <lukicdarkoo@gmail.com>
 */
class DriverManager {
    constructor() {
        this.drivers = {};
        this.driversOutOfOrder = {};
        this.config = Mep.Config.get('Drivers');
    }

    /**
     * Initialize all drivers
     */
    async init() {
        // Iterate through all drivers in config and initialize them
        for (let name of Object.keys(this.config)) {
            try {
                await this._initDriver(name);
            } catch (e) {
                Mep.Log.error(e);
            }
        }
    }

    /**
     * Check if driver is out of order
     * @param {String} name Unique name of a driver
     * @returns {Boolean}
     */
    isDriverOutOfOrder(name) {
        return (name in this.driversOutOfOrder);
    }

    /**
     * Get driver instance by driver name
     *
     * @param {String} name Driver name, eg. "MotionDriver", or "ModbusDriver".
     * @returns {Object} Required driver
     */
    getDriver(name) {
        if (this.isDriverAvailable(name) === false) {
            throw new Error('There is no driver with name ' + name);
        }

        return this.drivers[name];
    }

    /**
     * Returns true if driver is available
     * @param {String} name Driver name
     * @returns {Boolean} Is driver available
     */
    isDriverAvailable(name) {
        return (name in this.drivers);
    }

    /**
     * <p>Get all drivers that can provide specific type of data.</p>
     *
     * <p>Every driver that can provide a certain type of data has implemented
     * universal mechanism for getting that data from the driver. That is extremely
     * useful for services and in that case services implement only logic for
     * data processing, not mechanisms for data collection from different drivers.
     * Services are in this case also hardware independent.</p>
     *
     * @param {String} type Data type which driver can provide. Can be: position & terrain.
     * @returns {Object} List of filtered drivers
     */
    getDriversByGroup(type) {
        let filteredDrivers = {};

        for (let driverKey in this.drivers) {
            if (this.drivers.hasOwnProperty(driverKey) == false) {
                continue;
            }

            // Check if driver has defined list of data misc which can provide
            if (typeof this.drivers[driverKey].getGroups !== 'function') {
                continue;
            }

            // Check if driver can provide data
            if (this.drivers[driverKey].getGroups().indexOf(type) >= 0) {
                filteredDrivers[driverKey] = this.drivers[driverKey];
            }
        }

        return filteredDrivers;
    }

    /**
     * <p>Get all drivers that can provide specific type of data (or execute specific command) and call method.</p>
     *
     * <p>Eg. imagine you have laptop and monitor. If a monitor is not available then your laptop is it totally ok with it,
     * image will be sent only to laptop's monitor. However, if there is a monitor connected to the laptop then laptop
     * will be aware of monitor and it will send an image to monitor too. Your laptop don't really recognize difference
     * between displays, and it communicates between them in very similar way. To sum up, the same is for this method,
     * method will be called to all drivers that are the part of the group.</p>
     *
     * @param {String} type Data type which driver can provide. Can be: position & terrain.
     * @param {String} method Method to be called.
     * @param {Array} params Params to be passed to method.
     *
     * @see DriverManager.getDriversByGroup
     */
    callMethodByGroup(type, method, params) {
        let drivers = this.getDriversByGroup(type);
        for (let key in drivers) {
            drivers[key][method](...params);
        }
    }

    /**
     * Put driver out of order
     * @param {String} name Unique name of a driver
     * @param {String} message Describe more why the fault happened
     */
    putDriverOutOfOrder(name, message) {
        // Check if it is already out of order
        if (this.isDriverOutOfOrder(name) === true) {
            return;
        }

        // Move to outOfOther pool
        if (this.isDriverAvailable(name) === true) {
            delete this.drivers[name];
        }
        this.driversOutOfOrder[name] = true;

        // Notify user
        Mep.Log.error(TAG, name, 'is out of the order', message);
    }

    async _initDriver(driverIdentifier) {
        let moduleConfig = this.config[driverIdentifier];

        if (moduleConfig === undefined) {
            throw Error('Driver ' + driverIdentifier + ' is missing in configuration. ' +
                'Some driver probably depends on ' + driverIdentifier + ' driver');
        }

        if (this.isDriverAvailable(driverIdentifier) || this.isDriverOutOfOrder(driverIdentifier)) {
            return;
        }

        // Load driver
        let load = moduleConfig['@load'];
        let classPath = moduleConfig['@class'];

        // Do not initialize if `load field == false`
        if (load === true) {
            let ModuleClass;
            try {
                ModuleClass = Mep.require(classPath);
            } catch (e) {
                console.log(e);
            }

            // Resolve dependencies
            if (moduleConfig['@dependencies'] !== undefined) {
                try {
                    await this._resolveDependencies(moduleConfig['@dependencies']);
                    await this._loadDriverBasic(ModuleClass, moduleConfig, driverIdentifier, classPath);
                } catch (e) {
                    throw Error('Cannot load driver `' + driverIdentifier + '` because dependencies are out of order');
                }
            } else {
                await this._loadDriverBasic(ModuleClass, moduleConfig, driverIdentifier, classPath);
            }
        }
    }

    async _loadDriverBasic(ModuleClass, moduleConfig, driverIdentifier, classPath) {
        if (typeof ModuleClass === 'function') {
            let driverInstance;
            try {
                driverInstance = new ModuleClass(driverIdentifier, moduleConfig);
                this.drivers[driverIdentifier] = driverInstance;

                if (typeof driverInstance.init === 'function') {
                    await driverInstance.init();
                    Mep.Log.debug(TAG, 'Driver `' + driverIdentifier + '` initialized');
                } else {
                    Mep.Log.debug(TAG, 'Driver `' + driverIdentifier + '` created');
                }

                // Test if all methods are OK
                DriverChecker.check(driverInstance);
            } catch (error) {
                console.log(TAG, error);
                this.putDriverOutOfOrder(driverIdentifier, error);
            }
        } else {
            Mep.Log.error(TAG, 'There is no driver', classPath);
        }
    }

    async _resolveDependencies(dependencies) {
        for (let key in dependencies) {
            if (this.isDriverAvailable(dependencies[key]) === true) {
                continue;
            }

            if (this.isDriverOutOfOrder(dependencies[key]) === true) {
                this.putDriverOutOfOrder(dependencies[key],
                    'Cannot resolve dependency: ' + dependencies[key]);
                throw Error('Dependency `' + dependencies[key] + '` driver cannot be loaded');
            }

            await this._initDriver(dependencies[key]);
        }
    }
}

module.exports = DriverManager;