Skip to content

Commit fa2362c

Browse files
author
Johannes Hechtl
committed
added servo control
1 parent c547087 commit fa2362c

4 files changed

Lines changed: 210 additions & 52 deletions

File tree

extensions/rcs_ur5e/src/rcs_ur5e/hw.py

Lines changed: 200 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import typing
22
from dataclasses import dataclass
3-
3+
import multiprocessing as mp
4+
from multiprocessing.shared_memory import SharedMemory
5+
import time
46
import numpy as np
57
import rtde_control
68
import rtde_receive
7-
89
from 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

107261
class 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)

extensions/rcs_ur5e/src/rcs_ur5e/test_robot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
ROBOT_IP = "192.168.25.201"
66

7-
robot = UR5e(ROBOT_IP, "sth")
7+
robot = UR5e(ROBOT_IP)
88
print(f"Robot joint positions: {robot.get_joint_position()}")
99
print(f"Robot cartesian position: {robot.get_cartesian_position()}")
1010
print(f"Robot Parameters: {robot.get_parameters()}")

extensions/rcs_ur5e/src/rcs_ur5e/ur5e_env_cartesian_control.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
def main():
2828
if ROBOT_INSTANCE == RobotPlatform.HARDWARE:
2929
robot_cfg = UR5eConfig()
30-
robot_cfg.blocking = True
30+
robot_cfg.blocking = False
3131
env_rel = RCSUR5eEnvCreator()(
3232
robot_cfg=robot_cfg,
3333
control_mode=ControlMode.CARTESIAN_TQuat,
@@ -67,9 +67,9 @@ def main():
6767
env_rel.get_wrapper_attr("sim").open_gui()
6868

6969
obs, info = env_rel.reset()
70-
print(obs)
71-
print(env_rel.unwrapped.robot.get_cartesian_position()) # type: ignore
72-
print(env_rel.unwrapped.robot.get_joint_position()) # type: ignore
70+
# print(obs)
71+
# print(env_rel.unwrapped.robot.get_cartesian_position()) # type: ignore
72+
# print(env_rel.unwrapped.robot.get_joint_position()) # type: ignore
7373

7474
for _ in range(100):
7575
# act = {"joints": common.robots_meta_config(common.RobotType.UR5e).q_home, "gripper": 0}
@@ -80,9 +80,9 @@ def main():
8080
# sleep(1)
8181
for _ in range(10):
8282
# move 1cm in x direction (forward) and close gripper
83-
act = {"tquat": [0.01, 0, 0.0087265, 0, 0, 0, 0.9999619], "gripper": 0}
83+
act = {"tquat": [0.01, 0,0, 0.0087265, 0, 0, 0.9999619], "gripper": 0}
8484
obs, reward, terminated, truncated, info = env_rel.step(act)
85-
print(obs)
85+
#print(obs)
8686
# print(env_rel.unwrapped.robot.get_cartesian_position()) # type: ignore
8787
if truncated or terminated:
8888
logger.info("Truncated or terminated!")

extensions/rcs_ur5e/src/rcs_ur5e/ur5e_env_joint_control.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import rcs
1010
from rcs import sim
11+
from rcs_ur5e.hw import UR5eConfig
1112

1213
logger = logging.getLogger(__name__)
1314
logger.setLevel(logging.INFO)
@@ -19,8 +20,10 @@
1920
def main():
2021

2122
if ROBOT_INSTANCE == RobotPlatform.HARDWARE:
23+
robot_cfg = UR5eConfig()
2224
env_rel = RCSUR5eEnvCreator()(
2325
control_mode=ControlMode.JOINTS,
26+
robot_cfg=robot_cfg,
2427
ip=ROBOT_IP,
2528
camera_set=None,
2629
max_relative_movement=np.deg2rad(5),

0 commit comments

Comments
 (0)