UNPKG

sabertooth-usb

Version:

API for controlling USB-enabled Sabertooth motor drivers running in Packet Serial mode

339 lines 15.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.listSabertoothDevices = exports.SabertoothUSB = exports.MixedModeMotor = exports.Type = exports.Command = exports.SetType = exports.GetType = void 0; const lodash_1 = __importDefault(require("lodash")); let SerialPortClass; try { // In electron window.require should be used. SerialPortClass = window.require('serialport'); } catch (e) { SerialPortClass = require('serialport'); } var GetType; (function (GetType) { GetType[GetType["Value"] = 0] = "Value"; GetType[GetType["Battery"] = 16] = "Battery"; GetType[GetType["Current"] = 32] = "Current"; GetType[GetType["Temperature"] = 64] = "Temperature"; })(GetType = exports.GetType || (exports.GetType = {})); var SetType; (function (SetType) { SetType[SetType["Value"] = 0] = "Value"; SetType[SetType["KeepAlive"] = 16] = "KeepAlive"; SetType[SetType["Shutdown"] = 32] = "Shutdown"; SetType[SetType["Timeout"] = 64] = "Timeout"; })(SetType = exports.SetType || (exports.SetType = {})); var Command; (function (Command) { Command[Command["Set"] = 40] = "Set"; Command[Command["Get"] = 41] = "Get"; })(Command = exports.Command || (exports.Command = {})); var Type; (function (Type) { Type[Type["Motor"] = 77] = "Motor"; Type[Type["Freewheel"] = 81] = "Freewheel"; Type[Type["Signal"] = 83] = "Signal"; Type[Type["Aux"] = 65] = "Aux"; Type[Type["Power"] = 80] = "Power"; Type[Type["CurrentLimit"] = 84] = "CurrentLimit"; Type[Type["Ramping"] = 82] = "Ramping"; })(Type = exports.Type || (exports.Type = {})); var MixedModeMotor; (function (MixedModeMotor) { MixedModeMotor[MixedModeMotor["Drive"] = 68] = "Drive"; MixedModeMotor[MixedModeMotor["Turn"] = 84] = "Turn"; })(MixedModeMotor = exports.MixedModeMotor || (exports.MixedModeMotor = {})); const MASK = 127; const appendChecksum = (buffer, offset) => { buffer.push(lodash_1.default.sum(buffer.slice(offset)) & MASK); }; /** * Controls USB-enabled Sabertooth motor drivers running in Packet Serial mode. * * See https://www.dimensionengineering.com/datasheets/USBSabertoothPacketSerialReference.pdf * * Note: Only Checksum protection is implemented, not CRC. */ class SabertoothUSB { /** * Create an object to control a motor driver. * * A connection to the motor driver will be attempted upon creation but * this is asynchronous and is not available immediately after creation. * If the connection fails reconnection will be attempted automatically. * * @param path the path to the (USB) serial port. eg `/dev/ttyACM0` or `COM1`. * @param options Optional connection options. See `Options` type for details. */ constructor(path, options) { var _a, _b, _c, _d, _e, _f; this.lastError = null; this.motorCurrentExponetialAverage = { 1: 0, 2: 0, }; /** Returns true iff the USB serial connection to the motor driver is open and working. */ this.isConnected = () => this.serial.isOpen; /** Get the last error that occurred in the connection to the motor driver. */ this.getLastError = () => this.lastError; this.path = path; this.timeout = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : 1000; this.address = (_b = options === null || options === void 0 ? void 0 : options.address) !== null && _b !== void 0 ? _b : 128; this.maxGetAttemptCount = (_c = options === null || options === void 0 ? void 0 : options.maxGetAttemptCount) !== null && _c !== void 0 ? _c : 3; this.maxMotorOutputRate = (_d = options === null || options === void 0 ? void 0 : options.maxMotorOutputRate) !== null && _d !== void 0 ? _d : 0.95; this.motorCurrentDenoiseAlpha = (_e = options === null || options === void 0 ? void 0 : options.motorCurrentDenoiseAlpha) !== null && _e !== void 0 ? _e : 0.1; this.debug = !!(options === null || options === void 0 ? void 0 : options.debug); this.checkRange(this.timeout, 0, 10000, 'timeout'); this.checkRange(this.maxGetAttemptCount, 1, 10, 'maxGetAttemptCount'); this.checkRange(this.maxMotorOutputRate, 0.01, 1, 'maxMotorOutputRate'); this.checkRange(this.motorCurrentDenoiseAlpha, 0.0000001, 1, 'motorCurrentDenoiseAlpha'); this.serial = new SerialPortClass(path, { baudRate: (_f = options === null || options === void 0 ? void 0 : options.baudRate) !== null && _f !== void 0 ? _f : 38400, autoOpen: false, }); let connectIntervalHandle; const connect = () => { // Attempt to connect once per second. connectIntervalHandle = setInterval(() => this.serial.open(), 1000); }; // When a connection is established, cancel the connection attempt function. this.serial.on('open', () => { if (connectIntervalHandle) { clearInterval(connectIntervalHandle); connectIntervalHandle = null; this.lastError = null; } }); this.serial.on('error', (err) => { this.debug && console.error(`Sabertooth (${this.path}) error:`, err); this.lastError = err; }); this.serial.on('close', (err) => { this.debug && console.error(`Sabertooth (${this.path}) disconnected, attempting to reconnect`); this.lastError = err; connect(); }); connect(); } checkRange(value, min, max, argName) { if (value < min || value > max) { throw Error(`Sabertooth (${this.path}): invalid value for ${argName}: ${value}, must be in range [${min}, ${max}]`); } } /** * Controls the specified motor output(s). * This sets the output of the motor channel as a fraction of the battery voltage. * Note that the given rate is scaled by the `maxMotorOutputRate`. * * @param channel the motor channel(s), either `1`, `2`, or `*` for all motors. * @param rate the new rate for the motor(s), in range [-1, 1] for maximum reverse to maximum forward respectively. */ setMotor(channel, rate) { this.checkRange(rate, -1, 1, 'rate'); this.set(Type.Motor, channel, Math.round(rate * this.maxMotorOutputRate * 2047)); } /** * Controls the specified power output, if the power output is configured as a controllable output. * * @param channel the power channel(s), either `1`, `2`, or `*` for all channels. * @param rate the new rate for the power output(s), in range [-1, 1] */ setPower(channel, rate) { this.checkRange(rate, -1, 1, 'rate'); this.set(Type.Power, channel, Math.round(rate * 2047)); } /** * Controls the mixed-mode (differential) drive rate. * Note that the given rate is scaled by the `maxMotorOutputRate`. * * @param rate the new drive rate for the motors, in range [-1, 1] for maximum reverse to maximum forward respectively. */ setDrive(rate) { this.checkRange(rate, -1, 1, 'rate'); this.set(Type.Motor, MixedModeMotor.Drive, Math.round(rate * this.maxMotorOutputRate * 2047)); } /** * Controls the mixed-mode (differential) turn rate. * * @param rate the new turn rate for the motors, in range [-1, 1] for maximum left to maximum right respectively. */ setTurn(rate) { this.checkRange(rate, -1, 1, 'rate'); this.set(Type.Motor, MixedModeMotor.Turn, Math.round(rate * 2047)); } /** * Enables or disables freewheeling for the specified motor output(s). * * @param channel the motor channel(s), either `1`, `2`, or `*` for all motors. * @param enableFreewheel True to enable freewheeling, false to disable freewheeling. */ setFreewheel(channel, enableFreewheel) { this.set(Type.Freewheel, channel, enableFreewheel ? 2048 : 0); } /** * Shuts down motor or power output(s). * * @param type The type of output to shutdown, either `SetType.Motor` or `SetType.Power` * @param channel the channel(s), either `1`, `2`, or `*` for all channels. * @param enableFreewheel True to trigger shutdown, false to clear the shutdown. */ shutDown(type, channel, shutdownEnabled) { this.set(type, channel, shutdownEnabled ? 2048 : 0, SetType.Shutdown); } /** * Sets the current limit for the specified motor output channel. * * @param channel the motor channel(s), either `1`, `2`, or `*` for all channels. * @param currentLimit the new current limit on Amps, in range (0, 100], or -1 to use the default limit. */ setCurrentLimit(channel, currentLimit) { this.checkRange(currentLimit, -1, 100, 'currentLimit'); this.set(Type.CurrentLimit, channel, currentLimit < 0 ? -1 : Math.round(currentLimit / 100 * 2047)); } /** * Sets the ramping for the specified motor output channel. * * @param channel the motor channel(s), either `1`, `2`, or `*` for all channels. * @param ramping The ramping value, between -16383 (fast) and 2047 (slow). */ setRamping(channel, ramping) { this.checkRange(ramping, -16383, 2047, 'ramping'); this.set(Type.Ramping, channel, ramping); } /** * Get the battery/source voltage. */ getBatteryVoltage() { return __awaiter(this, void 0, void 0, function* () { return (yield this.get(Type.Motor, 1, GetType.Battery)) / 10; }); } /** * Get the output transistor temperature for the specified motor channel, in degrees centigrade. */ getMotorDriverOutputTemperature(channel) { return __awaiter(this, void 0, void 0, function* () { this.checkRange(channel, 1, 2, 'channel'); return this.get(Type.Motor, channel, GetType.Temperature); }); } /** * Get the output rate for the specified motor channel, in range [-1, 1]. */ getMotorDriverOutputRate(channel) { return __awaiter(this, void 0, void 0, function* () { this.checkRange(channel, 1, 2, 'channel'); return (yield this.get(Type.Motor, channel, GetType.Value)) / 2047; }); } /** * Get the output current for the specified motor channel, in Amps. * This is a noisy signal and may vary by up to several amps. * Positive current values indicate energy is being drawn from the battery, * and negative values indicate energy is being regenerated into the battery. */ getMotorCurrent(channel) { return __awaiter(this, void 0, void 0, function* () { this.checkRange(channel, 1, 2, 'channel'); const currentValue = (yield this.get(Type.Motor, channel, GetType.Current)) / 10; const previousValue = this.motorCurrentExponetialAverage[channel]; const denoisedValue = previousValue * (1 - this.motorCurrentDenoiseAlpha) + currentValue * this.motorCurrentDenoiseAlpha; this.motorCurrentExponetialAverage[channel] = denoisedValue; return denoisedValue; }); } get(type, channel, getType, scaled = true) { if (!scaled) getType |= 2; return new Promise((resolve, reject) => { let attemptCount = 0; let timeoutHandle; const attemptRequest = () => { attemptCount += 1; this.sendCommand(Command.Get, [getType, type, channel]); // If timeout enabled. if (this.timeout > 0) { timeoutHandle = setTimeout(() => { this.debug && console.warn(`Sabertooth (${this.path}) get request timed out after ${attemptCount} attempts`); if (attemptCount === this.maxGetAttemptCount) { this.debug && console.error(`Sabertooth (${this.path}) aborting get request`); this.serial.removeListener('data', dataListener); reject(Error(`Sabertooth (${this.path}) get request timed out`)); } else { this.debug && console.warn(`Sabertooth (${this.path}) retrying get request`); // Try again... attemptRequest(); } }, this.timeout); } }; const dataListener = (data) => { if (getType === (data[2] & ~1) && type === data[6] && channel === data[7]) { clearTimeout(timeoutHandle); this.serial.removeListener('data', dataListener); const value = data[4] << 0 | data[5] << 7; resolve((data[2] & 1) !== 0 ? -value : value); } }; this.serial.on('data', dataListener); attemptRequest(); }); } set(type, channel, value, setType = SetType.Value) { let flags = setType; if (value < 0) { value = -value; flags |= 1; } const data = [ flags, (value >> 0) & 0x7f, (value >> 7) & 0x7f, type, (typeof channel === 'string') ? channel.charCodeAt(0) : channel, ]; this.sendCommand(Command.Set, data); } sendCommand(command, data) { const buffer = [this.address, command, data[0]]; appendChecksum(buffer, 0); if (data.length > 1) { data.slice(1).forEach(d => buffer.push(d)); appendChecksum(buffer, 4); } this.serial.write(Buffer.from(buffer)); } } exports.SabertoothUSB = SabertoothUSB; /** Get a list of the avilable Sabertooth devices. * The return is an array of objects with fields like: * ``` * manufacturer: 'Dimension Engineering', * serialNumber: '1600DB368EC8', * pnpId: 'usb-Dimension_Engineering_Sabertooth_2x32_1600DB368EC8-if01', * locationId: undefined, * vendorId: '268b', * productId: '0201', * path: '/dev/ttyACM0' * ``` */ const listSabertoothDevices = () => __awaiter(void 0, void 0, void 0, function* () { return (yield SerialPortClass.list()).filter(port => { var _a; return (_a = port.pnpId) === null || _a === void 0 ? void 0 : _a.startsWith('usb-Dimension_Engineering_Sabertooth'); }); }); exports.listSabertoothDevices = listSabertoothDevices; //# sourceMappingURL=index.js.map