sabertooth-usb
Version:
API for controlling USB-enabled Sabertooth motor drivers running in Packet Serial mode
339 lines • 15.5 kB
JavaScript
"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