Skip to content

Proposal: find a model by name, state more servo facts, and turn the torque of a whole bus - #143

Merged
pierre-rouanet merged 4 commits into
developfrom
feat/servo-facts-and-torque
Oct 5, 2026
Merged

pierre-rouanet merged 4 commits into
developfrom
feat/servo-facts-and-torque

Conversation

@nicolas-rabault

@nicolas-rabault nicolas-rabault commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

This is a proposal, and turning it down (all of it or some commits) is a perfectly fine answer. Each commit stands alone, and for each one I say what LeRobot would do instead if you'd rather not have it in rustypot.

Why we are asking

LeRobot's port to rustypot.Bus (huggingface/lerobot#4672) got a review asking LeRobot to keep only robot logic, so that its two vendor bus classes (FeetechMotorsBus, DynamixelMotorsBus) can merge into one. What still keeps them apart is a short list of facts about the servos, transcribed by hand from the manuals:

  • which rustypot class covers a model name such as sts3215 or xl330-m288, and that model's exact number;
  • the homing offset is subtracted from the position on Feetech servos and added on Dynamixel ones;
  • position control is operating mode 0 on a Feetech and 3 on a Dynamixel, where 0 is current control (writing the Feetech value to a Dynamixel puts it in current mode);
  • Feetech servos have a lock register guarding their EEPROM.

The same review found a gap in how LeRobot releases a robot: torque goes off motor by motor, and the first one that does not answer stops the loop, leaving the motors after it holding their position.

The question behind this PR is whether those facts belong in the servo definitions. I think they do, because they are about the servos and any caller of the raw or by-name API needs them, but you know better than me what rustypot should carry. #142 covers finding and setting up motors and is independent of this one.

What it proposes

1. find_model(name), and a definition readable from Python (c9eeda5)

find_model(name) walks the registry and returns (model number, definition), comparing names without case, hyphens or underscores, so xl330-m288 finds XL330M288. Each ServoDefinition now carries the models it covers, and the Python ServoDefinition gets the getters the controller classes have as static methods (models, resolution, word_order, supports_sync_read, baudrates, registers(), register(name)). With that, a caller holding a definition, which is what a Bus takes, needs no controller class.

  • For: one lookup instead of a per-vendor list of classes; it also gives the exact model number, so a caller can tell an SM8512BL from an STS3215 even though they share a definition.
  • Against: the name matching is a convenience that may surprise (XL-330_M288 matches too). The Python getters duplicate the controller static methods. ServoDefinition gains a public field, so code building one by hand must add it.
  • If you'd rather not: LeRobot keeps a list of controller classes per vendor, which is the thing the review asks to remove.

2. homing_offset_sign, operating_modes and factory_baudrate (f347124)

Three optional facts a definition can state, ahead of encoding:, on ServoInfo and as Python getters:

STS3215 SCS0009 XL330, XL430
factory_baudrate 1 Mbps 1 Mbps 57.6 kbps
homing_offset_sign (present = actual + sign * offset) -1 none +1
operating_modes position 0, velocity 1, pwm 2, step 3 none current 0, velocity 1, position 3, extended_position 4, current_based_position 5, pwm 16

