@@ -30,6 +30,7 @@ def __init__(
3030 log_level : str = "INFO" ,
3131 check_collision : bool = False ,
3232 kinematics_engine : str = "AnalyticalKinematics" ,
33+ hardware_error_check_frequency : float = 1.0 ,
3334 ):
3435 """Initialize the RobotBackend.
3536
@@ -38,6 +39,7 @@ def __init__(
3839 log_level (str): The logging level for the backend. Default is "INFO".
3940 check_collision (bool): If True, enable collision checking. Default is False.
4041 kinematics_engine (str): Kinematics engine to use. Defaults to "AnalyticalKinematics".
42+ hardware_error_check_frequency (float): Frequency in seconds to check for hardware errors. Default is 1.0.
4143
4244 Tries to connect to the Reachy Mini motor controller and initializes the control loop.
4345
@@ -82,6 +84,8 @@ def __init__(
8284 self .target_antenna_joint_current = None # Placeholder for antenna joint torque
8385 self .target_head_joint_current = None # Placeholder for head joint torque
8486
87+ self .hardware_error_check_frequency = hardware_error_check_frequency # seconds
88+
8589 def run (self ) -> None :
8690 """Run the control loop for the robot backend.
8791
@@ -96,6 +100,8 @@ def run(self) -> None:
96100 self .retries = 5
97101 self .stats_record_t0 = time .time ()
98102
103+ self .last_hardware_error_check_time = time .time ()
104+
99105 next_call_event = Event ()
100106
101107 # Compute the forward kinematics to get the initial head pose
@@ -240,6 +246,18 @@ def _update(self) -> None:
240246 self ._stats ["nb_error" ] = 0
241247 self .stats_record_t0 = time .time ()
242248
249+ if (
250+ time .time () - self .last_hardware_error_check_time
251+ > self .hardware_error_check_frequency
252+ ):
253+ hardware_errors = self .read_hardware_errors ()
254+ if hardware_errors :
255+ for motor_name , errors in hardware_errors .items ():
256+ self .logger .error (
257+ f"Motor '{ motor_name } ' hardware errors: { errors } "
258+ )
259+ self .last_hardware_error_check_time = time .time ()
260+
243261 def close (self ) -> None :
244262 """Close the motor controller connection."""
245263 if self .c is not None :
@@ -478,6 +496,33 @@ def _infer_control_mode(self) -> MotorControlMode:
478496 else :
479497 raise ValueError (f"Unknown motor control mode: { mode } " )
480498
499+ def read_hardware_errors (self ) -> dict [str , list [str ]]:
500+ """Read hardware errors from the motor controller."""
501+ if self .c is None :
502+ return {}
503+
504+ def decode_hardware_error_byte (err_byte : int ) -> list [str ]:
505+ # https://emanual.robotis.com/docs/en/dxl/x/xl330-m288/#hardware-error-status
506+ bits_to_error = {
507+ 0 : "Input Voltage Error" ,
508+ 2 : "Overheating Error" ,
509+ 4 : "Electrical Shock Error" ,
510+ 5 : "Overload Error" ,
511+ }
512+ err_bits = [i for i in range (8 ) if (err_byte & (1 << i )) != 0 ]
513+ return [bits_to_error [b ] for b in err_bits if b in bits_to_error ]
514+
515+ errors = {}
516+ for name , id in self .c .get_motor_name_id ().items ():
517+ # https://emanual.robotis.com/docs/en/dxl/x/xl330-m288/#hardware-error-status
518+ err_byte = self .c .async_read_raw_bytes (id , 70 , 1 )
519+ assert len (err_byte ) == 1
520+ err = decode_hardware_error_byte (err_byte [0 ])
521+ if err :
522+ errors [name ] = err
523+
524+ return errors
525+
481526
482527@dataclass
483528class RobotBackendStatus :
0 commit comments