Home Manual Reference Source Test Repository

src/drivers/motion/MotionDriver.js

'use strict';
/** @namespace drivers.motion */

const Point = Mep.require('misc/Point');
const EventEmitter = require('events');
const Buffer = require('buffer').Buffer;
const TaskError = Mep.require('strategy/TaskError');


const TAG = 'MotionDriver';

/**
 * Driver enables communication with Memristor's motion driver.
 * @memberof drivers.motion
 * @author Darko Lukic <[email protected]>
 * @fires drivers.motion.MotionDriver#positionChanged
 * @fires drivers.motion.MotionDriver#orientationChanged
 * @fires drivers.motion.MotionDriver#stateChanged
 */
class MotionDriver extends EventEmitter  {
    static get STATE_IDLE() { return 'I'.charCodeAt(0); }
    static get STATE_STUCK() { return 'S'.charCodeAt(0); }
    static get STATE_MOVING() { return 'M'.charCodeAt(0); }
    static get STATE_ROTATING() { return 'R'.charCodeAt(0); }
    static get STATE_ERROR() { return 'E'.charCodeAt(0); }
    static get STATE_UNDEFINED() { return 'U'.charCodeAt(0); }
    static get STATE_BREAK() { return 'B'.charCodeAt(0); }

    static get DIRECTION_FORWARD() { return 1; }
    static get DIRECTION_UNDEFINED() { return 0; }
    static get DIRECTION_BACKWARD() { return -1; }

    static get CONFIG_DISTANCE_REGULATOR() { return 0; }
    static get CONFIG_ROTATION_REGULATOR() { return 1; }
    static get CONFIG_STUCK() { return 2; }
    static get CONFIG_DEBUG() { return 3; }
    static get CONFIG_STATUS_CHANGE_REPORT() { return 4; }

    static get CONFIG_STUCK_DISTANCE_JUMP() { return 5; }
    static get CONFIG_STUCK_ROTATION_JUMP() { return 6; }
    static get CONFIG_STUCK_DISTANCE_MAX_FAIL_COUNT() { return 7; }
    static get CONFIG_STUCK_ROTATION_MAX_FAIL_COUNT() { return 8; }

    static get CONFIG_WHEEL_DISTANCE() { return 9; }
    static get CONFIG_WHEEL_R1() { return 10; }
    static get CONFIG_WHEEL_R2() { return 11; }
    static get CONFIG_PID_D_P() { return 12; }
    static get CONFIG_PID_D_D() { return 13; }
    static get CONFIG_PID_R_P() { return 14; }
    static get CONFIG_PID_R_D() { return 15; }
    static get CONFIG_SPEED() { return 16; }
    static get CONFIG_RSPEED() { return 17; }
    static get CONFIG_ACCEL() { return 18; }
    static get CONFIG_ALPHA() { return 19; }


    /**
     * @param name {String} - Unique driver name
     * @param config {Object} - Configuration presented as an associative array
     */
    constructor(name, config) {
        super();

        this.name = name;
        this.config = Object.assign({
            startX: -1300,
            startY: 0,
            startOrientation: 0,
            startSpeed: 100,
            refreshDataPeriod: 100,
            connectionTimeout: 4000,
            ackTimeout: 150
        }, config);

        this.positon = new Point(config.startX, config.startY);
        this.state = MotionDriver.STATE_UNDEFINED;
        this.orientation = config.startOrientation;
        this._activeSpeed = config.startSpeed;
        this._breaking = false;
        this._direction = MotionDriver.DIRECTION_UNDEFINED;

        this.communicator = Mep.DriverManager.getDriver(config['@dependencies'].communicator);
        this.communicator.on('data', this._onDataReceived.bind(this));

        this._waitACKQueue = {};
    }

    _waitACK(type) {
        let motionDriver = this;

        setTimeout(() => {
            if (this._waitACKQueue[type] !== undefined) {
                Mep.Log.error(TAG, 'Error sending a command');
                motionDriver._sendCommand(this._waitACKQueue[type]);
            }
        }, this.config.ackTimeout);
    }