A sign on a servo without a homing_offset register, or modes without an operating_mode register, fail the build, like the encoding: names already do.

  • For: the facts sit next to the registers they describe, are checked at compile time, and a caller can say "position" without knowing the family.
  • Against: the XL430 definition also covers the XM430, so it lists the current modes that the XL430-W250 refuses; a per-model table would be exact, at the cost of a new place to describe models. The mode names are mine (I took the Robotis manual's for the X series). homing_offset_sign is one way to put it; you may prefer to describe the offset some other way.
  • If you'd rather not: LeRobot keeps two small tables, one per vendor.

3. Bus::set_torque(ids, enabled, retries) (fad79fa)

Turns the torque of every motor asked on or off, trying each one even after another fails (each with its own retries), and returns the ones that failed with their error ({id: message} on Python, empty when all went through). On a servo whose definition says eeprom_lock: true (STS3215, SCS0009), the lock follows the torque: closed with it on, open with it off. The AX and MX lock only clears at power-up, so they do not set the flag and their lock is never touched.

  • For: releasing a robot cannot stop halfway, and the Feetech lock rule lives with the servo.
  • Against: the lock following the torque is LeRobot's convention, not something Feetech requires. If that feels like too much policy, set_torque could leave the lock alone and LeRobot would handle it with the eeprom_lock fact, or without it. Returning the failures instead of a Result is unusual for this API.
  • If you'd rather not: LeRobot loops over the motors itself and collects the errors, as its branch now does.

4. Bus.is_open, and Bus as a context manager (ca8d69c)

is_open says whether close() has run; with Bus(...) as bus: closes it on the way out, an exception in the block included.

  • For: a caller can check the port was really released (LeRobot's tests could only trust their own flag), and the cleanup no longer needs a try/finally at every call site.
  • Against: nothing much beyond two more Python entry points.

Not included, and maybe not worth it

  • Typed errors (a timeout vs a port that is gone, so a caller can stop retrying a dead port): it needs care first, because today a failed write to the port (USB cable pulled) comes back as CommunicationErrorKind::TimeoutError, the same as a motor that did not answer. Worth a separate discussion if you are interested.
  • A timeout computed from the baud rate and the packet size: LeRobot now sets 50 ms itself, which covers its cases.
  • A factory id: it is 1 on every servo here, so I left it out.

Tests

  • Unit tests on the fake port: find_model by name with and without hyphens, the new facts of the STS3215, SCS0009, XL330, XL430 and MX, set_torque (torque then lock on an STS3215, torque only on an XL330, a motor that does not answer and an id not on the bus both reported while the others go through, the lock closing when the torque goes on).
  • I checked that a homing offset sign on a servo without the register fails the build.
  • From Python, on an SO-101 follower: find_model and the getters; set_torque(off) on its six motors plus an id that is not there, put in the middle of the list (the six went through, only the missing one came back, after three tries, in 0.16 s); the with block releases the port, also after an exception, and the port opens again straight away. I did not turn the torque on from these tests, since a motor would jump to whatever goal position it last had.

Merging with #142

The two PRs add to the same places (the Unreleased section of the changelog, src/bus.rs, the servo README), so whichever lands second will conflict there. I checked that the combination builds and passes the tests, and I will rebase the second one.

… Python

A caller that knows a motor by its model name, as LeRobot's robot configs
do (sts3215, xl330-m288), has to know which controller class covers it
before it can reach the model number, the resolution or the registers.
LeRobot keeps one bus class per vendor for little more than that list.

find_model(name) walks the registry and returns the model number and the
definition, comparing names without case, hyphens or underscores. Each
definition now carries the models it covers, and the Python
ServoDefinition exposes what the controller classes expose as static
methods, so the definition a Bus is given is enough on its own.
Three facts about a servo that every caller transcribes by hand from the
manuals, and gets wrong across families:

- the homing offset is subtracted from the position on Feetech servos and
  added on Dynamixel ones, so centring a joint computes it the other way;
- the operating mode register takes 0 for position control on a Feetech
  and 3 on a Dynamixel, where 0 is current control;
- a new motor answers at 1 Mbps on Feetech and 57.6 kbps on the XL330 and
  XL430.

A definition can now state them as factory_baudrate, homing_offset_sign
and operating_modes (by name), filled in for the STS3215, SCS0009, XL330
and XL430. A sign or modes on a servo without the register they describe
fails the build.
Releasing a robot means turning the torque off on all its motors. Written
as a loop of writes, the first motor that does not answer raises and
leaves every motor after it holding its position, which is the one thing
the release was for.

Bus::set_torque tries every motor, each with its own retries, and returns
the ones that failed with their error instead of stopping at the first.

Feetech servos also have a lock register guarding the EEPROM, which they
only write to with the lock open; LeRobot closes it with the torque on and
opens it with the torque off. A definition can now say eeprom_lock: true
(STS3215, SCS0009) and set_torque moves the lock with the torque on those.
The AX and MX lock is not that kind of lock (it only clears at power-up)
and is left alone.

Checked on an SO-101 follower: torque off on its six motors plus an id
that is not there, in the middle of the list; the six went through and
only the missing one came back, after three tries, in 0.16 s.
A Python caller could close a Bus but not ask whether it still held its
port, so a test checking that a failed connection released the port
could only trust its own bookkeeping. And releasing the port on every way
out of a block took a try/finally at every call site.

Bus.is_open says whether close() has run, and `with Bus(...) as bus:`
closes it on the way out, an exception in the block included, which then
goes on. Checked on an SO-101 follower: the port is released after the
block, after an exception in it, and opens again straight away.

@pierre-rouanet pierre-rouanet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, here's my review commit by commit. All four are accepted.

1. find_model and the Python definition getters (c9eeda5): accepted.

2. homing_offset_sign, operating_modes, factory_baudrate (f347124): accepted. One thing to keep in mind about factory_baudrate: it is a fact about current motors, not something that holds for every unit forever. Some older motors shipped with a different factory baud rate, a change that came with the protocol v1 → v2 transition. I don't expect it to happen again, so one value per definition is fine. Maybe mention it in the doc comment so nobody reads it as a guarantee for every unit ever made.

3. Bus::set_torque (fad79fa): accepted.

4. Bus.is_open and the context manager (ca8d69c): accepted.

A suggestion for the docs: between #142 and this PR, Bus and ServoDefinition are turning into a higher-level API, mostly meant for the LeRobot library (finding a model by name, servo facts like operating modes and homing sign, torque with the Feetech lock following it, setting up a motor). Could you say so explicitly, in the README and in the Bus doc? Something like "Bus / ServoDefinition give a higher-level, family-agnostic API, designed mostly for LeRobot; the per-servo controllers stay the low-level API". Then other users know which layer to pick, and it explains choices that are LeRobot conventions (e.g. the lock following the torque).

@pierre-rouanet
pierre-rouanet merged commit c2bcb64 into develop Oct 5, 2026
15 checks passed
@pierre-rouanet
pierre-rouanet deleted the feat/servo-facts-and-torque branch October 5, 2026 13:07
@nicolas-rabault

nicolas-rabault commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks. Both docs points (the higher-level API in the README and the Bus docs, and the factory_baudrate caveat) are in #142, commit cf861c5, since that PR adds the other half of that API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants