Home Manual Reference Source Test Repository

src/drivers/dynamixel/DynamixelDriver.js

'use strict';

/** @namespace drivers.dynamixel */
const TaskError = Mep.require('strategy/TaskError');



const TAG = 'DynamixelDriver';

/**
 * Communicates with dynamixel servos (AX12 & RX24).
 * NOTE: This class doesn't send start bytes & checksum, please make custom `communicator`
 * (`@dependecies.communicator`) if you want these features.
 * @memberOf drivers.dynamixel
 * @author Darko Lukic <[email protected]>
 */
class DynamixelDriver {
    static get AX_MODEL_NUMBER_L() { return 0; }
    static get AX_MODEL_NUMBER_H() { return 1; }
    static get AX_VERSION() { return 2; }
    static get AX_SERVO_ID() { return 3; }
    static get AX_BAUD_RATE() { return 4; }
    static get AX_RETURN_DELAY_TIME() { return 5; }
    static get AX_CW_ANGLE_LIMIT_L() { return 6; }
    static get AX_CW_ANGLE_LIMIT_H() { return 7; }
    static get AX_CCW_ANGLE_LIMIT_L() { return 8; }
    static get AX_CCW_ANGLE_LIMIT_H() { return 9; }
    static get AX_LIMIT_TEMPERATURE() { return 11; }
    static get AX_LOW_LIMIT_VOLTAGE() { return 12; }
    static get AX_HIGH_LIMIT_VOLTAGE() { return 13; }
    static get AX_MAX_TORQUE_L() { return 14; }
    static get AX_MAX_TORQUE_H() { return 15; }
    static get AX_RETURN_LEVEL() { return 16; }
    static get AX_ALARM_LED() { return 17; }
    static get AX_ALARM_SHUTDOWN() { return 18; }
    static get AX_DOWN_CALIBRATION_L() { return 20; }
    static get AX_DOWN_CALIBRATION_H() { return 21; }
    static get AX_UP_CALIBRATION_L() { return 22; }
    static get AX_UP_CALIBRATION_H() { return 23; }
    static get AX_TORQUE_ENABLE() { return 24; }
    static get AX_LED() { return 25; }
    static get AX_CW_COMPLIANCE_MARGIN() { return 26; }
    static get AX_CCW_COMPLIANCE_MARGIN() { return 27; }
    static get AX_CW_COMPLIANCE_SLOPE() { return 28; }
    static get AX_CCW_COMPLIANCE_SLOPE() { return 29; }
    static get AX_GOAL_POSITION_L() { return 30; }
    static get AX_GOAL_POSITION_H() { return 31; }
    static get AX_GOAL_SPEED_L() { return 32; }
    static get AX_GOAL_SPEED_H() { return 33; }
    static get AX_TORQUE_LIMIT_L() { return 34; }
    static get AX_TORQUE_LIMIT_H() { return 35; }
    static get AX_PRESENT_POSITION_L() { return 36; }
    static get AX_PRESENT_POSITION_H() { return 37; }
    static get AX_PRESENT_SPEED_L() { return 38; }
    static get AX_PRESENT_SPEED_H() { return 39; }
    static get AX_PRESENT_LOAD_L() { return 40; }
    static get AX_PRESENT_LOAD_H() { return 41; }
    static get AX_PRESENT_VOLTAGE() { return 42; }
    static get AX_PRESENT_TEMPERATURE() { return 43; }
    static get AX_REGISTERED_INSTRUCTION() { return 44; }
    static get AX_MOVING() { return 46; }
    static get AX_LOCK() { return 47; }
    static get AX_PUNCH_L() { return 48; }
    static get AX_PUNCH_H() { return 49; }

    static get AX_POLL_POSITION() { return 50; }        // Custom, Memristor's implementation

