11import typing
22from dataclasses import dataclass
3-
3+ import multiprocessing as mp
4+ from multiprocessing .shared_memory import SharedMemory
5+ import time
46import numpy as np
57import rtde_control
68import rtde_receive
7-
89from rcs import common
9-
10- from . import robotiq_gripper
10+ from rcs_ur5e import robotiq_gripper
1111
1212
1313@dataclass (kw_only = True )
@@ -16,8 +16,9 @@ class UR5eConfig(common.RobotConfig):
1616 gain : float = 300.0
1717 max_velocity : float = 1.0
1818 max_acceleration : float = 1.0
19- blocking : bool = True
2019 async_control : bool = False
20+ max_servo_joint_step : float = 0.05
21+ max_servo_cartesian_step : float = 0.01
2122
2223 def __post_init__ (self ):
2324 super ().__init__ ()
@@ -29,80 +30,233 @@ def __post_init__(self):
2930 super ().__init__ ()
3031
3132
32- class UR5e : # (common.Robot): # should inherit and implement common.Robot, but currently there is a bug that needs to be fixed
33- def __init__ (self , ip : str ):
34- # self.ik = None common.RL(urdf_path=urdf_path)
35- # ik = rcs.common.Pin(
36- # robot_cfg.kinematic_model_path,
37- # robot_cfg.attachment_site,
38- # urdf=robot_cfg.kinematic_model_path.endswith(".urdf"),
39- # )
40- self ._config = UR5eConfig () # with default values
41- self ._config .robot_platform = common .RobotPlatform .HARDWARE
33+ # Define the shared memory size and name outside the class
34+ SHM_SIZE = 4 + 1 + 48 + 48 + 48 + 48
35+ SHM_NAME = "ur5e_control_shm"
36+
37+ NO_MODE = 0
38+ JOINT_MODE = 1
39+ CARTESIAN_MODE = 2
4240
41+ def _control_robot (shm_name : str , ip : str , stop_queue : mp .Queue , config_queue : mp .Queue ) -> None :
42+ """
43+ Control loop for the robot, running in a separate process.
44+ This function is a helper and not part of the class.
45+ """
46+ robot_config = UR5eConfig ()
47+ try :
48+ # Initialize robot interfaces
4349 # TODO(j.hechtl): this is currently blocking if connection fails
44- self .ur_control = rtde_control .RTDEControlInterface (ip )
45- self .ur_receive = rtde_receive .RTDEReceiveInterface (ip )
50+ ur_control = rtde_control .RTDEControlInterface (ip )
51+ ur_receive = rtde_receive .RTDEReceiveInterface (ip )
52+
53+ if not ur_control .isConnected ():
54+ raise ConnectionError (f"Could not connect to UR5e at { ip } ." )
55+
56+ # Attach to the shared memory segment
57+ shm = SharedMemory (name = shm_name )
58+ data_buffer = shm .buf
59+
60+ # Define offsets for each field in the shared memory buffer
61+ offset_mode = 0
62+ offset_target_reached = offset_mode + 4
63+ offset_joint_target = offset_target_reached + 1
64+ offset_cartesian_target = offset_joint_target + 48
65+ offset_joint_state = offset_cartesian_target + 48
66+ offset_cartesian_state = offset_joint_state + 48
67+
68+ # Create numpy views on the shared memory buffer
69+ joint_target_view = np .ndarray (
70+ (6 ,), dtype = np .float64 , buffer = data_buffer , offset = offset_joint_target
71+ )
72+ cartesian_target_view = np .ndarray (
73+ (6 ,), dtype = np .float64 , buffer = data_buffer , offset = offset_cartesian_target
74+ )
75+ joint_state_view = np .ndarray (
76+ (6 ,), dtype = np .float64 , buffer = data_buffer , offset = offset_joint_state
77+ )
78+ cartesian_state_view = np .ndarray (
79+ (6 ,), dtype = np .float64 , buffer = data_buffer , offset = offset_cartesian_state
80+ )
4681
82+ print ("Robot control process started." )
83+
84+ dt = 1.0 / 500.0 # 2ms
85+
86+ while stop_queue .empty ():
87+ if not config_queue .empty ():
88+ robot_config = config_queue .get ()
89+ t_start = ur_control .initPeriod ()
90+ mode = int .from_bytes (data_buffer [offset_mode :offset_target_reached ], "little" )
91+
92+ joint_state = np .array (ur_receive .getActualQ ())
93+ joint_state_view [:] = joint_state
94+
95+ cartesian_state = np .array (ur_receive .getActualTCPPose ())
96+ cartesian_state_view [:] = cartesian_state
97+
98+ if mode == JOINT_MODE :
99+ diff = joint_target_view - joint_state_view
100+ if np .max (np .abs (diff )) < 0.01 :
101+ data_buffer [offset_target_reached ] = 1
102+ if np .max (np .abs (diff )) > robot_config .max_servo_joint_step :
103+ diff = (robot_config .max_servo_joint_step / np .max (np .abs (diff ))) * diff
104+ target_q = (joint_state_view + diff ).tolist ()
105+ else :
106+ target_q = joint_target_view .tolist ()
107+ ur_control .servoJ (
108+ target_q ,
109+ robot_config .max_velocity ,
110+ robot_config .max_acceleration ,
111+ dt ,
112+ robot_config .lookahead_time ,
113+ robot_config .gain ,
114+ )
115+
116+ elif mode == CARTESIAN_MODE :
117+ diff = cartesian_target_view - cartesian_state_view
118+ if np .max (np .abs (diff )) < 0.0025 :
119+ data_buffer [offset_target_reached ] = 1
120+ if np .max (diff ) > robot_config .max_servo_cartesian_step :
121+ diff = (robot_config .max_servo_cartesian_step / np .max (diff )) * diff
122+ target_pose = (cartesian_state_view + diff ).tolist ()
123+ else :
124+ target_pose = cartesian_target_view .tolist ()
125+ ur_control .servoL (
126+ target_pose ,
127+ robot_config .max_velocity ,
128+ robot_config .max_acceleration ,
129+ dt ,
130+ robot_config .lookahead_time ,
131+ robot_config .gain ,
132+ )
133+
134+ ur_control .waitPeriod (t_start )
135+
136+ except Exception as e :
137+ print (f"Robot control process encountered an error: { e } " )
138+
139+ finally :
140+ print ("Robot control process is shutting down..." )
141+ if 'ur_control' in locals ():
142+ ur_control .servoStop ()
143+ ur_control .stopScript ()
144+ ur_control .disconnect ()
145+ if 'shm' in locals ():
146+ shm .close ()
147+
148+ class UR5e : # (common.Robot): # should inherit and implement common.Robot, but currently there is a bug that needs to be fixed
149+ def __init__ (self , ip : str ):
150+ self ._config = UR5eConfig ()
47151 self ._config .robot_type = common .RobotType .UR5e
48-
49- if not self .ur_control .isConnected ():
50- raise ConnectionError (f"Could not connect to UR5e at { ip } . Please check the IP address and connection." )
152+ self ._ip = ip
153+
154+ # Initialize shared memory and communication queues
155+ self ._shm = SharedMemory (name = SHM_NAME , create = True , size = SHM_SIZE )
156+ self ._shm_buffer = self ._shm .buf
157+ self ._stop_queue = mp .Queue ()
158+ self ._config_queue = mp .Queue ()
159+
160+ # Define numpy views on the shared memory buffer
161+ self ._offset_mode = 0
162+ self ._offset_target_reached = self ._offset_mode + 4
163+ self ._offset_joint_target = self ._offset_target_reached + 1
164+ self ._offset_cartesian_target = self ._offset_joint_target + 48
165+ self ._offset_joint_state = self ._offset_cartesian_target + 48
166+ self ._offset_cartesian_state = self ._offset_joint_state + 48
167+
168+ self ._joint_target_shm = np .ndarray ((6 ,), dtype = np .float64 , buffer = self ._shm_buffer , offset = self ._offset_joint_target )
169+ self ._cartesian_target_shm = np .ndarray ((6 ,), dtype = np .float64 , buffer = self ._shm_buffer , offset = self ._offset_cartesian_target )
170+ self ._joint_state_shm = np .ndarray ((6 ,), dtype = np .float64 , buffer = self ._shm_buffer , offset = self ._offset_joint_state )
171+ self ._cartesian_state_shm = np .ndarray ((6 ,), dtype = np .float64 , buffer = self ._shm_buffer , offset = self ._offset_cartesian_state )
172+
173+ # Initialise with -10 to check
174+ self ._joint_state_shm [:] = - 10
175+
176+ # Start the robot control process
177+ self ._robot_process = mp .Process (
178+ target = _control_robot ,
179+ args = (SHM_NAME , self ._ip , self ._stop_queue , self ._config_queue )
180+ )
181+ self ._robot_process .daemon = True # Kills process if main process exits
182+ self ._robot_process .start ()
183+
184+ # Check for first update
185+ while self ._joint_state_shm [0 ]== - 10 :
186+ print ("Waiting for first robot state to arrive.." )
187+ time .sleep (1 )
188+ print ("Robot Connection established." )
189+
190+
191+ def __del__ (self ):
192+ """Ensures resources are cleaned up when the object is garbage collected."""
193+ self .stop_control_process ()
194+
195+ def stop_control_process (self ):
196+ """Safely stops the robot control process."""
197+ if self ._robot_process .is_alive ():
198+ self ._stop_queue .put ("STOP" )
199+ self ._robot_process .join (timeout = 5 )
200+ if self ._robot_process .is_alive ():
201+ self ._robot_process .terminate ()
202+ self ._shm .close ()
203+ self ._shm .unlink ()
204+ print ("UR5e control process stopped and shared memory unlinked." )
51205
52206 def get_cartesian_position (self ) -> common .Pose :
53- ur_pose = self .ur_receive .getActualTCPPose ()
54-
55- trans = [ur_pose [0 ], ur_pose [1 ], ur_pose [2 ]]
207+ ur_pose = self ._cartesian_state_shm
208+ trans = ur_pose [0 :3 ]
56209 rotvec = common .RotVec (np .array (ur_pose [3 :6 ]))
57-
58210 pose = common .Pose (quaternion = rotvec .as_quaternion_vector (), translation = trans )
59-
60211 pose = common .Pose (rpy_vector = [0 , 0 , np .deg2rad (180 )], translation = [0 , 0 , 0 ]).inverse () * pose
61- print (f"Current: { pose .xyzrpy ()} " )
62212 return pose
63213
64214 def get_ik (self ) -> common .IK | None :
65- # TODO return ik object with values from robot
66215 return None
67216
68- def get_joint_position (self ) -> np .ndarray [tuple [typing .Literal [6 ]], np .dtype [np .float64 ]]: # type: ignore
69- return np .array (self .ur_receive . getActualQ () )
217+ def get_joint_position (self ) -> np .ndarray [tuple [typing .Literal [6 ]], np .dtype [np .float64 ]]:
218+ return np .array (self ._joint_state_shm )
70219
71220 def get_parameters (self ) -> UR5eConfig :
72221 return self ._config
73222
74223 def set_parameters (self , robot_cfg : UR5eConfig ) -> None :
75224 self ._config = robot_cfg
225+ self ._config_queue .put (robot_cfg )
76226
77227 def get_state (self ) -> UR5eState :
78228 return UR5eState
79229
230+ def set_cartesian_position (self , pose : common .Pose ) -> None :
231+ target_pose = (common .Pose (rpy_vector = [0 , 0 , np .deg2rad (180 )], translation = [0 , 0 , 0 ]) * pose ).rotvec ()
232+ self ._cartesian_target_shm [:] = target_pose
233+ self ._shm_buffer [self ._offset_mode :self ._offset_target_reached ] = (CARTESIAN_MODE ).to_bytes (4 , "little" )
234+ if not self ._config .async_control :
235+ while not self ._shm_buffer [self ._offset_target_reached ]:
236+ time .sleep (0.01 )
237+
238+ def set_joint_position (self , q : np .ndarray [tuple [typing .Literal [6 ]], np .dtype [np .float64 ]]) -> None :
239+ self ._joint_target_shm [:] = q
240+ self ._shm_buffer [self ._offset_mode :self ._offset_target_reached ] = (JOINT_MODE ).to_bytes (4 , "little" )
241+ if not self ._config .async_control :
242+ while not self ._shm_buffer [self ._offset_target_reached ]:
243+ time .sleep (0.01 )
244+
80245 def move_home (self ) -> None :
81246 home = typing .cast (
82247 np .ndarray [tuple [typing .Literal [6 ]], np .dtype [np .float64 ]],
83248 common .robots_meta_config (common .RobotType .UR5e ).q_home ,
84249 )
85- # check if home position is between -2*pi and 2*pi
86250 if np .any ((home < - 2 * np .pi ) | (home > 2 * np .pi )):
87251 raise ValueError (f"Home position { home } is out of bounds." )
88252 print (f"Moving to home position: { home } " )
89- asynchronous = not self ._config .blocking
90- self .ur_control .moveJ (home , self ._config .max_velocity , self ._config .max_acceleration , asynchronous )
253+ self ._joint_target_shm [:] = home
254+ self ._shm_buffer [self ._offset_mode :self ._offset_target_reached ] = (JOINT_MODE ).to_bytes (4 , "little" )
255+ while not self ._shm_buffer [self ._offset_target_reached ]:
256+ time .sleep (0.01 )
91257
92258 def reset (self ) -> None :
93- self .ur_control .stopL ()
94-
95- def set_cartesian_position (self , pose : common .Pose ) -> None :
96- asynchronous = not self ._config .blocking
97- print (f"target: { pose .xyzrpy ()} " )
98- pose = common .Pose (rpy_vector = [0 , 0 , np .deg2rad (180 )], translation = [0 , 0 , 0 ]) * pose
99- target_pose = pose .rotvec ()
100- self .ur_control .moveL (target_pose , self ._config .max_velocity , self ._config .max_acceleration , asynchronous )
101-
102- def set_joint_position (self , q : np .ndarray [tuple [typing .Literal [6 ]], np .dtype [np .float64 ]]) -> None : # type: ignore
103- asynchronous = not self ._config .blocking
104- self .ur_control .moveJ (q .tolist (), self ._config .max_velocity , self ._config .max_acceleration , asynchronous )
105-
259+ pass
106260
107261class RobotiQGripper : # (common.Gripper):
108262 def __init__ (self , ip ):
@@ -114,6 +268,7 @@ def __init__(self, ip):
114268 self .gripper .activate ()
115269 if not self .gripper .is_active ():
116270 raise RuntimeError ("Failed to activate Robotiq gripper." )
271+ print ("Gripper Connection established." )
117272
118273 def get_normalized_width (self ) -> float :
119274 # value between 0 and 1 (0 is closed)
0 commit comments