Skip to content

Commit ac09692

Browse files
nu-jliuclaude
andauthored
Fix: Default disable auto recovery on motor loss comm (#62)
* feat(dm_driver): make motor auto-recovery opt-in, fail-fast by default - i2rt/motor_drivers/dm_driver.py: added enable_auto_recovery constructor flag (default False, read live each control-loop iteration); gated both control-loop recovery paths (the RuntimeError "Motor error detected" catch and the feedback error_code check) behind it, so a loss-comm error (0xD) now sets running=False and re-raises instead of silently clearing and re-enabling the motor - i2rt/robots/get_robot.py: threaded enable_auto_recovery through get_yam_robot and _get_gripper_only_robot into the DMChainCanInterface constructor (default False) - i2rt/robots/motor_chain_robot.py: added enable_auto_recovery override on MotorChainRobot (None = inherit the chain's setting) and exposed the effective value in the robot info dict Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(flow_base): explicitly disable motor auto-recovery on the mobile base - i2rt/flow_base/flow_base_controller.py: pass enable_auto_recovery=False at the holonomic_base and unified_motor_chain DMChainCanInterface constructions so the base's fail-fast intent is explicit at the call sites (ROB-1449); functional no-op since the constructor default is already False. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0f201d7 commit ac09692

4 files changed

Lines changed: 33 additions & 15 deletions

File tree

i2rt/flow_base/flow_base_controller.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def _initialize_motor_chain(
160160
channel=channel,
161161
motor_chain_name="holonomic_base",
162162
control_mode=ControlMode.VEL,
163+
enable_auto_recovery=False, # fail-fast on motor error (ROB-1449); base does not self-heal
163164
)
164165
return motor_interface
165166

@@ -676,6 +677,7 @@ def __init__(
676677
motor_chain_name="linear_rail_vehicle" if enable_linear_rail else "holonomic_base",
677678
control_mode=ControlMode.VEL,
678679
control_freq=control_freq,
680+
enable_auto_recovery=False, # fail-fast on motor error (ROB-1449); base does not self-heal
679681
)
680682

681683
# Initialize brake GPIO only if linear rail is enabled

i2rt/motor_drivers/dm_driver.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ def __init__(
384384
use_buffered_reader: bool = False, # buffered reader is not very stable, the latest encoder fix allows us to use the non-buffered reader
385385
report_interval: float = REPORT_INTERVAL,
386386
control_freq: float = CONTROL_FREQ, # Control loop frequency (Hz), used for the CAN bandwidth check
387+
enable_auto_recovery: bool = False, # if True, try to clean+re-enable errored motors in the control loop instead of failing fast
387388
):
388389
assert not use_buffered_reader, (
389390
"buffered reader is not very stable, the latest encoder fix allows us to use the non-buffered reader"
@@ -401,6 +402,9 @@ def __init__(
401402
self.motor_offset = np.array(motor_offset, dtype=float)
402403
self.motor_direction = np.array(motor_direction)
403404
self.channel = channel
405+
# Read live each control-loop iteration; must be set before _motor_on()/start_thread() since
406+
# some callers (e.g. _get_gripper_only_robot) start the thread inside this constructor.
407+
self.enable_auto_recovery = enable_auto_recovery
404408
logging.info(f"Channel: {channel}, Bitrate: {bitrate}")
405409
if "can" in channel:
406410
self.motor_interface = DMSingleMotorCanInterface(
@@ -578,29 +582,25 @@ def _set_torques_and_update_state(self) -> None:
578582
try:
579583
motor_feedback = self._set_commands(self.commands)
580584
except RuntimeError as e:
581-
if "Motor error detected" in str(e):
585+
if self.enable_auto_recovery and "Motor error detected" in str(e):
582586
logging.warning(f"Motor error in control loop, attempting recovery: {e}")
583-
recovered = self._try_recover_motors()
584-
if recovered:
587+
if self._try_recover_motors():
585588
logging.warning("Motor recovery successful, continuing control loop")
586589
continue
587-
else:
588-
self.running = False
589-
raise
590+
self.running = False
591+
raise
590592
raise
591593

592594
errors = np.array([motor_feedback[i].error_code != "0x1" for i in range(len(motor_feedback))])
593595
if np.any(errors):
594-
logging.warning(f"Motor errors detected in feedback: {errors}")
595-
recovered = self._try_recover_motors(motor_feedback)
596-
if recovered:
597-
logging.warning("Motor recovery successful, continuing control loop")
598-
continue
596+
if self.enable_auto_recovery:
597+
logging.warning(f"Motor errors detected in feedback: {errors}, attempting recovery")
598+
if self._try_recover_motors(motor_feedback):
599+
logging.warning("Motor recovery successful, continuing control loop")
600+
continue
599601
self.running = False
600602
logging.error(f"motor errors: {errors}")
601-
raise Exception(
602-
"motors have unrecoverable errors after recovery attempts, stopping control loop"
603-
)
603+
raise Exception(f"motor errors detected: {errors}, stopping control loop")
604604

605605
with self.state_lock:
606606
self.state = motor_feedback

i2rt/robots/get_robot.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,16 @@ def _get_gripper_only_robot(
6262
channel: str = "can0",
6363
gripper_type: GripperType = GripperType.LINEAR_4310,
6464
sim: bool = False,
65+
enable_auto_recovery: bool = False,
6566
) -> "Robot":
6667
"""Create a gripper-only robot (no arm).
6768
6869
Args:
6970
channel: CAN interface name (e.g. "can0"). Ignored in sim mode.
7071
gripper_type: Which gripper to load. Must not be NO_GRIPPER.
7172
sim: If True, return a SimRobot instead of connecting to real hardware.
73+
enable_auto_recovery: If True, the motor chain tries to clean+re-enable errored motors in its
74+
control loop instead of failing fast. Defaults to False (fail-fast).
7275
"""
7376
if gripper_type == GripperType.NO_GRIPPER:
7477
raise ValueError("gripper_type cannot be NO_GRIPPER when arm_type is NO_ARM")
@@ -108,6 +111,7 @@ def _get_gripper_only_robot(
108111
motor_chain_name="gripper_only",
109112
receive_mode=ReceiveMode.p16,
110113
start_thread=True,
114+
enable_auto_recovery=enable_auto_recovery,
111115
)
112116

113117
return MotorChainRobot(
@@ -140,6 +144,7 @@ def get_yam_robot(
140144
sim: bool = False,
141145
joint_state_saver_factory: Optional[Callable[[], Any]] = None,
142146
set_realtime_and_pin_callback: Optional[Callable[[int], None]] = None,
147+
enable_auto_recovery: bool = False,
143148
) -> "Robot":
144149
"""Create a YAM-family robot (real or sim).
145150
@@ -156,10 +161,14 @@ def get_yam_robot(
156161
gripper_kp: Optional gripper kp override. Defaults to gripper_type's default.
157162
gripper_kd: Optional gripper kd override. Defaults to gripper_type's default.
158163
sim: If True, return a SimRobot instead of connecting to real hardware.
164+
enable_auto_recovery: If True, the motor chain tries to clean+re-enable errored motors in its
165+
control loop instead of failing fast. Defaults to False (fail-fast).
159166
"""
160167
# --- Gripper-only path (no arm) -------------------------------------------
161168
if arm_type == ArmType.NO_ARM:
162-
return _get_gripper_only_robot(channel=channel, gripper_type=gripper_type, sim=sim)
169+
return _get_gripper_only_robot(
170+
channel=channel, gripper_type=gripper_type, sim=sim, enable_auto_recovery=enable_auto_recovery
171+
)
163172

164173
with_gripper = gripper_type not in (GripperType.YAM_TEACHING_HANDLE, GripperType.NO_GRIPPER)
165174
with_teaching_handle = gripper_type == GripperType.YAM_TEACHING_HANDLE
@@ -245,6 +254,7 @@ def get_yam_robot(
245254
start_thread=False,
246255
get_same_bus_device_driver=get_encoder_chain if with_teaching_handle else None,
247256
use_buffered_reader=False,
257+
enable_auto_recovery=enable_auto_recovery,
248258
)
249259
motor_states = motor_chain.read_states()
250260
logging.debug(f"motor_states: {motor_states}")

i2rt/robots/motor_chain_robot.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def __init__(
9393
pinned_cpu: int | None = None,
9494
joint_state_saver_factory: Optional[Callable[[], Any]] = None,
9595
set_realtime_and_pin_callback: Optional[Callable[[int], None]] = None,
96+
enable_auto_recovery: Optional[bool] = None, # None: inherit motor_chain's setting; True/False: override it
9697
) -> None:
9798
# Set up CPU pinning and real-time scheduling if requested
9899
if pinned_cpu is not None and set_realtime_and_pin_callback is not None:
@@ -138,6 +139,10 @@ def __init__(
138139
assert clip_motor_torque >= 0.0
139140
self._clip_motor_torque = clip_motor_torque
140141
self.motor_chain = motor_chain
142+
# None means inherit whatever the chain was constructed with; an explicit value overrides it.
143+
# The chain reads this flag live each control-loop iteration, so a late set is safe.
144+
if enable_auto_recovery is not None:
145+
self.motor_chain.enable_auto_recovery = enable_auto_recovery
141146
self.use_gravity_comp = use_gravity_comp
142147
self.gravity_comp_factor = (
143148
gravity_comp_factor if gravity_comp_factor is not None else np.ones(len(motor_chain))
@@ -303,6 +308,7 @@ def get_robot_info(self) -> Dict[str, Any]:
303308
"gripper_limits": self._gripper_limits,
304309
"gravity_comp_factor": self.gravity_comp_factor,
305310
"gripper_index": self._gripper_index,
311+
"enable_auto_recovery": getattr(self.motor_chain, "enable_auto_recovery", False),
306312
}
307313
if self._gripper_index is not None:
308314
info["limit_gripper_effort"] = self._limit_gripper_force

0 commit comments

Comments
 (0)