    _sendCommand(buff) {
        let type = buff.readUInt8(0);

        this._waitACKQueue[type] = buff;
        this._waitACK(type);

        this.communicator.send(buff);
    }

    getDirection() {
        return this._direction;
    }

    /**
     * Check driver by checking checking communication with motion driver board
     */
    init() {
        let motionDriver = this;

        this.reset();
        this.setPositionAndOrientation(
            this.config.startX,
            this.config.startY,
            this.config.startOrientation
        );
        this.setRefreshInterval(this.config.refreshDataPeriod);
        this.requestRefreshData();

        return new Promise((resolve) => {
            let driverChecker = setInterval(() => {
                if (motionDriver.getState() !== MotionDriver.STATE_UNDEFINED) {
                    clearInterval(driverChecker);
                    Mep.Log.info(TAG, 'Communication is validated');
                    resolve();
                }
            }, 100);
            setTimeout(() => {
                if (motionDriver.getState() === MotionDriver.STATE_UNDEFINED) {
                    throw Error(TAG, 'No response from motion driver');
                }
            }, this.config.connectionTimeout);
        })
    }

    /**
     * Finish `moveToCurvilinear` command and prepare robot for another one
     */
    finishCommand() {
        this._sendCommand(Buffer.from(['i'.charCodeAt(0)]));
        Mep.Log.debug(TAG, 'Finish command sent');
    }

    /**
     * Reset all settings in motion driver
     */
    reset() {
        this._sendCommand(Buffer.from(['R'.charCodeAt(0)]));
    }

    /**
     * Request state, position and orientation from motion driver
     */
    requestRefreshData() {
        this._sendCommand(Buffer.from(['P'.charCodeAt(0)]));
    }

    /**
     * Reset position and orientation
     * @param x {Number} - New X coordinate relative to start position of the robot
     * @param y {Number} - New Y coordinate relative to start position of the robot
     * @param orientation {Number} - New robot's orientation
     */
    setPositionAndOrientation(x, y, orientation) {
        this._sendCommand(Buffer.from([
            'I'.charCodeAt(0),
            x >> 8,
            x & 0xFF,
            y >> 8,
            y & 0xFF,
            orientation >> 8,
            orientation & 0xFF
        ]));
    }

    setConfig(key, value, exp = 3) {
        let fixedValue = (value * Math.pow(10, exp)) | 0;

        let buffer = Buffer.from([
            'c'.charCodeAt(0),
            key & 0xFF,
            (fixedValue >> 20) & 0x0F,
            (fixedValue >> 12) & 0xFF,
            (fixedValue >> 4) & 0xFF,
            ((fixedValue & 0x0F) << 4) | (exp & 0x0F)
        ]);

        console.log(buffer);

        this._sendCommand(buffer);
    }

    getConfig(key) {
        let buffer = Buffer.from([
            'C'.charCodeAt(0),
            key & 0xFF
            ]);
        this._sendCommand(buffer);
    }

    _onCReceived(buffer) {
        let value = buffer.readUInt8(0) << 20;
        value |= buffer.readUInt8(1) << 12;
        value |= buffer.readUInt8(2) << 4;
        value |= buffer.readUInt8(3) >> 4;

        let exp = buffer.readUInt8(3) & 0x0F;

        let result = value / Math.pow(10, exp);

        Mep.Log.info(TAG, 'Value =', result)
    }

    /**
     * Rotate robot to given angle
     * @param angle {Number} - Angle
     */
    rotateTo(angle) {
        this._direction = MotionDriver.DIRECTION_UNDEFINED;
        this._sendCommand(Buffer.from([
            //'A'.charCodeAt(0),
            'T'.charCodeAt(0),
            angle >> 8,
            angle & 0xFF
        ]));
        this._breaking = false;

        return this._promiseToStateChanged();
    }

    _promiseToStateChanged() {
        let motionDriver = this;

        return new Promise((resolve, reject) => {
            let stateListener = (name, state) => {
                switch (state) {
                    case MotionDriver.STATE_IDLE:
                        resolve();
                        motionDriver.removeListener('stateChanged', stateListener);
                        break;
                    case MotionDriver.STATE_STUCK:
                        reject(new TaskError(TAG, 'stuck', 'Robot is stacked'));
                        motionDriver.removeListener('stateChanged', stateListener);
                        break;
                    case MotionDriver.STATE_ERROR:
                        reject(new TaskError(TAG, 'error', 'Unknown moving error'));
                        motionDriver.removeListener('stateChanged', stateListener);
                        break;
                    case MotionDriver.STATE_BREAK:
                        reject(new TaskError(TAG, 'break', 'Command is broken by another one'));
                        motionDriver.removeListener('stateChanged', stateListener);
                        break;
                }
            };

            this.on('stateChanged', stateListener);
        });
    }

    /**
     * Move robot forward or backward depending on sign
     * @param millimeters
     * @deprecated
     */
    goForward(millimeters) {
        this._direction = (millimeters > 0) ?
            MotionDriver.DIRECTION_FORWARD : MotionDriver.DIRECTION_BACKWARD;
        this._sendCommand(Buffer.from([
            'D'.charCodeAt(0),
            millimeters >> 8,
            millimeters & 0xFF,
            0
        ]));
        return this._promiseToStateChanged();
    }

    /**
     * Stop the robot.
     */
    stop() {
        this._direction = MotionDriver.DIRECTION_UNDEFINED;
        this._breaking = true;
        this.state = MotionDriver.STATE_BREAK;
        this.emit('stateChanged', this.name, MotionDriver.STATE_BREAK);
        this._sendCommand(Buffer.from(['S'.charCodeAt(0)]));

        return new Promise((resolve, reject) => {
                setTimeout(resolve, 700);
        });
    }

    /**
     * Stop robot by turning off motors.
     */
    softStop() {
        this._direction = MotionDriver.DIRECTION_UNDEFINED;
        this._breaking = true;
        this.emit('stateChanged', this.name, MotionDriver.STATE_BREAK);
        this._sendCommand(Buffer.from(['s'.charCodeAt(0)]));

        return new Promise((resolve, reject) => {
            resolve();
        });
    }

    /**
     * Set required refresh interval of robot status. Note that it is required
     * refresh interval and robot can choose to send or not depending on it's state.
     * @param interval {Number} - Period in milliseconds
     */
    setRefreshInterval(interval) {
        this._sendCommand(Buffer.from([
            'p'.charCodeAt(0),
            interval >> 8,
            interval & 0xff,
        ]));
    }


    /**
     * Set default speed of the robot
     * @param speed {Number} - Speed (0 - 255)
     */
    setSpeed(speed) {
        this._activeSpeed = speed;
        this._sendCommand(Buffer.from([
            'V'.charCodeAt(0),
            speed
        ]));
    }

    /**
     * Move robot to absolute position
     * @param position {misc.Point} - Required position of the robot
     * @param direction {Number} - Direction, can be MotionDriver.DIRECTION_FORWARD or
     * MotionDriver.DIRECTION_BACKWARD
     */
    moveToPosition(position, direction) {
        this._direction = direction;
        this._sendCommand(Buffer.from([
            'G'.charCodeAt(0),
            position.getX() >> 8,
            position.getX() & 0xff,
            position.getY() >> 8,
            position.getY() & 0xff,
            0,
            direction
        ]));
        this._breaking = false;

        return this._promiseToStateChanged();
    }

    /**
     * Move robot to absolute position but robot make curves to speed up motion. This
     * command requires `finishCommand()` before next motion command.
     * @param {misc.Point} position Required position of the robot
     * @param {Number} direction Direction, can be MotionDriver.DIRECTION_FORWARD or
     * MotionDriver.DIRECTION_BACKWARD
     * @param {Number} tolerance Radius
     */
    moveToCurvilinear(position, direction, tolerance) {
        let motionDriver = this;

        this._direction = direction;
        this._sendCommand(Buffer.from([
            'N'.charCodeAt(0),
            position.getX() >> 8,
            position.getX() & 0xff,
            position.getY() >> 8,
            position.getY() & 0xff,
            direction
        ]));
        this._breaking = false;

        return new Promise((resolve, reject) => {
            let positionListener = (name, currentPosition) => {
                if (currentPosition.getDistance(position) <= tolerance) {
                    motionDriver.finishCommand();
                    motionDriver.removeListener('positionChanged', positionListener);
                    resolve();
                }
            };
            this.on('positionChanged', positionListener);
            this._promiseToStateChanged()
                .then(() => {
                    motionDriver.removeListener('positionChanged', positionListener);
                    resolve();
                })
                .catch((e) => {
                    motionDriver.removeListener('positionChanged', positionListener);
                    reject(e);
                });
        });
    }

    /**
     * Packet type P is received
     * @param buffer {Buffer} - Payload
     * @private
     */
    _onPReceived(buffer) {
        // Ignore garbage
        let state = buffer.readInt8(0);
        let position = new Point(
            (buffer.readInt8(1) << 8) | (buffer.readInt8(2) & 0xFF),
            (buffer.readInt8(3) << 8) | (buffer.readInt8(4) & 0xFF)
        );
        let orientation = (buffer.readInt8(5) << 8) | (buffer.readInt8(6) & 0xFF);
        let speed = (buffer.readInt8(7) << 8) | (buffer.readInt8(8) & 0xFF);

        Mep.Telemetry.send(TAG, 'Speed', {speed: speed});

        if (this.positon.equals(position) === false) {
            this.positon = position;

            /**
             * Position changed event.
             * @event drivers.motion.MotionDriver#positionChanged
             * @property {String} driverName - Unique name of a driver
             * @property {misc.Point} point - Position of the robot
             */
            this.emit('positionChanged',
                this.name,
                this.positon,
                this.config.precision
            );
        }

        // If state is changed
        if (state !== this.state) {
            if (this._breaking === false) {
                this.state = state;

                /**
                 * State change event.
                 * @event drivers.motion.MotionDriver#stateChanged
                 * @property {Number} state - New state
                 */
                this.emit('stateChanged', this.name, this.state);
            }
        }

        if (orientation !== this.orientation) {
            this.orientation = orientation;

            /**
             * Orientation change event.
             * @event drivers.motion.MotionDriver#orientationChanged
             * @property {String} driverName - Unique name of a driver
             * @property {Number} orientation - New orientation
             */
            this.emit('orientationChanged',
                this.name,
                orientation,
                this.config.precision
            );
        }
    }

    _onAReceived(buffer) {
        delete this._waitACKQueue[buffer.readUInt8()];
    }

    /**
     * Callback will be called when new packet is arrived and it will dispatch event to new
     * callback depending on packet type
     * @param buffer {Buffer} - Payload
     * @param type {String} - Packet type
     * @private
     */
    _onDataReceived(buffer, type) {
        if (type == 'P'.charCodeAt(0) && buffer.length === 9) {
            this._onPReceived(buffer);
        }
        else if (type == 'A'.charCodeAt(0) && buffer.length === 1) {
            this._onAReceived(buffer);
        }
        else if (type == 'C'.charCodeAt(0) && buffer.length === 4) {
            this._onCReceived(buffer);
        }
    }

    /**
     * Get position of the robot
     * @return {Point} - Position of the robot
     */
    getPosition() {
        return this.positon;
    }

    getState() {
        return this.state;
    }

    getGroups() {
        return ['position'];
    }

    getOrientation() {
        return this.orientation;
    }

    getActiveSpeed() {
        return this._activeSpeed;
    }
}

module.exports = MotionDriver;