- Register the Feetech STS3235 (model number 2057) under the STS3215 definition: it reads and moves through the STS3215 control table.
- Python:
ServoDefinition.refuses(status)tells a refusal from an alarm in the error field the*_with_errorreads and writes return, asServoDefinition::refusesdoes in Rust, so a Python caller fails an access only when the motor refused it.
Bus::set_torque,change_idandchange_baudrate(and the torque off and lock opening ahead of the last two) check the error field of each answer: a motor that refuses the write has failed, with a newStatusPacketError(its id andStatusError;RuntimeErroron Python), where they used to report success.set_torquelists it with the motors that failed, and like aRegisterErrorit is not retried. An alarm in an answer (overload, temperature, voltage, the v2 alert bit) is not a refusal.ServoDefinition::refusestells a refusal from an alarm: an error number on protocol v2, an instruction, checksum or range error on protocol v1, never on a servo whose newstatus_alarms_onlysays its status byte only reports alarms, as the Feetech servos.
- A failed sync read names the motor: the error is a
SyncReadError { id, cause }(reached withdowncast_ref, and printedmotor 12: Timeout error). A plain sync read names the motor whose status packet did not come back. A fast sync read names the first motor whose block is not there as asked, read from what arrived before the timeout, which used to be thrown away: missing, cut short, out of order (Incorrect id) or failing its CRC. A packet where nothing came back names the first motor, whose block carries the header. A fast sync read that times out now reportsTimeoutErroras that cause, where it used to return the port'sio::Error; an I/O error other than a timeout is still returned as it is. - Register the Feetech STS3036 (model number 521) under the STS3215 definition: an ST-3036-C001 answers with that number and reads and moves through the STS3215 control table (checked on one servo at 6 V and 1 Mbps).
find_model(name)returns the model number of a servo model and the definition that covers it, from the registry (DEFINITIONSlists every definition). Names compare without case, hyphens or underscores, so LeRobot'sxl330-m288findsXL330M288.ServoDefinitiongainsmodels, the (name, model number) pairs it covers, and on Pythonfind_modelplusmodels,resolution,word_order,supports_sync_read,baudrates,registers()andregister(name)onServoDefinition, so code holding a definition (what aBustakes) needs no controller class.- Three more facts a definition can state, ahead of
encoding::factory_baudrate:,homing_offset_sign:(present = actual + sign * homing_offset) andoperating_modes: [(position, 0), ...], the values of the operating mode register by name. A sign without ahoming_offsetregister, or modes without anoperating_moderegister, fail the build. Filled in for the STS3215 (1 Mbps, -1; position, velocity, pwm, step), the SCS0009 (1 Mbps) and the XL330 and XL430 (57.6 kbps, +1; the X-series modes, of which the XL430-W250 refuses the current ones). OnServoInfo, and on Python asServoDefinitiongetters. Bus::set_torque(ids, enabled, retries)turns the torque of every motor asked on or off, trying each one even after another fails, and returns the motors that failed with their error ({id: message}on Python, empty when all went through). On servos whose definition sayseeprom_lock: true(STS3215, SCS0009), the lock follows the torque: closed with it on, open with it off. The new fact is onServoInfoand the PythonServoDefinition; it fails the build on a servo without alockregister.- Python:
Bus.is_open, andBusas a context manager:with Bus(...) as bus:releases the serial port on the way out, an exception raised in the block included. DynamixelProtocolHandler::broadcast_pingpings every motor at once and returns the ids that answer, listening forbroadcast_ping_window(baudrate): the status packets of every possible id, 3 ms of turn per id and 16 ms for a USB adapter's latency timer, the window the vendor SDKs use (about 0.8 s at 1 Mbps). Protocol v2 Dynamixel and Feetech STS answer it; protocol v1 Dynamixel and Feetech SCS do not. What arrives in the window is parsed once it closes, resyncing on the next header past anything that does not frame a status packet (a stray byte, a packet cut short), as the Dynamixel SDK does, so it costs that packet and not the motors after it.- A protocol v1 status packet with a bad checksum no longer prints to stdout; it is still
a
ChecksumError. broadcast_scan(ids)/broadcast_scan_all()onServoDefinition, the controllers andBus(broadcast_scan(ids=None)on Python): which ids answer, found with one broadcast ping, then the model number of each id asked that answered, read under the port's own timeout. The ping's window does not shrink with the number of ids, soscanstays the fast way to check a few ids and is unchanged;broadcast_scanis for sweeping many, and for USB adapters whose latency timer outlastsscan's short timeout. Servos statesupports_broadcast_ping: true(STS3215, XL320, XL330, XL430) onServoInfo, the Python controller classes andServoDefinition; on any other,broadcast_scanis aRegisterError::BroadcastPing(ValueErroron Python) and nothing is sent.change_id(definition, id, new_id)andchange_baudrate(definition, id, baudrate)onServoDefinitionandBus(and the PythonBus): torque off and, on servos with a lock register, lock open, then the write. The baud rate is given in bauds and written as the value the definition'sbaudrateslists for it; a rate it does not list is aRegisterError::Baudrate(ValueErroron Python) and nothing is sent; the port stays at its rate, for the caller to switch withset_baudrate. Likescan, both reach motors the bus does not have.- Python:
baudrates()(slowest first) andmodels()(by name) on the controller classes return their dict in a fixed order, like theServoDefinitiongetters. They came from aHashMap, so the order changed from one process to the next, and so did the order in which a caller trying each baud rate found a motor.
- Register the model numbers of the X-series servos that share an existing definition: XC330-T181 and XC330-T288 under XL330; XC430-W150, XM430-W350, XM540-W270 and XH540-W150 under XL430, whose definition is the XM430 control table. On the Feetech side, STS3250 and SM8512BL join STS3215, and the STS3215 model number is corrected to 777: bytes 9 and 3 at address 3 read little-endian (the STS byte order), as LeRobot's handshake reads them; 2307 was the same bytes swapped and never matched.
- STS3215 and SCS0009: add the firmware version bytes and the factory block (moving
velocity threshold, DTs, velocity unit factor, Hts and maximum velocity limit on the
STS3215; acceleration, sync write flag, PWM maximum step, and the velocity threshold and
limits on the SCS0009). Registers that exist on Dynamixel servos under another name get
that name as a second name:
model_number,baud_rate,min_position_limit,max_position_limit,homing_offset,operating_mode,goal_velocity,present_velocity. Same address, size and access; the original names stay. - Feetech SCS0009: the model number is 1284, not 1280: bytes 5 and 4 at address 3, read big-endian as the SCS series stores words. LeRobot has checked the 16 SCS0009 of its HopeJR hand against 1284 at every connection.
- Servo definitions can state what a raw-address caller needs to know beyond the
registers, with
resolution:,word_order:,supports_sync_read:,baudrates:andencoding:entries ahead of thereg:list, all optional. They fill aServoInfo(INFOon each servo module) and anencodingon everyRegisterInfo: unsigned, two's complement, or sign-magnitude with its sign bit, taken from the register's integer type unless the definition says otherwise; anencoding:entry that names no register of the servo fails the build. Each servo module also getsMODELS, the (name, model number) pairs its registry entry lists. Filled in for the Feetech STS3215 and SCS0009 (big-endian, no Sync Read; speeds and load sign-magnitude on both) and the Dynamixel XL330 and XL430, resolution alone for MX, AX and XL320. - Python:
resolution(),word_order(),supports_sync_read(),baudrates()andmodels()on every controller class, static likeregisters(), andencoding/sign_bitonRegisterInfo. set_baudrate(baudrate)andset_timeout(duration)on every controller, and on the Python classes with the timeout in seconds like the constructor's (a negative, NaN or infinite one raisesValueError). Both change the open port in place. A caller probing a bus at each rate a motor might be at, or shortening the timeout for an ID sweep, no longer has to close the controller and build a new one, which on Python could fail on a port still held by a traceback.- Integer access to registers chosen by name:
read_register(id, name),write_register(id, name, value),sync_read_register(ids, name)andsync_write_register(ids, name, values)on every controller, plus_with_errorvariants of the first three carrying the status packet's error field. They apply what the definition states about the register: the bytes go in the servo's word order (a big-endian servo swaps the two bytes of each 16-bit word, low word first) and the sign follows the register's encoding, so a Feetech sign-magnitude offset reads as-709and writes back as0x0AC5. A value that does not fit the register, or a name the servo does not define, fails before anything reaches the bus (RegisterError, aValueErroron Python). The same eight methods exist on the Python classes and release the GIL like the raw ones.WordOrder::to_bytes/from_bytes,Encoding::encode/decodeandRegisterInfo::encode/decodeare public for callers holding their own bytes. scan(ids)on every controller: which ofidsanswer, with their model number, as one Model Number read per id. The sweep runs under a read timeout sized to the baud rate (servo::scan_timeout: 320 bits of wire time, 5 ms at least) so that absent ids do not each cost the port's timeout, and puts the timeout back afterwards. On Python it returns{id: model number}and releases the GIL for the whole sweep.scan_all(), andscan()without ids on Python, sweep every id the protocol allows: 0 to 253 on v1, 0 to 252 on v2, now given byDynamixelProtocolHandler::max_id().with_retries(retries, op)on every controller runs a register access again, up toretriesmore times, while it fails on the bus: a timeout or a corrupted status packet. A bad name or value fails at once, and a motor answering with a fault is not retried, since the_with_errorvariants carry its error field. The Python by-name methods take it as aretries=0keyword, so a caller retrying a read no longer crosses back into Python between attempts.sync_read_registerand its_with_errorvariant read a servo whose firmware has no Sync Read (the Feetech SCS series,supports_sync_readfalse) one id at a time, in the order asked, instead of sending an instruction that is never answered. The raw and typed sync reads still send Sync Read as asked.- Register access by name for buses that mix servo definitions, or protocols (Reachy
Mini has STS3215 on v1 and XL330 on v2 on one port). Each servo module has its
definition as
DEFINITION, aServoDefinitionvalue (name, protocol,ServoInfo, registers;Hash, so ids group by it), and the by-name access andscanare on it, taking the protocol handler and the port like the typed module functions; the definition checks that the handler speaks its protocol. The controllers' by-name methods now call their ownDEFINITION, with no change to their API.servo::definition::sync_read_register/sync_write_registerreach motors of several definitions sharing a protocol and the register's address and size in one instruction, each value laid out through its motor's definition.bus::Busholds a port, a handler per protocol and the definition of each motor, and sends one instruction per such group, every group encoded before the first is sent. On Python:Bus(serial_port, baudrate, timeout, {id: definition})with the same methods as a controller's by-name ones,retriesincluded,scan(definition, ids=None), and the staticdefinition()of each controller class.RegisterErrorgainsLayout,Protocol,UnknownMotorandValueCount, all found before anything is sent.
- Feetech STS3215:
maximum_acceleration(address 85) is one byte, not two, andacceleration_multiplieris added at 86. The two-byte size was inherited from an early LeRobot table that marked the register as not in the memory table; Feetech's memory table has two one-byte registers there, so a two-byte write clobbered the multiplier. - XL430: the baud rate register is now spelled
baud_rate, as on the XL330; it wasbuad_rate. The generatedread_buad_rate/write_buad_rateaccessors and their sync variants are renamed accordingly, in Rust and Python. - Python: expose each servo's control table. Every controller class gets two static
methods,
registers()andregister(name), so a register name can be resolved to its address and size from Python without opening a serial port. The Rust side already hadREGISTERSandregister(); the Python side was left to copy the table by hand. Entries areRegisterInfovalues (name,addr,size,access) andaccessis aRegisterAccessenum (Read,Write,ReadWrite); both types are new in the module.
- Expose each servo's control table: every servo module now has
REGISTERS: &[RegisterInfo]andregister(name), giving each register's name, address, size and access (read, write or read-write). Code that picks a register at runtime no longer has to hand maintain a name to accessor match.RegisterInfois#[non_exhaustive]. - Add the
indirect_addressingexample, which points a block of indirect address slots at scattered registers so they can be read as one contiguous block, and compares it against one read per register and one wide read spanning the gaps. Registers are named on the command line and resolved throughREGISTERS. - Fix the documentation generated for the per-register accessors: a register with a
conversion documented only its raw accessor, one without rendered the literal
placeholder
Read register $name (addr: $addr, ...), and raw and converted labels were swapped on several controller and Python methods. - Python: add
close()andis_open()on the controllers, so the serial port is released at a known point instead of whenever the object happens to be dropped. Methods called on a closed controller raiseRuntimeError. - Python: release the GIL for the duration of every serial transaction. Other Python
threads now keep running while the bus is busy, instead of being blocked for the whole
read or write. Covers the raw-address bindings and every generated per-register
accessor (
read_*,sync_read_*,write_*,sync_write_*), plusping,rebootandfactory_reset. The Python API is unchanged -- the generatedrustypot.pyiis byte-identical. - Document
sys.setswitchintervalin the README: it caps how much GIL time a competing thread gets back, and its 5 ms default dominates a millisecond-scale bus transaction. - Surface the status packet's error field, which was parsed and then dropped, so a caller
can tell a healthy motor from one answering while it reports a fault. Adds
read_with_error,write_with_errorandsync_read_with_erroron the protocol handler (sync read included, since a control loop polls there), the matchingread_raw_data_with_error,write_raw_data_with_errorandsync_read_raw_data_with_erroron the controllers, and all three in Python.read/write/sync_readare untouched. - Add
StatusError, a newtype over that byte with the two protocol readings named:v1_conditions()decodes the v1 bitfield, whilev2_instruction_error()andv2_alert()read the v2 layout, where bits 0-6 are an error number rather than flags.DynamixelErrorV1is now public. Python receives the raw byte, as the vendor SDKs do. - Fix
with_post_delaybeing ignored bysync_read,fast_sync_readandsync_write. The delay is documented as applying after each communication, but onlyread,writeandwrite_fbever slept it, so a control loop polling withsync_read-- the common case on hardware that needs the gap -- ran without one. Every transaction now sleeps it, including one that failed, which is when an immediate retry would otherwise close the gap;writeandwrite_with_errorpreviously skipped it on error and no longer do.
- Add fast sync read (protocol v2 instruction 0x8A): every motor appends its answer to a
single status packet returned from the broadcast id, instead of each sending its own.
Available as
DynamixelProtocolHandler::fast_sync_read, or by enabling it once withwith_fast_sync_read()so allsync_read_*methods use it. Requires firmware new enough to implement it (XL330: v46+); older firmware does not answer and the read times out. - Add the
fast_sync_read_benchexample, which checks both instructions return the same data and times them side by side. - Python: add
set_fast_sync_read()on the protocol v2 controllers, which reroutes the existingsync_read_*methods.
- Handle Dynamixel protocol 2.0 byte stuffing: status packets whose data contains FF FF FD are now de-stuffed on reception (previously returned oversized params, breaking fixed-size reads e.g. when present current = -1), and instruction packet params are stuffed on transmission.
- Bump pyo3 to 0.29.0 and pyo3-stub-gen to 0.23.0 (fixes RUSTSEC-2026-0176, an out-of-bounds read in the PyList/PyTuple iterators).
- Track Cargo.lock in the repository.
- Add a memory soak test example.
- Add support for feetech Scs0043 motor.
- Pin pyo3 to 0.27.1 and pyo3-stub-gen to 0.21.0.
- Make flush method more robust and replace potential panic with error.
- Add support for factory reset in core library and python bindings.
- Add reboot support in core library.
- Add ping and reboot support in python bindings.
- Add support for AX motors (see #93, thanks to @kacper-uminski)
- Add support for feetech Scs0009
- Add python type annotation
- Cleanup APIs to offer two interfaces:
- high-level interface (Controller) with a simple API for the most common use cases.
- low-level interface (DynamixelProtocolHandler) for direct access to the protocol and fine-grained control of the bus ownership.
- Add Python bindings for the library (controller API).
- Add support for the feetech servo.
- Define register conversion at the macro level to simplify the code.
- Add dxl XL330 support
- Add an post delay option to the read and write method.
- Add dxl XM motor device
- Add support for orbita-2dof-foc device.
- Patch torque limit conversion.
- Add support for orbita-foc device.
- Add support for timeout in sync read v1
- Support protocol v1 and v2
- Support read, sync read, write, sync write, ping
- Support mx and xl-320