Skip to content

refactor(motors): one rustypot-backed SerialMotorsBus for Feetech and Dynamixel - #4672

Merged
s1lent4gnt merged 11 commits into
mainfrom
refactor/motors-rustypot-backend
Oct 16, 2026
Merged

s1lent4gnt merged 11 commits into
mainfrom
refactor/motors-rustypot-backend

Conversation

@nicolas-rabault

@nicolas-rabault nicolas-rabault commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

What

Replaces the vendor Feetech and Dynamixel Python SDKs with rustypot (>= 1.13.1), removes the control tables LeRobot carried for them, and merges the two family buses into one SerialMotorsBus.

feetech-servo-sdk and dynamixel-sdk are pure-Python implementations of the two serial protocols. rustypot implements both in Rust behind pyo3 bindings: framing, checksums and packet parsing run in compiled code, and the GIL is released for the whole transaction. It also states the facts LeRobot transcribed by hand from the vendor manuals (register addresses and sizes, byte order, sign encoding, resolution, baud-rate codes, model numbers), and since 1.11.0 the ones that told the two families apart: the homing offset sign, the operating mode values, whether the drive mode is a register, the Feetech EEPROM lock, and since 1.13.1 which status bits mean a refused access. So both tables.py and the per-family bus classes go.

How

Each motor's model (sts3215, xl330-m288, ...) is looked up with rustypot.find_model, which returns its exact model number and its servo definition. On connect, the bus opens a rustypot.Bus with each motor's definition, so models share a port (STS and SCS, XL430 and XL330), each read and written with its own layout, byte order and sign encoding.

What used to differ between FeetechMotorsBus and DynamixelMotorsBus now comes from the definition:

Feetech Dynamixel decided by
homing offset subtracted from the position added to it homing_offset_sign
position control operating mode 0 operating mode 3 set_operating_mode("position")
drive mode applied in software, from the calibration file Drive_Mode register whether the register exists
firmware check the motors of each servo definition on one version none whether Firmware_Major_Version exists
configure_motors accelerations Maximum_Acceleration (STS), Acceleration none whether the registers exist
torque the EEPROM lock follows it torque only rustypot's Bus.set_torque

What stays LeRobot's: normalisation, the calibration procedure, the recommended settings (configure_motors), the operating mode each robot uses.

