From adccdea1cfbec83ed98263feb7e59f7d047c5692 Mon Sep 17 00:00:00 2001 From: Xingdong Zuo Date: Sun, 2 Aug 2026 05:50:31 +0900 Subject: [PATCH] fix(robots): retry LeKiwi bus reads on transient Feetech errors (#4283) Same fix as #4207 (SO follower/leader), applied to LeKiwi, which reads its motors through the same Feetech bus and had the same gap: `sync_read` was called without `num_retry`, so a single corrupted status packet raised ConnectionError and took the control loop down. Adds `num_read_retries` (default 2, matching #4207) to LeKiwiConfig and forwards it at all three read sites. Two of them are Present_Position, as in #4207; the third is the Present_Velocity read of the omniwheel base, which is LeKiwi-specific and is where this was observed in the field: File "lerobot/robots/lekiwi/lekiwi.py", line 351, in get_observation base_wheel_vel = self.bus.sync_read("Present_Velocity", self.base_motors) ConnectionError: Failed to sync read 'Present_Velocity' on ids=[7, 8, 9] after 1 tries. [TxRxResult] There is no status packet! All nine servos pinged 20/20 with the bus idle immediately afterwards, so this is transient corruption under load rather than a wiring fault. Relates to #4207. Refs #3131. --- src/lerobot/robots/lekiwi/config_lekiwi.py | 6 ++++++ src/lerobot/robots/lekiwi/lekiwi.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/lerobot/robots/lekiwi/config_lekiwi.py b/src/lerobot/robots/lekiwi/config_lekiwi.py index c75bf8d2d..79f7bd647 100644 --- a/src/lerobot/robots/lekiwi/config_lekiwi.py +++ b/src/lerobot/robots/lekiwi/config_lekiwi.py @@ -58,6 +58,12 @@ class LeKiwiConfig(RobotConfig): # Set to `True` for backward compatibility with previous policies/dataset use_degrees: bool = True + # Number of extra attempts when a `sync_read` of the motors fails. Feetech buses can occasionally + # return a corrupted status packet ("Incorrect status packet!"), especially when several joints move + # at once, which otherwise aborts the control loop. Retries are immediate (no sleep) and only happen on + # failure, so the steady-state read cost is unchanged. + num_read_retries: int = 2 + @dataclass class LeKiwiHostConfig: diff --git a/src/lerobot/robots/lekiwi/lekiwi.py b/src/lerobot/robots/lekiwi/lekiwi.py index 7d7f984ab..17f096260 100644 --- a/src/lerobot/robots/lekiwi/lekiwi.py +++ b/src/lerobot/robots/lekiwi/lekiwi.py @@ -347,8 +347,12 @@ class LeKiwi(Robot): def get_observation(self) -> RobotObservation: # Read actuators position for arm and vel for base start = time.perf_counter() - arm_pos = self.bus.sync_read("Present_Position", self.arm_motors) - base_wheel_vel = self.bus.sync_read("Present_Velocity", self.base_motors) + arm_pos = self.bus.sync_read( + "Present_Position", self.arm_motors, num_retry=self.config.num_read_retries + ) + base_wheel_vel = self.bus.sync_read( + "Present_Velocity", self.base_motors, num_retry=self.config.num_read_retries + ) base_vel = self._wheel_raw_to_body( base_wheel_vel["base_left_wheel"], @@ -397,7 +401,9 @@ class LeKiwi(Robot): # Cap goal position when too far away from present position. # /!\ Slower fps expected due to reading from the follower. if self.config.max_relative_target is not None: - present_pos = self.bus.sync_read("Present_Position", self.arm_motors) + present_pos = self.bus.sync_read( + "Present_Position", self.arm_motors, num_retry=self.config.num_read_retries + ) goal_present_pos = {key: (g_pos, present_pos[key]) for key, g_pos in arm_goal_pos.items()} arm_safe_goal_pos = ensure_safe_goal_position(goal_present_pos, self.config.max_relative_target) arm_goal_pos = arm_safe_goal_pos