11"""LeRobot Robot implementation for MuJoCo simulation with LeKiwi robot"""
22
3- from dataclasses import dataclass
3+ import logging
44import threading
5+ from dataclasses import dataclass
56from typing import Any
67
78import mujoco
89import mujoco .viewer
910import numpy as np
1011from lerobot .robots .robot import Robot
1112
13+ from .kinematics import LeKiwiMobileBase
1214from .utilities import get_scene_path , get_timestep_config
1315
1416
@@ -120,6 +122,7 @@ def __init__(self, config: LeKiwiMujocoConfig) -> None:
120122 self .mj_data = mujoco .MjData (self .mj_model )
121123 self .simulation_thread = threading .Thread (target = self .run_mujoco_loop , daemon = True )
122124 self .mujoco_is_running = False
125+ self .mobile_base_kinematics = LeKiwiMobileBase (wheel_radius = 0.05 , robot_base_radius = 0.125 )
123126
124127 def run_mujoco_loop (self ) -> None :
125128 """Run the MuJoCo simulation loop in a separate thread."""
@@ -138,12 +141,18 @@ def run_mujoco_loop(self) -> None:
138141 if joint_name .startswith ("arm_" ):
139142 arm_state [f"{ joint_name } .pos" ] = self .mj_data .joint (joint_name ).qpos [0 ]
140143
141- wheel_state = self ._wheel_rads_to_body (
142- self .mj_data .joint ("base_left_wheel_joint" ).qvel [0 ],
143- self .mj_data .joint ("base_back_wheel_joint" ).qvel [0 ],
144- self .mj_data .joint ("base_right_wheel_joint" ).qvel [0 ],
144+ mobile_base_joint_velocities = [self .mj_data .joint ("base_left_wheel_joint" ).qvel [0 ],
145+ self .mj_data .joint ("base_right_wheel_joint" ).qvel [0 ],
146+ self .mj_data .joint ("base_back_wheel_joint" ).qvel [0 ]]
147+ mobile_base_velocity = self .mobile_base_kinematics .forward_kinematics (
148+ np .array (mobile_base_joint_velocities )
145149 )
146-
150+ wheel_state = {
151+ "x.vel" : mobile_base_velocity [0 ],
152+ "y.vel" : mobile_base_velocity [1 ],
153+ "theta.vel" : np .degrees (mobile_base_velocity [2 ]),
154+ }
155+
147156 self .protected_observation .set_observation ({** arm_state , ** wheel_state })
148157
149158 viewer .sync ()
@@ -259,21 +268,26 @@ def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
259268 safety limits on velocity.
260269
261270 """
262- print ("Action received:" , action )
271+ logging . debug ("Action received: %s " , action )
263272 base_goal_vel = {k : v for k , v in action .items () if k .endswith (".vel" )}
264273
265- base_wheel_goal_vel = self ._body_to_wheel_rads (
266- base_goal_vel ["x.vel" ], base_goal_vel ["y.vel" ], base_goal_vel ["theta.vel" ]
274+
275+ base_wheel_goal_vel = self .mobile_base_kinematics .inverse_kinematics (
276+ np .array ([base_goal_vel .get ("x.vel" , 0.0 ),
277+ base_goal_vel .get ("y.vel" , 0.0 ),
278+ np .radians (base_goal_vel .get ("theta.vel" , 0.0 ))])
267279 )
268280
269281 self .protected_lekiwi_data .set_base_data (
270- base_wheel_goal_vel ["base_left_wheel" ],
271- base_wheel_goal_vel ["base_back_wheel" ],
272- base_wheel_goal_vel ["base_right_wheel" ],
282+ base_left_wheel_vel = base_wheel_goal_vel [0 ],
283+ base_right_wheel_vel = base_wheel_goal_vel [1 ],
284+ base_back_wheel_vel = base_wheel_goal_vel [2 ],
273285 )
274- print ( "Wheels vel: " , base_wheel_goal_vel )
286+ logging . debug ( "Set wheel velocities to: %s " , base_wheel_goal_vel )
275287
276- return base_wheel_goal_vel
288+ return {"base_left_wheel_vel" : base_wheel_goal_vel [0 ],
289+ "base_right_wheel_vel" : base_wheel_goal_vel [1 ],
290+ "base_back_wheel_vel" : base_wheel_goal_vel [2 ]}
277291
278292 def disconnect (self ) -> None :
279293 """Disconnect from the robot and perform any necessary cleanup."""
@@ -285,94 +299,3 @@ def stop_base(self) -> None:
285299 """Stop the robot's base movement immediately."""
286300 # TODO(arilow): Implement.
287301 return
288-
289- # TODO(https://github.com/ekumenlabs/lekiwi-dora/pull/11#discussion_r2310632598): Move this
290- # to a kinematics module.
291- def _body_to_wheel_rads (
292- self ,
293- x : float ,
294- y : float ,
295- theta : float ,
296- wheel_radius : float = 0.05 ,
297- base_radius : float = 0.125 ,
298- max_raw : int = 3000 ,
299- ) -> dict [str , float ]:
300- """Convert desired body-frame velocities into wheel raw commands.
301-
302- Args:
303- x : Linear velocity in x (m/s).
304- y : Linear velocity in y (m/s).
305- theta : Rotational velocity (deg/s).
306- wheel_radius: Radius of each wheel (meters).
307- base_radius : Distance from the center of rotation to each wheel (meters).
308- max_raw : Maximum allowed raw command (ticks) per wheel.
309-
310- Returns:
311- A dictionary with wheels angular speeds in rad/s as:
312- {"base_left_wheel": value, "base_back_wheel": value, "base_right_wheel": value}.
313-
314- """
315- # Convert rotational velocity from deg/s to rad/s.
316- theta_rad = theta * (np .pi / 180.0 )
317-
318- # Create the body velocity vector [x, y, theta_rad].
319- velocity_vector = np .array ([x , y , theta_rad ])
320-
321- # Define the wheel mounting angles with a -90° offset.
322- angles = np .radians (np .array ([240 , 0 , 120 ]) - 90 )
323- # Build the kinematic matrix: each row maps body velocities to a wheel's linear speed.
324- # The third column (base_radius) accounts for the effect of rotation.
325- m = np .array ([[np .cos (a ), np .sin (a ), base_radius ] for a in angles ])
326-
327- # Compute each wheel's linear speed (m/s) and then its angular speed (rad/s).
328- wheel_linear_speeds = m .dot (velocity_vector )
329- wheel_angular_speeds = wheel_linear_speeds / wheel_radius
330-
331- # TODO(arilow): Find out why the wheels need to be inverted.
332- corrected_wheel_angular_speeds = - wheel_angular_speeds
333- return {
334- "base_left_wheel" : corrected_wheel_angular_speeds [0 ],
335- "base_back_wheel" : corrected_wheel_angular_speeds [1 ],
336- "base_right_wheel" : corrected_wheel_angular_speeds [2 ],
337- }
338-
339- def _wheel_rads_to_body (
340- self ,
341- left_wheel_speed : float ,
342- back_wheel_speed : float ,
343- right_wheel_speed : float ,
344- wheel_radius : float = 0.05 ,
345- base_radius : float = 0.125 ,
346- ) -> dict [str , Any ]:
347- """Convert wheel raw command feedback back into body-frame velocities.
348-
349- Args:
350- left_wheel_speed : Left wheel velocity in rad/s
351- back_wheel_speed : Back wheel velocity in rad/s
352- right_wheel_speed : Right wheel velocity in rad/s
353- wheel_radius: Radius of each wheel (meters).
354- base_radius : Distance from the robot center to each wheel (meters).
355-
356- Returns:
357- A dict (x.vel, y.vel, theta.vel) all in m/s
358-
359- """
360- wheel_radps = np .array ([left_wheel_speed , back_wheel_speed , right_wheel_speed ])
361- # Compute each wheel's linear speed (m/s) from its angular speed.
362- wheel_linear_speeds = wheel_radps * wheel_radius
363-
364- # Define the wheel mounting angles with a -90° offset.
365- angles = np .radians (np .array ([240 , 0 , 120 ]) - 90 )
366- # TODO(https://github.com/ekumenlabs/lekiwi-dora/pull/11#discussion_r2310641980): Review kinematics here.
367- m = np .array ([[np .cos (a ), np .sin (a ), base_radius ] for a in angles ])
368-
369- # Solve the inverse kinematics: body_velocity = M⁻¹ · wheel_linear_speeds.
370- m_inv = np .linalg .inv (m )
371- velocity_vector = m_inv .dot (wheel_linear_speeds )
372- x , y , theta_rad = velocity_vector
373- theta = theta_rad * (180.0 / np .pi )
374- return {
375- "x.vel" : x ,
376- "y.vel" : y ,
377- "theta.vel" : theta ,
378- } # m/s and deg/s
0 commit comments