Repository navigation
Proposal: find motors with a broadcast ping, and set their id and baud rate - #142
Conversation
pierre-rouanet
left a comment
There was a problem hiding this comment.
Thanks for splitting this into separate commits with the for/against for each one. That made it easy to review commit by commit.
1. broadcast_ping (fe7cd78): approved. One improvement doesn't need to block this PR, so I opened #144 for it: the listening loop stops at the first bad header or mid-packet timeout, which loses every motor that hadn't answered yet. The SDK resyncs on the next header instead.
2. scan with a broadcast ping (c538508): partly accepted.
- The
supports_broadcast_pingentry in the definition (macro,ServoInfo, Python static method) is fine, please keep it. - I'd rather not change what the existing
scandoes. Right nowscan(&[1])on an STS3215 or an XL takes about 5 ms. With this change it always takes the full window (about 0.8 s at 1 Mbps), whateveridsasks for. Anyone who usesscanto check whether one motor is there gets that slowdown with no change at the call site. - Please put the ping-then-read path in a separate method instead (
smart_scan,broadcast_scan, whatever name you prefer), onServoDefinition, the controllers andBus/Python, and leavescanas the plain sweep. Callers like LeRobot can then pick it on purpose. Your question about whether v2 should imply the flag fits that new method well.
3. change_id / change_baudrate (9e953da): fine with it. This is new API that changes nothing existing, so OK. One request: say in the change_baudrate docstrings (ServoDefinition, Bus and the Python Bus) that the bus's own port stays at the old rate, so the caller has to call set_baudrate to keep talking to the motor afterwards.
4. baudrates() / models() in a fixed order (b0a1444): nice, approved.
So the only change that blocks merging is moving the new scan behaviour into its own method, plus the docstring.
Finding the motors on a port meant reading the model number of every id in turn, under a short timeout so that 254 absent ids do not take minutes. That timeout has to be short (5 ms) for the sweep to stay usable, and a USB adapter with a 16 ms latency timer, such as the FTDI chip of a U2D2, can hold a healthy motor's answer longer than that. A broadcast ping reaches every motor with one instruction; each one that hears it answers in turn. The handler listens for one window, sized like the vendor SDKs size it: the status packets of every possible id, 3 ms of turn per id and 16 ms of adapter latency, about 0.8 s at 1 Mbps. A status packet that does not parse is skipped; the port's timeout is put back. Checked on two SO-101 arms (six STS3215 each): the six ids at 1 Mbps, none at 500 kbps or 115.2 kbps, in 790 ms.
scan reads the model number of every id under a 5 ms timeout. A USB adapter that holds short answers for its 16 ms latency timer, such as the FTDI chip of a U2D2, makes a motor that is there read as absent, and a full sweep costs 254 timeouts. broadcast_scan pings once, then reads the model number of the ids asked that answered, under the port's own timeout, so the adapter's latency is covered once for the whole bus. The ping listens for its whole window (0.8 s at 1 Mbps) however few ids are asked, so scan is left as it is: still the fast way to check whether a few motors are there, while a caller sweeping the whole bus picks broadcast_scan. A definition states whether its firmware answers a broadcast ping: the protocol v2 Dynamixel (XL320, XL330, XL430) and the Feetech STS3215 do, the protocol v1 Dynamixel and the Feetech SCS do not, and broadcast_scan refuses those before sending anything. On ServoDefinition, the controllers and Bus, and on Python. On two SO-101 arms, the same path (ping, then reads) found the six motors (777) in 0.8 s, where the sweep takes 1.3 s.
b0a1444 to
dbfeffa
Compare
Setting up a new motor means giving it its id and the bus's baud rate. Both live in EEPROM: a Dynamixel only writes it with the torque off, a Feetech also needs its lock register open, and the baud rate register takes a code, not a rate. Every caller has to know all three, and LeRobot does it by hand for each family. change_id and change_baudrate on ServoDefinition and Bus do it: torque off, lock open where the servo has one (writing 0 is harmless on the AX and MX, whose lock only clears at power-up), then the write. The baud rate is looked up in the definition's baudrates, and a rate it does not list fails as a RegisterError before anything is sent; the port is left at its rate, for the caller to switch. Like scan, they take the definition and reach motors the bus does not have, since a new motor answers at its factory id. Checked on an SO-101 leader: its gripper motor taken from id 6 to 7 and back, then to 500 kbps and back to 1 Mbps, each step confirmed by a scan.
Both static methods built their dict from a HashMap, so its order changed from one process to the next. A caller trying each baud rate in turn to find a motor (LeRobot's setup does) tried them in a different order on every run, and took a different time to find the same motor. They now come from a BTreeMap: baud rates slowest first, models by name. The return type is still a dict.
Between the by-name register access, the servo facts, finding and setting up motors and the torque of a whole bus, Bus and ServoDefinition have become an API of their own on top of the controllers, designed mostly for LeRobot. Say so in the README, the crate docs and the Bus docs, so other users know which layer to pick and why some of its choices (the Feetech lock following the torque) are LeRobot conventions rather than vendor rules. The README and crate docs now list three APIs, from the wire up, and the README's mixed-bus section shows what the higher layer carries. Also say that factory_baudrate holds for the units made today: some older motors left the factory at another rate, around the move from protocol v1 to v2.
dbfeffa to
cf861c5
Compare
|
Thanks for the review. I rebased on develop now that #143 is in, and pushed:
The description is updated with the new hashes. |
pierre-rouanet
left a comment
There was a problem hiding this comment.
LGTM, thanks @nicolas-rabault
|
Ran this branch on hardware after the split: two arms with six STS3215 each, read only. On each arm, 20 runs of |
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.
Updated after review: rebased on develop (with #143),
scanleft as it was and the broadcast path moved to its ownbroadcast_scan, thechange_baudratedocstrings say the port stays at the old rate, and a docs commit for the two points raised on #143. The resync of the broadcast ping is #144, in a follow-up PR.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. Going through it, most of what is left in those classes is not LeRobot logic: it is knowledge about the servos that LeRobot transcribed by hand from the manuals. The review also found two real problems in how LeRobot now finds and sets up motors:scanreads the model number of every id under a 5 ms timeout. A U2D2 (FTDI chip) holds a short answer up to 16 ms (its latency timer) before passing it on, so a motor that is there can read as absent. Lengthening the per-id timeout to cover that makes a sweep at one baud rate take about 5 s.baudrates()comes from aHashMap, so LeRobot tries the baud rates in a different order on every run.This PR covers finding and setting up motors. #143 (merged) covered the servo facts and the torque.
What it proposes
1.
DynamixelProtocolHandler::broadcast_ping(4f35736)One ping to the broadcast id; every motor that hears it answers in turn, and the handler listens for one window sized like the vendor SDKs size it: the status packets of every possible id, 3 ms of turn per id and 16 ms of adapter latency (about 0.8 s at 1 Mbps). A status packet with a bad checksum is skipped, and the port's timeout is put back. A framing error still ends the listening; #144 tracks resyncing on the next header instead.
broadcastPing), and how LeRobot found its STS motors before moving to rustypot.2.
broadcast_scan, next to an unchangedscan(09765a9)broadcast_scan(ids)/broadcast_scan_all()onServoDefinition, the controllers andBus(broadcast_scan(ids=None)on Python): one broadcast ping, then the model number of each id asked that answered, under the port's own timeout.scanis untouched, so checking whether one motor is there still takes 5 ms; a caller sweeping the whole bus picksbroadcast_scanon purpose.A definition says
supports_broadcast_ping: true(defaultfalse): the STS3215 and the protocol v2 Dynamixel (XL320, XL330, XL430). On any other servobroadcast_scanis a newRegisterError::BroadcastPing(ValueErroron Python) and nothing is sent, rather than falling back to the sweep behind the caller's back. The flag is also onServoInfo, the controller classes (supports_broadcast_ping()) and the PythonServoDefinition, so LeRobot writesbus.broadcast_scan(d) if d.supports_broadcast_ping else bus.scan(d).scanchanges speed.3.
change_idandchange_baudrate(ba79256)On
ServoDefinitionandBus(and the PythonBus): torque off, lock open where the servo has alockregister, 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 a newRegisterError::Baudrate(ValueErroron Python) and nothing is sent. The port stays at its rate, and the docstrings now tell the caller to callset_baudrateto keep talking to the motor. Likescan, they take the definition and reach motors the bus does not have, since a new motor answers at its factory id.4.
baudrates()andmodels()in a fixed order (ab9abcf)The two Python static methods of the controller classes now build their dict from a
BTreeMap(baud rates slowest first, models by name), like theServoDefinitiongetters from #143. Still a dict.5. Docs:
BusandServoDefinitionas the higher-level API (cf861c5)As suggested on #143. The README and the crate docs listed two APIs, the protocol handler as the low-level one and the controllers as the high-level one; they now list three, from the wire up, with
Bus/ServoDefinitionas a higher-level, family-agnostic API designed mostly for LeRobot, whose choices can be LeRobot conventions (the Feetech lock following the torque). The controllers are described as the per-servo API rather than "low-level", so the new text does not contradict the existing one; tell me if you prefer other words. TheBusdocs (Rust and Python) say the same, the README's mixed-bus section shows what the higher layer carries, andfactory_baudratesays it holds for the units made today.Not included, on purpose
scan; tell me if you want it raised or made a parameter.Tests
broadcast_scan(one ping then one read, one instruction for a whole v2 bus, an SCS0009 refused with nothing sent),change_id(torque, lock and id writes, at an id the bus does not have),change_baudrate(the register value, and a refused rate sends nothing). Thescantests are the ones on develop, unchanged.broadcast_scan, returns{1..6: 777}in 0.8 s (1.3 s with the sweep); on a leader arm, motor 6 taken to id 7 and back, then to 500 kbps and back to 1 Mbps, each step confirmed by a scan. After the split, on the same two arms (read only): 20 runs ofbroadcast_scan_allper arm all return exactly what thescan_allsweep returns ({1..6: 777}), in 0.8 to 0.9 s.