    constructor(name, config) {
        this.config = Object.assign({
            id: 0xFE,
            maxPosition: 1023,
            minPosition: 0
        }, config);
        this.name = name;

        if (this.config.cid === undefined) {
            throw Error(TAG, this.name, 'You must provide a communication ID');
        }

        this._onDataReceived = this._onDataReceived.bind(this);
        this.uniqueDataReceivedCallback = null;

        this.communicator = null;
        if (this.config._communicator !== undefined) {
            // For testing purposes only (experiments)
            this.communicator = this.config._communicator;
        } else {
            this.communicator = Mep.DriverManager.getDriver(this.config['@dependencies'].communicator);
        }
        this.communicator.on('data_' + this.config.cid, this._onDataReceived);
    }

    _onDataReceived(data) {
        if (data.readUInt8(0) === (this.config.id | 0)) {

            // Length === 1 means there is an error in communication (UART, Servo <--> AVR)
            if (data.length === 2) {
                switch (data.readInt8(1)) {
                    case 0x03:
                        Mep.Log.error(TAG, this.name, 'RX Timeout error');
                        break;

                    case 0x02:
                        Mep.Log.error(TAG, this.name, 'RX Data corrupted');
                        break;

                    case 0x04:
                        Mep.Log.error(TAG, this.name, 'TX transfer failed');
                        break;

                    case 0x05:
                        Mep.Log.error(TAG, this.name, 'TX transfer timeout');
                        break;

                    default:
                        Mep.Log.error(TAG, this.name, 'Unhandled error', data);
                        break;
                }
                return;
            }

            if (this.uniqueDataReceivedCallback !== null) {
                this.uniqueDataReceivedCallback(data);
            }
        }
    }

    getTemperature() {
        return this._read(DynamixelDriver.AX_PRESENT_TEMPERATURE);
    }

    async getVoltage() {
        return await this._read(DynamixelDriver.AX_PRESENT_VOLTAGE);
    }

    getLoad() {
        return this._read(DynamixelDriver.AX_PRESENT_LOAD_L);
    }

    getTorqueLimit() {
        return this._read(DynamixelDriver.AX_TORQUE_LIMIT_L);
    }

    getFirmwareVersion() {
        return this._read(DynamixelDriver.AX_VERSION);
    }

    async getPosition() {
        return await this._read(DynamixelDriver.AX_PRESENT_POSITION_L, true);
    }

    getSpeed() {
        return this._read(DynamixelDriver.AX_PRESENT_SPEED_L, true);
    }

    /**
     * Set servo to required position and get promise when position is reached
     * @param {Number} position Required position in degrees
     * @param {Object} [config] Configuration options.
     * @param {Number} [config.pollingPeriod] Polling period for servo's present position in ms
     * @param {Number} [config.tolerance] Tolerated error in degrees
     * @param {Number} [config.timeout] Maximal time servo to reach a position in ms
     * @param {Boolean} [config.firmwareImplementation] Should it use firmware (true) or software (false) implementation.
     * Firmware implementation is faster, however in that case we have to have a dedicated hardware (our actuator board supports it).
     */
    go(position, config) {
        let c = Object.assign({
            pollingPeriod: 150,
            tolerance: 35,
            timeout: 3000,
            firmwareImplementation: false
        }, config);

        let ax = this;
        let timeout = false;
        this.setPosition(position);

        return new Promise((resolve, reject) => {
            // Apply time out
            setTimeout(() => {
                timeout = true;
                reject(new TaskError(TAG, 'timeout', 'Dynamixel cannot reach position in time'));
            }, c.timeout);

            if (c.firmwareImplementation === true) {
                // Firmware polling implementation
                throw Error('Firmware implementation is not implemented');
                this._writeWord(
                    DynamixelDriver.AX_POLL_POSITION,
                    ((c.tolerance << 8) | (c.pollingPeriod & 0xFF))
                );

            } else {
                // Software polling implementation
                let checkPosition = () => {
                    setTimeout(() => {
                        ax.getPosition().then((currentPosition) => {
                            if (Math.abs(currentPosition - position) <= c.tolerance) {
                                resolve();
                            } else {
                                if (timeout === false) {
                                    checkPosition();
                                }
                            }
                        }).catch(checkPosition);
                    }, c.pollingPeriod);
                };
                checkPosition();
            }
        });
    }