Behaviour and API changes

  • Extras: feetech and dynamixel install rustypot instead of the vendor SDKs and no longer pull deepdiff. They now alias a new serial-motors extra (rustypot + pyserial, which lerobot-find-port needs); a missing rustypot points to it.
  • Deprecated, for one release: FeetechMotorsBus and DynamixelMotorsBus, now aliases of SerialMotorsBus that emit a DeprecationWarning; FeetechMotorsBus(protocol_version=...), accepted and ignored; their OperatingMode and TorqueMode enums. Use SerialMotorsBus (exported from lerobot.motors) and bus.set_operating_mode("position").
  • New: SerialMotorsBus.set_operating_mode(mode, motors=None), with position, velocity and pwm on both families, extended_position, current_based_position and current on Dynamixel, step on Feetech; DriveMode in lerobot.motors.
  • Removed: lerobot.motors.{feetech,dynamixel}.tables and their star re-exports, encoding_utils, the MotorsBus alias, the model_*_table attributes (bus.resolution(model) gives the encoder resolution), models, apply_drive_mode, set_timeout(), get_baudrate(), broadcast_ping(), scan_port() (nothing in LeRobot called it; rustypot's Bus.scan and Bus.broadcast_scan do the same), the SDK objects (port_handler, packet_handler, sync_reader, sync_writer) and the PortHandler, PacketHandler, GroupSyncRead and GroupSyncWrite protocols of motors_bus.py.
  • Model check: the handshake and setup_motor() check the exact model number of the configured model: an SM8512BL set as sts3215 is refused, although both share a control table.
  • Torque: disable_torque() and enable_torque() go through rustypot, which attempts every selected motor: failures are reported in a ConnectionError naming them, and a motor that does not answer may stay under torque. On a closed bus they raise DeviceNotConnectedError. The Feetech lock still follows the torque.
  • Timeout: the port waits 50 ms per status packet and per attempt, and each retry waits again. The SDKs recomputed their wait per packet (about 50 ms for Feetech, 34 ms for Dynamixel); a dead motor at disconnect() costs 0.3 s.
  • Checks and errors: set_operating_mode() and set_half_turn_homings() check every selected motor before writing any register. read(), write() and sync_read() raise a RuntimeError when a motor refuses the access, as rustypot's ServoDefinition.refuses tells it (an error number on Dynamixel), not on an alarm it reports while carrying it out: a Feetech status only reports alarms, and the alert bit of a Dynamixel is a latched hardware fault. Error messages name motors by motor ID. Register names are normalised whatever their case. set_baudrate() and ping() raise DeviceNotConnectedError on a closed bus.
  • Half-turn homing: the fix of fix(motors): keep Feetech half-turn homing offsets representable #4940 is carried over from main. A Feetech homing offset that does not fit its sign-magnitude register (2048, for a joint held at position 4095) is clamped to 2047, the bound coming from the register's sign bit in the definition. The Acceleration_Multiplier name fixed in fix(motors): remove trailing space from Feetech Acceleration_Multiplier name #4926 now comes from rustypot, and test_control_tables.py no longer checks the removed tables.
  • Finding and setting up motors: a broadcast ping where the servo answers one (Feetech STS, Dynamixel X), which covers a USB adapter's latency timer once; one Model Number read per ID elsewhere (Feetech SCS). setup_motor() tries the default baud rate, then the factory one, then the rest in a fixed order, has rustypot write the ID and the baud rate (torque off and lock open first), puts the port back at the default rate even when the search or the writes fail, and reads the model number back at the new ID and the default rate, raising if the motor does not answer there as the model it is configured as. ping() reads Model_Number instead of sending a ping instruction.
  • SCS values: Present_Velocity, Present_Load and Goal_Velocity of the SCS0009 are decoded with their sign bit (e.g. -232 instead of the raw 33 000), as they already were on STS servos. Resetting an SCS calibration no longer writes a homing offset it does not have.
  • Dynamixel is_calibrated no longer compares the ID stored in the calibration file, like Feetech.
  • disconnect() is safe on a closed bus and releases the port even when disabling torque fails.
  • Devices: every Feetech and Dynamixel robot and teleoperator builds a SerialMotorsBus and names its operating modes. HopeJR's hand drops protocol_version and reads its SCS servos with sync_read. OpenArm Mini asks the bus for the motor resolution. LeKiwi clamps wheel speeds to ±32767, the sign-magnitude range of Goal_Velocity.

Tests

The byte-level SDK mocks are replaced by an in-memory rustypot.Bus (MockBus) keyed by motor ID and register name. Register access and its error statuses, connection handling, torque, scanning and setup are tested once, on SerialMotorsBus with dummy definitions. The family files run the real rustypot definitions: Feetech (STS and SCS sharing a bus, the SCS without a homing offset, the acceleration registers, the firmware check, the drive mode applied in software to reads and commands, the STS3215 Phase quirk, an SM8512BL refused as an sts3215, the deprecated alias) and Dynamixel (two definitions sharing a bus, the homing offset sign, the operating mode values, the drive mode left to the register, the calibration mismatches, configure_motors, the deprecated alias).

tests/motors, tests/robots, tests/teleoperators and tests/test_control_robot.py: 321 passed at 2eea94e, 3 skipped (they need onnx, the Unitree SDK and Damiao hardware).

Hardware

Copilot AI balanced review requested due to automatic review settings September 18, 2026 13:23
@github-actions github-actions Bot added documentation Improvements or fixes to the project’s docs tests Problems with test coverage, failures, or improvements to testing robots Issues concerning robots HW interfaces labels Sep 18, 2026
@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Optional-dependency tests will fail isolated CI tiers, and protocol-1 scanning remains incorrectly blocked.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Replaces vendor motor SDKs with a shared rustypot-backed transport and introduces automatic serial motor-family selection.

Changes:

  • Adds MotorTransport/RustypotTransport and centralizes serial I/O.
  • Adds the SerialMotorsBus factory and migrates robot/teleoperator consumers.
  • Reworks dependencies and tests around an in-memory transport.
File summaries
File Description
uv.lock Replaces vendor SDK packages with rustypot.
pyproject.toml Defines rustypot motor dependencies.
docs/source/integrate_hardware.mdx Documents SerialMotorsBus.
src/lerobot/motors/__init__.py Exports the bus factory.
src/lerobot/motors/transport.py Adds the rustypot transport seam.
src/lerobot/motors/motors_bus.py Centralizes transport I/O and family resolution.
src/lerobot/motors/feetech/feetech.py Adapts Feetech behavior to raw transport bytes.
src/lerobot/motors/dynamixel/dynamixel.py Adapts Dynamixel behavior to rustypot.
src/lerobot/motors/dynamixel/tables.py Extracts the model-number address.
src/lerobot/utils/import_utils.py Removes vendor SDK availability flags.
src/lerobot/robots/so_follower/so_follower.py Uses automatic bus selection.
src/lerobot/robots/omx_follower/omx_follower.py Uses automatic bus selection.
src/lerobot/robots/lekiwi/lekiwi.py Uses automatic bus selection.
src/lerobot/robots/koch_follower/koch_follower.py Uses automatic bus selection.
src/lerobot/robots/hope_jr/hope_jr_arm.py Uses automatic bus selection.
src/lerobot/robots/hope_jr/hope_jr_hand.py Retains explicit protocol-specific construction.
src/lerobot/teleoperators/so_leader/so_leader.py Uses automatic bus selection.
src/lerobot/teleoperators/openarm_mini/openarm_mini.py Uses automatic bus selection.
src/lerobot/teleoperators/omx_leader/omx_leader.py Uses automatic bus selection.
src/lerobot/teleoperators/koch_leader/koch_leader.py Uses automatic bus selection.
tests/motors/test_motors_bus.py Tests shared I/O and factory behavior.
tests/motors/test_feetech.py Tests Feetech-specific encoding and calibration.
tests/motors/test_dynamixel.py Tests Dynamixel-specific behavior.
tests/mocks/mock_motors_bus.py Adds an in-memory transport fake.
tests/mocks/mock_feetech.py Removes packet-level Feetech mocks.
tests/mocks/mock_dynamixel.py Removes packet-level Dynamixel mocks.
tests/robots/test_so100_follower.py Patches the new bus factory.
Review details
  • Files reviewed: 26/27 changed files
  • Comments generated: 6
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/motors/test_dynamixel.py Resolved
Comment thread tests/motors/test_feetech.py Resolved
Comment thread tests/motors/test_motors_bus.py Resolved
Comment thread src/lerobot/motors/motors_bus.py Resolved
Comment thread src/lerobot/motors/motors_bus.py Resolved
Comment thread src/lerobot/motors/transport.py Resolved
@s1lent4gnt s1lent4gnt self-assigned this Sep 18, 2026
@nicolas-rabault
nicolas-rabault force-pushed the refactor/motors-rustypot-backend branch 2 times, most recently from e01585d to f7be6b1 Compare September 22, 2026 10:26
@nicolas-rabault
nicolas-rabault force-pushed the refactor/motors-rustypot-backend branch from f7be6b1 to b68a245 Compare September 29, 2026 21:24
@nicolas-rabault nicolas-rabault changed the title refactor(motors): replace the vendor SDKs with a rustypot transport refactor(motors): drive the Feetech and Dynamixel buses through rustypot Sep 29, 2026
@nicolas-rabault
nicolas-rabault force-pushed the refactor/motors-rustypot-backend branch from b68a245 to c513b74 Compare September 29, 2026 21:38
@ravediamond

Copy link
Copy Markdown
Collaborator

Community review

Tested this branch on a real SO-101 (follower + leader, Jetson Orin NX, aarch64, Python 3.12, rustypot 1.10.0) and compared against main with the same scripts and calibration.

Read-only, same arm and pose on both:

  • scan_port: ids 1-6, model 777 on both. 14.2 s on this PR, 16.1 s on main.
  • Raw Present_Position identical: 2030 / 852 / 3065 / 2844 / 2013 / 2066. Normalized identical too (e.g. shoulder_lift -102.99).
  • Min/Max_Position_Limit, Homing_Offset and Phase (12) identical.
  • sync_read x200: mean 0.92 ms on the PR, 1.00 ms on main (p95 0.99 vs 1.00), so no regression.

Through the real scripts, on the PR:

  • SOFollower.connect() + configure(): torque 1 on all six, position unchanged after 2 s, Torque_Enable 0 after disconnect().
  • lerobot-teleoperate, 20 s with the leader at 30 fps: loop stayed at 33.40 ms, no errors, clean exit, torque 0 afterwards.
  • lerobot-record, 2 episodes of 8 s with a reset in between: 480 frames, 29.94 Hz measured, 0/478 ticks over the 33.3 ms budget (work mean 2.1 ms, worst 2.4 ms). Torque 0 afterwards.

Everything above works on the SO-101 with this PR, no regression compared to main.
I only tested the SO-101 (Feetech), so I can't speak for the other motor families.

@nicolas-rabault
nicolas-rabault marked this pull request as draft October 5, 2026 08:27
@nicolas-rabault nicolas-rabault changed the title refactor(motors): drive the Feetech and Dynamixel buses through rustypot refactor(motors): one rustypot-backed SerialMotorsBus for Feetech and Dynamixel Oct 6, 2026
@nicolas-rabault

Copy link
Copy Markdown
Member Author

Updated, in four commits on top of the reviewed ones:

  • b0c67a0 rustypot 1.11.0, and a serial-motors extra that feetech and dynamixel now alias.
  • 498e3f2 SerialMotorsBus is now the one bus for both families. rustypot 1.11.0 states what told them apart (homing offset sign, operating mode values, drive mode register, Feetech lock), so FeetechMotorsBus and DynamixelMotorsBus become deprecated aliases for one release. The same commit carries the review fixes: exact model number check, a 50 ms timeout instead of 1 s, torque turned off on every motor even when one fails, motor search and setup through rustypot (broadcast ping, fixed baud rate order, ID and baud rate written by rustypot), and the port always released or restored.
  • 79860bc every Feetech and Dynamixel robot and teleoperator builds a SerialMotorsBus and names its operating modes (set_operating_mode("position")).
  • 4184107 docs.

The description lists every behaviour and API change. Each commit passes tests/motors, tests/robots, tests/teleoperators and tests/test_control_robot.py, and pre-commit. The SO-101 runs, including the community review above, predate the bus merge, and nothing Dynamixel has run on hardware yet, so tests on Koch or OMX would help.

@Sjeongheon

Copy link
Copy Markdown

Hi, I ran into an intermittent calibration write failure on an SO-101 / Feetech STS3215 and was looking through the related motor-bus work when I found this refactor.

On LeRobot v0.6.0, calibration would intermittently fail on persistent register writes with errors like:

Failed to write 'Min_Position_Limit' on id_=3 with '880' after 1 tries.
[TxRxResult] There is no status packet!

I saw this repeatedly enough that it prevented calibration from completing reliably.

What was interesting was that after one of these failures, I immediately read the calibration registers back and found that the write reported as failed had actually been applied.

At that point the range-limit state was roughly:

ID1: Min / Max applied
ID2: Min / Max applied
ID3: Min = 880 applied
     Max still unchanged
ID4-ID6: range limits not updated yet

(Homing_Offset is not useful for showing the progress of this particular write_calibration() step because those offsets had already been written earlier in the calibration flow.)

So at least on the STS3215 I tested, this sequence appears possible:

write reaches the servo
-> persistent register is updated
-> status reply is lost
-> host reports the write as failed
-> write_calibration() aborts

This leaves a partially applied calibration even though the failed write itself may already be stored on the servo.

As a local workaround, I changed the calibration write path so that on a communication failure it reads the same register back before deciding that the write failed:

try:
    write(register, motor, value)
except ConnectionError:
    readback = read(register, motor)

    if readback == value:
        # The write was applied; only the reply appears to have been lost.
        continue

    raise

With that verification in place, I was able to complete calibration despite the intermittent missing status replies.

I also tried writing the same register/value repeatedly, running Homing_Offset -> Min_Position_Limit -> Max_Position_Limit repeatedly, and running the sequence across all six motors. Those later tests completed successfully, so I could not tie the failure to a specific register, value, or simply to consecutive EEPROM writes.

I have also tested the current #4672 branch on the same STS3215 hardware, but I have not been able to reproduce the failure again so far. Since the original issue was intermittent, I don't know whether the rustypot backend changes its occurrence rate.

However, looking at the current write path, my understanding is that a communication failure is retried by issuing the write operation again. Unless I am missing another verification mechanism, the semantic ambiguity still seems possible: after a lost status reply, the caller does not know whether the write failed or whether it succeeded and only the reply was lost.

I also noticed that #4695 deliberately kept torque-register readback / write semantic changes out of scope, so I am not assuming this necessarily belongs in this PR.

Would you prefer this kind of verification to be handled here in the new serial write path, in write_calibration() as a follow-up, or possibly lower in rustypot?

For persistent readable configuration registers, readback-after-communication-failure worked well as a practical workaround for me.

I have only observed the write-applied / reply-lost behavior on Feetech STS3215 hardware; I have not tested the same failure mode on Dynamixel.

@nicolas-rabault

Copy link
Copy Markdown
Member Author

Ran the bus merge on hardware: two more SO-101 arms (six STS3215 each, firmware 3.10, 1 Mbps), driven through SerialMotorsBus at 8d35116.

  • Handshake (exact model numbers, one firmware) passes on both; read_calibration returns the same homing offsets and limits as reading the registers through rustypot directly.
  • sync_read of the six positions: p50 1.00 ms, p99 1.09 and 1.22 ms over 300 reads.
  • disable_torque on the six motors: 4.7 to 5.5 ms.
  • setup_motor("gripper", initial_baudrate=1_000_000, initial_id=6), which rewrites the id and rate it already has, then reads the model number back: passes in 3 to 4 ms.
  • setup_motor for a model that is not on the bus (an XL330): the search walks every XL330 baud rate, fails after 10 s, and the port is back at 1 Mbps (the next read answers).

Not covered yet: calibration and teleoperation on these arms, and anything Dynamixel.

varun-projects added a commit to varun-projects/lerobot that referenced this pull request Oct 10, 2026
Read/write asymmetry diagram, the self-perpetuating is_calibrated loop, EEPROM
write-safety reasoning, the tests/motors silent-skip trap, why it is distinct
from PR huggingface#4672, and the PR description.
varun-projects added a commit to varun-projects/lerobot that referenced this pull request Oct 10, 2026
Read/write asymmetry diagram, the self-perpetuating is_calibrated loop, EEPROM
write-safety reasoning, the tests/motors silent-skip trap, why it is distinct
from PR huggingface#4672, and the PR description.
The Feetech and Dynamixel buses reached the wire through the vendors'
pure-Python SDKs, behind a layer shaped like them (port and packet
handlers, group sync readers and writers), and carried their own control
tables: addresses, sizes, sign bits, resolutions, baud rate codes and
model numbers transcribed from the vendor manuals, plus the code applying
them -- byte order, sign encoding, the ID sweep and its timeout.

rustypot implements both protocols in Rust, releases the GIL for the
whole transaction, and states and applies those facts itself. Each family
now names its servo definitions in `_servos()`, the rustypot controller
classes. The base bus resolves a model to one of them (rustypot spells
XL330M288 where LeRobot writes xl330-m288) and a register name by
lowering it, and on connect opens a `rustypot.Bus` with each motor's
definition. Models of one family therefore share a port -- STS and SCS,
XL430 and XL330 -- each read and written in its own byte order and sign
encoding. Retries, sync reads on servos without Sync Read, and the bus
scan are rustypot's as well.

Feetech's `protocol_version` goes: STS and SCS differ in byte order, in
whether they answer Sync Read and in which registers they have, and the
definition knows all three. The handshake accepts any model number a
definition covers, `scan_port()` sweeps the bus itself in place of
`broadcast_ping()`, and `setup_motor()` reaches a new motor at whatever
id it answers through the motor's own definition. `disconnect()` is safe
on a closed bus and releases the port even when disabling torque fails.

Gone: both tables.py, encoding_utils.py, the SDK-shaped protocols, the
`MotorsBus` alias, and deepdiff from the motor extras. pyserial stays in
them for `lerobot-find-port`. rustypot 1.10.0 is the first release that
carries the servo metadata and the multi-definition `Bus`.

The devices that relied on the old surface follow: HopeJR's hand drops
`protocol_version` and sync-reads its SCS servos, OpenArm Mini asks the
bus for the motor resolution, and LeKiwi clamps wheel speeds to the
sign-magnitude range of Goal_Velocity.

Tests replace the byte-level SDK mocks with an in-memory `rustypot.Bus`
keyed by motor id and register name. The IO helpers shared by both
families are tested once, on the base bus.
The install steps stop sending users to the vendor SDKs, the hardware
guide lists the servos rustypot defines and points new models at rustypot
rather than at control tables that no longer exist, and the API page
documents SerialMotorsBus and both family buses.
rustypot 1.11.0 finds a servo model by name with its exact model number,
states what differs between families (homing offset sign, operating mode
values, factory baud rate, whether the EEPROM has a lock), turns the
torque of a whole bus on or off, scans with a broadcast ping, and sets a
new motor's id and baud rate. LeRobot's bus is about to rely on all of it.

One bus class will drive both families, so there is one extra to point
users at: serial-motors installs rustypot and pyserial. feetech and
dynamixel stay, as the names the setup guides use, and now alias it.
FeetechMotorsBus and DynamixelMotorsBus had become a list of rustypot
classes plus a handful of facts about each family: which way the homing
offset goes, which operating mode value means position control, whether
the drive mode is a register, which acceleration and firmware registers
exist, and the Feetech lock. rustypot 1.11.0 states those facts in its
servo definitions, so SerialMotorsBus is now the one, concrete bus:

- each motor's model is looked up with rustypot.find_model, which gives
  its exact model number and its definition. The handshake and the motor
  search check that exact number again, so an SM8512BL set up as an
  sts3215 is refused, though both share a control table;
- calibration reads and writes the homing offset where the register
  exists (not on the SCS), compares the drive mode where it is a register
  (Dynamixel) and applies it in software elsewhere (Feetech), and centres
  a joint with the homing offset sign of the definition;
- the firmware check runs on the motors that report a firmware major and
  minor version (Feetech); configure_motors writes the acceleration
  registers a motor has; set_operating_mode("position") writes each
  motor's own value for a mode named the same on every family;
- torque goes on or off through rustypot's Bus.set_torque, which tries
  every motor, moves the Feetech lock with the torque, and reports the
  motors that failed: one that does not answer no longer leaves the
  motors after it under torque, and the error names it;
- setup_motor has rustypot change the id and the baud rate (torque off
  and lock open first), with no reopening of the bus, and puts the port
  back at the default rate even when that fails. The motor search tries
  the default baud rate, then the factory one, then the rest in a fixed
  order, with a broadcast ping where the servo answers one, which covers
  a USB adapter's latency once. scan_port takes the model it looks for,
  releases the port when a scan fails, and reports ids only;
- a status packet is waited for 50 ms, not a second, so one lost reply
  costs a 30 Hz loop a tick (the SDKs waited about 35 to 50 ms).

FeetechMotorsBus and DynamixelMotorsBus stay for one release as
deprecated aliases (FeetechMotorsBus still accepts protocol_version), and
so do their OperatingMode and TorqueMode enums. A missing rustypot points
to the serial-motors extra. Resetting the calibration of an SCS no longer
writes a homing offset it does not have. On Dynamixel, is_calibrated no
longer compares the id stored in the calibration file, like Feetech.
The SO, Koch, OMX, LeKiwi, HopeJR and OpenArm Mini devices built a
FeetechMotorsBus or a DynamixelMotorsBus and wrote operating modes as the
value of a family's enum. They now build a SerialMotorsBus and name the
mode: set_operating_mode("position"), "extended_position", "velocity", or
"current_based_position" for the Dynamixel grippers. DriveMode comes from
lerobot.motors.
The hardware guide builds its example robot on SerialMotorsBus, names
operating modes, and says what the definitions settle between families.
The API page documents SerialMotorsBus and DriveMode, and mentions the
two family classes as deprecated aliases.
…port

With the torque and the motor setup handled by rustypot, _write,
_sync_read and _sync_write each had a single caller: write, sync_read and
sync_write now talk to the rustypot bus themselves. _read stays, shared by
read and ping, and always raises; its raise_on_error and err_msg
parameters, which only ping used, go, and ping catches the error instead
of asking not to get one. The Feetech firmware check reads the major and
minor versions with one sync read each rather than two reads per motor.

scan_port goes: nothing in LeRobot calls it, and rustypot's Bus.scan and
Bus.broadcast_scan do the same for anyone who needs it.

The tests that went through the private helpers now go through read,
write, sync_read and sync_write against the in-memory bus.
…t-up motor

setup_motor started its cleanup only after finding the motor. The search
walks the baud rates, so a motor that was not there left the port on the
last rate tried, and anything done next on the same bus went unanswered.
The search now runs inside the try whose finally puts the port back at the
default rate.

A motor answers the id and baud rate writes before they take effect, and
nothing checked that they did. setup_motor now reads the model number back
at the new id and the default baud rate, and raises if the motor does not
answer there with the model it is configured as.
… errors, document the bus

- set_operating_mode() and set_half_turn_homings() check every selected motor before
  writing any register, so an unsupported motor leaves no partial configuration.
- sync_read() raises on a motor's error status, as read() does.
- Register names normalise whatever their case; set_baudrate() and ping() need a
  connected bus; resolution() raises for a model that counts no steps; the firmware
  check compares the motors of each servo definition on their own.
- Each model is looked up once, and the bus opens rustypot's Bus through _open_bus()
  and reaches it, typed, through _connected_bus.
- Errors name motors by motor ID; docstrings follow the documentation format and its
  cross-references. The hardware tutorial and the motor API page gain the serial-motors
  extra, the SCS limits, a migration note and the DriveMode members.
- Tests cover write and sync-read error statuses, sync writes no motor answers,
  calibrated commands and calibration mismatches of each family, through bus helpers
  shared by the family suites.
A status byte also carries conditions a motor reports while it carries the access out:
a Feetech status only reports alarms (input voltage, temperature, current, load), and on
protocol 2 bit 7 is the alert of a latched hardware fault. Raising on them stopped a
control loop on a gripper squeezing an object, and made ping() miss an overloaded motor.

read(), write(), sync_read() and ping() now fail only when the status says the motor
refused the access: an error number on protocol 2. The protocol-1 servos LeRobot drives
are Feetech ones, which never refuse through their status.
…cess

rustypot 1.13.1's ServoDefinition.refuses tells a refusal from an alarm for every
servo it defines, so the bus no longer keeps its own copy of the rule, which assumed
every protocol-1 servo is a Feetech one. The serial-motors extra now needs rustypot
1.13.1, and the tutorial links rustypot's servo guide at that version.
@nicolas-rabault
nicolas-rabault force-pushed the refactor/motors-rustypot-backend branch from ecc1b54 to 2eea94e Compare October 16, 2026 09:01
@s1lent4gnt
s1lent4gnt marked this pull request as ready for review October 16, 2026 10:50

@s1lent4gnt s1lent4gnt 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 @nicolas-rabault, this is an exciting step for LeRobot. Consolidating Feetech and Dynamixel under SerialMotorsBus eliminates duplicate SDK code and control tables by offloading that logic to rustypot. It sets a much cleaner foundation for our current motor support and future arm integrations. Excited to see this land on main, approved, great work!

@s1lent4gnt
s1lent4gnt merged commit 146d847 into main Oct 16, 2026
24 checks passed
@s1lent4gnt
s1lent4gnt deleted the refactor/motors-rustypot-backend branch October 16, 2026 11:56
zeta0707 added a commit to zeta0707/lerobot_zml that referenced this pull request Oct 29, 2026
…ypot refactor

Upstream (huggingface#4672) replaced the SDK-based SerialMotorsBus with a
rustypot-backed one and dropped motors/encoding_utils.py. rustypot does not speak
the LewanSoul protocol, so keep the previous bus base inside the lewansol package
for the LX-16A driver used by Rebearm.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or fixes to the project’s docs robots Issues concerning robots HW interfaces tests Problems with test coverage, failures, or improvements to testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants