A TypeScript/Node.js driver for Feetech STS3215 serial-bus servos using the SMS/STS half-duplex TTL protocol.
Use a TTL half-duplex servo adapter that exposes a serial port to the host. Connect adapter DATA to the servo signal line and connect adapter ground, servo ground, and power-supply ground together. Power the servo from a suitable external supply; do not power it from a USB serial signal pin.
Both 7.4 V (19 kg·cm) and 12 V (30 kg·cm) products are sold as STS3215. Check the label or datasheet for your exact unit and use only its rated supply voltage. This library cannot detect the safe supply voltage.
The default serial format is 1,000,000 baud, 8 data bits, one stop bit, and no parity. Caller-selected rates are supported down to the product's advertised 38,400 baud.
npm install sts3215-nodeNode.js 18 or newer is required.
import { STS3215Bus } from "sts3215-node";
const bus = await STS3215Bus.open({
path: "/dev/cu.usbserial-110", // Use your adapter's path (COM3 on Windows, for example).
baudRate: 1_000_000,
timeoutMs: 100,
});
const servo = bus.servo(1);
try {
await servo.ping();
await servo.enableTorque();
await servo.moveTo({ position: 2048, speed: 1000, acceleration: 50 });
const telemetry = await servo.readTelemetry();
console.log(telemetry.positionSteps, telemetry.positionDegrees);
console.log(telemetry.voltageRaw, telemetry.voltageV, telemetry.temperatureC);
} finally {
try {
await servo.disableTorque();
} finally {
await bus.close();
}
}close() is idempotent. It removes the transport listener, aborts active and queued operations, clears timers/parser state, and closes the serial port.
Single-turn position commands accept 0..4095, with 2048 as nominal center and approximately 0.088° per step. degreesToSteps() and stepsToDegrees() are exported for conversion.
const servo = bus.servo(1);
await servo.setWheelMode(); // EEPROM operation: torque is disabled and the write is verified.
await servo.setSpeed({ speed: -800, acceleration: 20 });
const telemetry = await servo.readTelemetry();
// positionSteps, positionDegrees, speedRaw, loadRaw, voltageRaw,
// voltageV, temperatureC, moving, currentRawSpeed and load use Feetech signed-magnitude decoding. Positive and negative speed values select direction. Voltage has a documented 0.1 V scale. Current remains currentRaw because no cross-variant ampere conversion is treated as authoritative.
Raw byte operations are available as read(), write(), regWrite(), action(), and syncWrite(). Metadata-backed readRegister() and writeRegister() validate register access, width, and encoding.
import { STS3215_REGISTERS } from "sts3215-node";
const position = await bus.readRegister(1, STS3215_REGISTERS.PRESENT_POSITION);
await bus.writeRegister(1, STS3215_REGISTERS.TORQUE_ENABLE, 0);EEPROM-changing high-level helpers disable torque, unlock EEPROM, write, verify where communication remains possible, and re-lock in a finally path. setBaudRate(115_200) accepts one of the exported SupportedBaudRate values and cannot verify until the bus is reopened at the new rate. setId() re-locks and pings using the new ID before updating the handle. setPositionOffset() provides explicit signed calibration. Factory reset requires factoryReset({ confirm: true }).
import { STS3215Bus, encodeSignedMagnitude16 } from "sts3215-node";
const bus = await STS3215Bus.open("/dev/cu.usbserial-110");
try {
await bus.syncWrite(42, 2, [
{ id: 1, data: encodeSignedMagnitude16(1024) },
{ id: 2, data: encodeSignedMagnitude16(3072) },
]);
} finally {
await bus.close();
}Sync and other broadcast writes resolve after the bytes are drained and do not wait for status packets.
sts3215 scan --port /dev/cu.usbserial-110 --baud 1000000
sts3215 ping --port /dev/cu.usbserial-110 --id 1
sts3215 status --port /dev/cu.usbserial-110 --id 1
sts3215 move --port /dev/cu.usbserial-110 --id 1 --position 2048 --speed 500 --torque
sts3215 torque --port /dev/cu.usbserial-110 --id 1 --off
sts3215 --helpThe port is always explicit. move only enables torque when --torque is supplied and disables it again after the command, including on failure. The CLI intentionally omits destructive EEPROM configuration commands.
- A timeout usually means the wrong port, baud rate, servo ID, missing common ground, inadequate power, or swapped/missing DATA wiring.
PacketChecksumError,PacketFramingError, andPacketLengthErrorindicate corrupt or malformed received data rather than silence. EnableonDebugLogandonDiscardedBytesin bus options to inspect recovery and discarded bytes.- If an adapter echoes transmitted packets, leave
suppressEchoenabled (the default). Disable it only for adapter diagnosis. - Servo-reported failures throw
ServoStatusError, which retains the raw error byte, decoded flags, servo ID, and raw packet. - After changing baud rate, close the bus and reopen the adapter at the corresponding new rate.
npm test
npm run test:coverage
npm run build
npm run test:packageUnit and fake-transport tests require no hardware. Hardware tests are skipped unless STS3215_PORT is set:
STS3215_PORT=/dev/cu.usbserial-110 \
STS3215_BAUD_RATE=1000000 \
STS3215_ID=1 \
npm testMotion remains disabled by default. In a physically safe setup, opt in with STS3215_ENABLE_MOTION_TESTS=true. The smoke test pings, reads model/telemetry, disables torque, performs only the optional bounded move, disables torque in finally, and closes the port. It never changes EEPROM or resets the servo.