    async getStatus() {
        let status = {};
        status.temperature = await this.getTemperature();
        status.voltage = await this.getVoltage();
        status.load = await this.getLoad();
        status.firmwareVersion = await this.getFirmwareVersion();
        status.position = await this.getPosition();
        status.speed = await this.getSpeed();
        return status;
    }

    setPunch(current) {
        return this._writeWord(DynamixelDriver.AX_PUNCH_L, current);
    }

    setId(id) {
        return this._writeByte(DynamixelDriver.AX_SERVO_ID, id);
    }

    setBaudrate(baudrate) {
        let baudPairs = {
            1000000: 0x01,
            500000: 0x03,
            400000: 0x04,
            250000: 0x07,
            200000: 0x09,
            115200: 0x10,
            57600: 0x22,
            19200: 0x67,
            9600: 0xCF
        };
        if (baudPairs.hasOwnProperty(baudrate)) {
            return this._writeByte(DynamixelDriver.AX_BAUD_RATE, baudPairs[baudrate]);
        } else {
            return new Promise((resolve, reject) => {
                Mep.Log.error(TAG, this.name, 'Invalid baud rate');
                reject();
            });
        }
    }

    setPosition(position) {
        if (position > this.config.maxPosition || position < this.config.minPosition) {
            throw Error(TAG, this.name, 'Position out of range!');
        }
        this._writeWord(DynamixelDriver.AX_GOAL_POSITION_L, position | 0);
    }

    setSpeed(speed, inverse = false) {
        if (speed > 1023 || speed < 0) {
            Mep.Log.error(TAG, this.name, 'Speed out of range!');
            return;
        }

        if (inverse === true) {
            speed = (1 << 10) | speed;
        }

        this._writeWord(DynamixelDriver.AX_GOAL_SPEED_L, speed | 0);
    }

    setCWAngleLimit(angle) {
        this._writeWord(DynamixelDriver.AX_CW_ANGLE_LIMIT_L, angle | 0);
    }

    setCCWAngleLimit(angle) {
        this._writeWord(DynamixelDriver.AX_CCW_ANGLE_LIMIT_L, angle | 0);
    }

    setLED(on) {
        this._writeByte(DynamixelDriver.AX_LED, on | 0);
    }

    _writeWord(address, word) {
        this.communicator.send(
            this.config.cid,
            Buffer.from([
                this.config.id,     // AX12 ID
                0x05,               // Length
                0x03,               // Write
                address,            // Address (function)
                word & 0xFF,
                (word >> 8) & 0xFF
        ]));
    }

    _writeByte(address, byte) {
        this.communicator.send(
            this.config.cid,
            Buffer.from([
                this.config.id,     // AX12 ID
                0x04,               // Length
                0x03,               // Write
                address,            // Address (function)
                byte & 0xFF         // Param
        ]));
    }

    _read(address, word = false) {
        let ax = this;
        let buffer = Buffer.from([
            this.config.id,     // AX12 ID
            0x04,               // Length
            0x02,               // Read
            address,            // Address (function)
            word ? 0x02 : 0x01
        ]);

        return new Promise((resolve, reject) => {
            if (ax.config.id === 0xFE) {
                reject(new TaskError(TAG, 'broadcast', 'Cannot use broadcast ID for reading'));
                return;
            }



            ax.uniqueDataReceivedCallback = (data) => {
                // Catch error code
                if (data.readUInt8(2) !== 0x00) {
                    Mep.Log.error(TAG, this.name, 'Response error', data.readUInt8(2));
                    reject(data.readUInt8(2));
                    return;
                }

                if (word === true && data.length > 4) {
                    resolve(
                        (data.readUInt8(4) << 8) |
                        (data.readUInt8(3) & 0xFF)
                    );
                } else if (data.length > 3) {
                    resolve(data.readUInt8(3));
                }
                ax.uniqueDataReceivedCallback = null;
            };

            ax.communicator.send(
                ax.config.cid,
                buffer
            );
        });
    }

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

module.exports = DynamixelDriver;