2121
2222# Default paths and configurations for a single right arm
2323DEFAULT_SINGLE_A1X_URDF_PATH = os .path .join (ASSET_PATH , "galaxea/A1X/a1x.urdf" )
24+ DEFAULT_DUAL_A1X_URDF_PATH = os .path .join (ASSET_PATH , "galaxea/A1X/dual_a1x.urdf" )
2425DEFAULT_SCALE_FACTOR = 1.0
2526CONTROLLER_DEADZONE = 0.1
2627
3940 },
4041}
4142
43+ DEFAULT_DUAL_END_EFFECTOR_CONFIG = {
44+ "right_arm" : {
45+ "link_name" : "right_gripper_link" , # URDF link name for the single arm
46+ "pose_source" : "right_controller" ,
47+ "control_trigger" : "right_grip" ,
48+ "gripper_config" : {
49+ "joint_name" : "right_gripper_finger_joint1" ,
50+ "gripper_trigger" : "right_trigger" ,
51+ "open_pos" : - 2.0 ,
52+ "close_pos" : 0.0 ,
53+ },
54+ },
55+ "left_arm" : {
56+ "link_name" : "left_gripper_link" , # URDF link name for the single arm
57+ "pose_source" : "left_controller" ,
58+ "control_trigger" : "left_grip" ,
59+ "gripper_config" : {
60+ "joint_name" : "left_gripper_finger_joint1" ,
61+ "gripper_trigger" : "left_trigger" ,
62+ "open_pos" : - 2.0 ,
63+ "close_pos" : 0.0 ,
64+ },
65+ },
66+ }
67+
4268
4369class GalaxeaA1XTeleopController :
4470 def __init__ (
@@ -59,7 +85,13 @@ def __init__(
5985 self .visualize_placo = visualize_placo
6086 self .end_effector_config = end_effector_config
6187
62- self .right_controller = A1XController ()
88+ rospy .init_node ("galaxea_teleop_controller" , anonymous = True )
89+ self .right_controller = A1XController (
90+ arm_control_topic = "/motion_control/control_arm_right" ,
91+ gripper_control_topic = "/motion_control/control_gripper_right" ,
92+ arm_state_topic = "/hdas/feedback_arm_right" ,
93+ rate_hz = ros_rate_hz ,
94+ )
6395
6496 self .placo_robot = placo .RobotWrapper (self .robot_urdf_path )
6597 self .solver = placo .KinematicsSolver (self .placo_robot )
@@ -205,3 +237,198 @@ def run_control_thread(self, stop_event: threading.Event):
205237 self .right_controller .publish_gripper_control ()
206238 self .right_controller .rate .sleep ()
207239 print ("Galaxea teleop controller shutting down." )
240+
241+
242+ class GalaxeaDualA1XTeleopController :
243+ def __init__ (
244+ self ,
245+ xr_client : XrClient ,
246+ robot_urdf_path : str = DEFAULT_DUAL_A1X_URDF_PATH ,
247+ end_effector_config : dict = DEFAULT_DUAL_END_EFFECTOR_CONFIG ,
248+ R_headset_world : np .ndarray = R_HEADSET_TO_WORLD ,
249+ scale_factor : float = DEFAULT_SCALE_FACTOR ,
250+ visualize_placo : bool = True ,
251+ ros_rate_hz : int = 100 ,
252+ ):
253+
254+ self .xr_client = xr_client
255+ self .robot_urdf_path = robot_urdf_path
256+ self .R_headset_world = R_headset_world
257+ self .scale_factor = scale_factor
258+ self .visualize_placo = visualize_placo
259+ self .end_effector_config = end_effector_config
260+
261+ rospy .init_node ("galaxea_teleop_controller" , anonymous = True )
262+
263+ self .left_controller = A1XController (
264+ arm_control_topic = "/motion_control/control_arm_left" ,
265+ gripper_control_topic = "/motion_control/control_gripper_left" ,
266+ arm_state_topic = "/hdas/feedback_arm_left" ,
267+ rate_hz = ros_rate_hz ,
268+ )
269+
270+ self .right_controller = A1XController (
271+ arm_control_topic = "/motion_control/control_arm_right" ,
272+ gripper_control_topic = "/motion_control/control_gripper_right" ,
273+ arm_state_topic = "/hdas/feedback_arm_right" ,
274+ rate_hz = ros_rate_hz ,
275+ )
276+
277+ self .placo_robot = placo .RobotWrapper (self .robot_urdf_path )
278+ self .solver = placo .KinematicsSolver (self .placo_robot )
279+ self .solver .dt = 1.0 / ros_rate_hz
280+ self .solver .mask_fbase (True )
281+ self .solver .add_kinetic_energy_regularization_task (1e-6 )
282+
283+ self .effector_task = {}
284+ self .init_ee_xyz = {}
285+ self .init_ee_quat = {}
286+ self .init_controller_xyz = {}
287+ self .init_controller_quat = {}
288+ self .arm_active = {}
289+ for name , config in self .end_effector_config .items ():
290+ self .effector_task [name ] = self .solver .add_frame_task (config ["link_name" ], np .eye (4 ))
291+ self .effector_task [name ].configure (f"{ name } _frame" , "soft" , 1.0 )
292+ self .solver .add_manipulability_task (config ["link_name" ], "both" , 1.0 ).configure (
293+ f"{ name } _manipulability" , "soft" , 5e-2
294+ )
295+ self .init_ee_xyz [name ] = None
296+ self .init_ee_quat [name ] = None
297+ self .init_controller_xyz [name ] = None
298+ self .init_controller_quat [name ] = None
299+ self .arm_active [name ] = False
300+
301+ self .gripper_pos_target = {}
302+ for name , config in end_effector_config .items ():
303+ if "gripper_config" in config :
304+ gripper_config = config ["gripper_config" ]
305+ self .gripper_pos_target [gripper_config ["joint_name" ]] = gripper_config ["open_pos" ]
306+
307+ print ("Waiting for initial joint state from the Galaxea arm..." )
308+ while not rospy .is_shutdown () and self .right_controller .timestamp == 0 :
309+ rospy .sleep (0.1 )
310+ print ("Initial joint state received." )
311+
312+ self .placo_robot .state .q [7 :13 ] = self .left_controller .qpos
313+ self .placo_robot .state .q [15 :21 ] = self .right_controller .qpos
314+
315+ if self .visualize_placo :
316+ self .placo_robot .update_kinematics ()
317+ self .placo_vis = robot_viz (self .placo_robot )
318+ time .sleep (0.5 )
319+ webbrowser .open (self .placo_vis .viewer .url ())
320+ self .placo_vis .display (self .placo_robot .state .q )
321+ for name , config in self .end_effector_config .items ():
322+ robot_frame_viz (self .placo_robot , config ["link_name" ])
323+ frame_viz (f"vis_target_{ name } " , self .effector_task [name ].T_world_frame )
324+
325+ def _process_xr_pose (self , xr_pose , arm_name : str ):
326+ controller_xyz = np .array ([xr_pose [0 ], xr_pose [1 ], xr_pose [2 ]])
327+ controller_quat = np .array ([xr_pose [6 ], xr_pose [3 ], xr_pose [4 ], xr_pose [5 ]])
328+
329+ controller_xyz = self .R_headset_world @ controller_xyz
330+ R_transform = np .eye (4 )
331+ R_transform [:3 , :3 ] = self .R_headset_world
332+ R_quat = tf .quaternion_from_matrix (R_transform )
333+ controller_quat = tf .quaternion_multiply (
334+ tf .quaternion_multiply (R_quat , controller_quat ), tf .quaternion_conjugate (R_quat )
335+ )
336+
337+ if self .init_controller_xyz [arm_name ] is None :
338+ self .init_controller_xyz [arm_name ] = controller_xyz .copy ()
339+ self .init_controller_quat [arm_name ] = controller_quat .copy ()
340+ delta_xyz = np .zeros (3 )
341+ delta_rot = np .array ([0.0 , 0.0 , 0.0 ])
342+ else :
343+ delta_xyz = (controller_xyz - self .init_controller_xyz [arm_name ]) * self .scale_factor
344+ delta_rot = quat_diff_as_angle_axis (self .init_controller_quat [arm_name ], controller_quat )
345+ return delta_xyz , delta_rot
346+
347+ def update_ik (self ):
348+ self .placo_robot .state .q [7 :13 ] = self .left_controller .qpos
349+ self .placo_robot .state .q [15 :21 ] = self .right_controller .qpos
350+ self .placo_robot .update_kinematics ()
351+
352+ for arm_name , config in self .end_effector_config .items ():
353+ xr_grip_val = self .xr_client .get_key_value_by_name (config ["control_trigger" ])
354+ self .arm_active [arm_name ] = xr_grip_val > (1.0 - CONTROLLER_DEADZONE )
355+
356+ if self .arm_active [arm_name ]:
357+ if self .init_ee_xyz [arm_name ] is None :
358+ self .init_ee_xyz [arm_name ] = self .placo_robot .get_T_world_frame (config ["link_name" ])[:3 , 3 ]
359+ self .init_ee_quat [arm_name ] = tf .quaternion_from_matrix (
360+ self .placo_robot .get_T_world_frame (config ["link_name" ])
361+ )
362+ print (f"{ arm_name } activated." )
363+
364+ xr_pose = self .xr_client .get_pose_by_name (config ["pose_source" ])
365+ delta_xyz , delta_rot = self ._process_xr_pose (xr_pose , arm_name )
366+ target_xyz , target_quat = apply_delta_pose (
367+ self .init_ee_xyz [arm_name ], self .init_ee_quat [arm_name ], delta_xyz , delta_rot
368+ )
369+ target_pose = tf .quaternion_matrix (target_quat )
370+ target_pose [:3 , 3 ] = target_xyz
371+ self .effector_task [arm_name ].T_world_frame = target_pose
372+
373+ else :
374+ if self .init_ee_xyz [arm_name ] is not None :
375+ self .init_ee_xyz [arm_name ] = None
376+ self .init_controller_xyz [arm_name ] = None
377+ print (f"{ arm_name } deactivated." )
378+ self .effector_task [arm_name ].T_world_frame = self .placo_robot .get_T_world_frame (config ["link_name" ])
379+
380+ try :
381+ self .solver .solve (True )
382+
383+ if self .visualize_placo :
384+ self .placo_vis .display (self .placo_robot .state .q )
385+ for name in self .end_effector_config :
386+ frame_viz (f"vis_target_{ name } " , self .effector_task [name ].T_world_frame )
387+ except RuntimeError as e :
388+ print (f"IK solver failed: { e } " )
389+ except Exception as e :
390+ print (f"An unexpected error occurred in IK: { e } " )
391+
392+ for arm_name , config in self .end_effector_config .items ():
393+ if arm_name == "left_arm" and self .arm_active [arm_name ]:
394+ self .left_controller .q_des = self .placo_robot .state .q [7 :13 ].copy ()
395+ if arm_name == "right_arm" and self .arm_active [arm_name ]:
396+ self .right_controller .q_des = self .placo_robot .state .q [15 :21 ].copy ()
397+ # Update gripper position based on XR input
398+ if "gripper_config" in config :
399+ gripper_config = config ["gripper_config" ]
400+ trigger_value = self .xr_client .get_key_value_by_name (gripper_config ["gripper_trigger" ])
401+ gripper_pos = calc_parallel_gripper_position (
402+ gripper_config ["open_pos" ],
403+ gripper_config ["close_pos" ],
404+ trigger_value ,
405+ )
406+ joint_name = gripper_config ["joint_name" ]
407+ self .gripper_pos_target [joint_name ] = [gripper_pos ]
408+
409+ self .left_controller .q_des_gripper = self .gripper_pos_target [
410+ self .end_effector_config ["left_arm" ]["gripper_config" ]["joint_name" ]
411+ ]
412+ self .right_controller .q_des_gripper = self .gripper_pos_target [
413+ self .end_effector_config ["right_arm" ]["gripper_config" ]["joint_name" ]
414+ ]
415+ print (self .right_controller .q_des_gripper )
416+
417+ def run_ik_thread (self , stop_event : threading .Event ):
418+ print ("Starting Galaxea A1X single arm teleop controller IK loop..." )
419+ while not (rospy .is_shutdown () or stop_event .is_set ()):
420+ start = time .time ()
421+ self .update_ik ()
422+ elapsed = time .time () - start
423+ time .sleep (max (0 , (self .solver .dt - elapsed )))
424+ print ("Galaxea teleop controller IK loop shutting down." )
425+
426+ def run_control_thread (self , stop_event : threading .Event ):
427+ print ("Starting Galaxea A1X single arm teleop controller loop..." )
428+ while not (rospy .is_shutdown () or stop_event .is_set ()):
429+ self .left_controller .publish_arm_control ()
430+ self .left_controller .publish_gripper_control ()
431+ self .right_controller .publish_arm_control ()
432+ self .right_controller .publish_gripper_control ()
433+ self .right_controller .rate .sleep ()
434+ print ("Galaxea teleop controller shutting down." )
0 commit comments