Skip to content

Commit 83922de

Browse files
niklasschlueterdanielsanjoseproNiklas SchuelterCopilot
authored
feat: add dynaarm config https://www.duatic.com/dynaarm (#67)
* add option to activate other controller with switch controller * example with dynaarm * add params for dyna * example dynaarm * Update examples/example_dynaarm.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update examples/example_dynaarm.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update examples/example_dynaarm.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update crisp_py/control/controller_switcher.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update robot_config.py to include dynaarm * Refactor variablecontroller_switcher.py * Update crisp_py/control/controller_switcher.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update example_dynaarm.py with new variable name --------- Co-authored-by: Daniel San José Pro <42489409+danielsanjosepro@users.noreply.github.com> Co-authored-by: Niklas Schuelter <niklasschuelter@tum.de> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 815e09c commit 83922de

6 files changed

Lines changed: 310 additions & 18 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
base_frame: base_link
2+
end_effector_frame: flange
3+
filter.dq: 1.0
4+
filter.output_torque: 1.0
5+
filter.q: 1.0
6+
filter.target_pose: 0.1
7+
joint_limit_repulsion.enabled: false
8+
joint_limit_repulsion.max_torque: 5.0
9+
joint_limit_repulsion.safe_range: 0.1
10+
11+
limit_error: false
12+
limit_torques: true
13+
log.computed_torques: false
14+
log.control_values: false
15+
log.controller_parameters: true
16+
log.dynamic_params: false
17+
log.enabled: true
18+
log.limits: false
19+
log.robot_state: false
20+
log.timing: false
21+
22+
max_delta_tau: 0.5
23+
noise.add_random_noise: false
24+
noise.amplitude: 0.0
25+
nullspace.damping: 1.0
26+
nullspace.max_tau: 5.0
27+
nullspace.projector_type: none
28+
nullspace.regularization: 1.0e-06
29+
nullspace.stiffness: 0.0
30+
nullspace.weights.elbow_flexion.value: 0.0
31+
nullspace.weights.forearm_rotation.value: 0.0
32+
nullspace.weights.shoulder_flexion.value: 0.0
33+
nullspace.weights.shoulder_rotation.value: 0.0
34+
nullspace.weights.wrist_flexion.value: 0.0
35+
nullspace.weights.wrist_rotation.value: 0.0
36+
operational_space_regularization: 1.0
37+
stop_commands: false
38+
39+
task.d_pos_x: -1.0
40+
task.d_pos_y: -1.0
41+
task.d_pos_z: -1.0
42+
task.d_rot_x: -1.0
43+
task.d_rot_y: -1.0
44+
task.d_rot_z: -1.0
45+
task.error_clip.rx: 0.005
46+
task.error_clip.ry: 0.005
47+
task.error_clip.rz: 0.005
48+
task.error_clip.x: 0.005
49+
task.error_clip.y: 0.005
50+
task.error_clip.z: 0.005
51+
52+
task.k_pos_x: 100.0
53+
task.k_pos_y: 100.0
54+
task.k_pos_z: 100.0
55+
task.k_rot_x: 10.0
56+
task.k_rot_y: 10.0
57+
task.k_rot_z: 10.0
58+
59+
use_coriolis_compensation: true
60+
use_friction: false
61+
use_gravity_compensation: true
62+
use_local_jacobian: true
63+
use_operational_space: false
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
joint_limit_repulsion.enabled: false
2+
joint_limit_repulsion.max_torque: 5.0
3+
joint_limit_repulsion.safe_range: 0.1
4+
5+
limit_error: true
6+
limit_torques: true
7+
8+
log.enabled: false
9+
10+
max_delta_tau: 0.5
11+
12+
nullspace.damping: -1.0
13+
nullspace.max_tau: 5.0
14+
nullspace.projector_type: none
15+
nullspace.regularization: 1.0e-06
16+
nullspace.stiffness: 0.0
17+
18+
operational_space_regularization: 1.0
19+
stop_commands: false
20+
21+
task.d_pos_x: -1.0
22+
task.d_pos_y: -1.0
23+
task.d_pos_z: -1.0
24+
task.d_rot_x: -1.0
25+
task.d_rot_y: -1.0
26+
task.d_rot_z: -1.0
27+
28+
task.error_clip.rx: 0.5
29+
task.error_clip.ry: 0.5
30+
task.error_clip.rz: 0.5
31+
task.error_clip.x: 0.05
32+
task.error_clip.y: 0.05
33+
task.error_clip.z: 0.05
34+
35+
task.k_pos_x: 0.0
36+
task.k_pos_y: 0.0
37+
task.k_pos_z: 0.0
38+
task.k_rot_x: 0.0
39+
task.k_rot_y: 0.0
40+
task.k_rot_z: 0.0
41+
42+
use_coriolis_compensation: true
43+
use_friction: false
44+
45+
use_gravity_compensation: true
46+
use_local_jacobian: true
47+
48+
use_operational_space: false
49+
50+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
robot_type: "dynaarm"

crisp_py/control/controller_switcher.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,26 @@ def _switch_controller(self, to_deactivate: list[str], to_activate: list[str]) -
120120

121121
return response.ok
122122

123-
def switch_controller(self, controller_name: str) -> bool | None:
123+
def switch_controller(
124+
self,
125+
controller_name: str,
126+
controllers_that_should_be_active: list[str] | None = None,
127+
) -> bool | None:
124128
"""Switch to a different ros2_controller that is already loaded using a service.
125129
126130
First we request a list of current controllers.
127131
If the desired controller is not loaded, then we request to load it and configure it.
128-
Finally, we request to switch to the desired controller.
132+
Then we prepare a list of controllers to deactivate (all active controllers except broadcasters and those that should remain active).
133+
We also prepare a list of controllers to activate (the desired controller and those that should remain active).
134+
Finally, we request to switch to the desired controllers.
129135
130136
Args:
131137
controller_name (str): Name of the controller to switch to.
138+
controllers_that_should_be_active (list[str] | None): List of controller names to keep active or activate in the switch. Defaults to None.
132139
"""
140+
if controllers_that_should_be_active is None:
141+
controllers_that_should_be_active = []
142+
133143
controllers = self.get_controller_list()
134144

135145
active_controllers = [
@@ -144,24 +154,23 @@ def switch_controller(self, controller_name: str) -> bool | None:
144154
return True
145155

146156
if controller_name not in inactive_controllers:
147-
ok = self.load_controller(controller_name)
148-
if not ok:
149-
self.node.get_logger().error(
150-
f"Failed to load controller {controller_name}. Are you sure the controller exists?"
151-
)
152-
raise RuntimeError(f"Failed to load controller {controller_name}.")
153-
154-
ok = self.configure_controller(controller_name)
155-
if not ok:
156-
self.node.get_logger().error(f"Failed to configure controller {controller_name}.")
157-
raise RuntimeError(f"Failed to configure controller {controller_name}.")
157+
self._try_loading_and_configuring_controller(controller_name)
158158

159159
to_deactivate = []
160160
for active_controller in active_controllers:
161-
if not active_controller.endswith("broadcaster"): # Do not deactivate broadcasters
162-
to_deactivate.append(active_controller)
161+
if active_controller in controllers_that_should_be_active:
162+
continue # Keep this controller active
163+
164+
if active_controller.endswith("broadcaster"): # Do not deactivate broadcasters
165+
continue
163166

164-
to_activate = [controller_name]
167+
to_deactivate.append(active_controller)
168+
169+
to_activate = [
170+
inactive_controller
171+
for inactive_controller in inactive_controllers
172+
if inactive_controller in controllers_that_should_be_active
173+
] + [controller_name]
165174

166175
ok = self._switch_controller(to_deactivate, to_activate)
167176

@@ -170,3 +179,21 @@ def switch_controller(self, controller_name: str) -> bool | None:
170179
raise RuntimeError(f"Failed to switch to controller {controller_name}.")
171180

172181
return True
182+
183+
def _try_loading_and_configuring_controller(self, controller_name: str) -> None:
184+
"""Try to load and configure a controller.
185+
186+
Args:
187+
controller_name (str): Name of the controller to load and configure.
188+
"""
189+
ok = self.load_controller(controller_name)
190+
if not ok:
191+
self.node.get_logger().error(
192+
f"Failed to load controller {controller_name}. Are you sure the controller exists?"
193+
)
194+
raise RuntimeError(f"Failed to load controller {controller_name}.")
195+
196+
ok = self.configure_controller(controller_name)
197+
if not ok:
198+
self.node.get_logger().error(f"Failed to configure controller {controller_name}.")
199+
raise RuntimeError(f"Failed to configure controller {controller_name}.")

crisp_py/robot/robot_config.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,44 @@ class SO101Config(RobotConfig):
258258
target_frame: str = "Fixed_Gripper"
259259

260260

261+
@dataclass
262+
class DynaArmConfig(RobotConfig):
263+
"""Configuration specific to DynaArm robots.
264+
265+
Provides default values for frame names, joint names, and home configuration
266+
specifically for DynaArm robots.
267+
"""
268+
269+
joint_names: list = field(
270+
default_factory=lambda: [
271+
"shoulder_rotation",
272+
"shoulder_flexion",
273+
"elbow_flexion",
274+
"forearm_rotation",
275+
"wrist_flexion",
276+
"wrist_rotation",
277+
]
278+
)
279+
home_config: list = field(
280+
default_factory=lambda: [
281+
2.4,
282+
-0.15,
283+
2.0,
284+
0.0,
285+
1.26,
286+
0.0,
287+
],
288+
)
289+
base_frame: str = "base_link"
290+
target_frame: str = "flange"
291+
cartesian_impedance_controller_name: str = "crisp_cartesian_controller"
292+
293+
261294
def make_robot_config(robot_type: str, **kwargs) -> RobotConfig: # noqa: ANN003
262295
"""Factory function to create robot configuration objects.
263296
264297
Args:
265-
robot_type (str): Type of robot ('franka', 'kinova', 'iiwa', 'so101')
298+
robot_type (str): Type of robot ('franka', 'kinova', 'iiwa', 'so101', 'dynaarm')
266299
**kwargs: Additional keyword arguments to override default configuration
267300
268301
Returns:
@@ -283,7 +316,9 @@ def make_robot_config(robot_type: str, **kwargs) -> RobotConfig: # noqa: ANN003
283316
return IiwaConfig(**kwargs)
284317
elif robot_type == "so101":
285318
return SO101Config(**kwargs)
319+
elif robot_type == "dynaarm":
320+
return DynaArmConfig(**kwargs)
286321
else:
287322
raise ValueError(
288-
f"Unsupported robot type: {robot_type}. Supported types: franka, panda, kinova, iiwa, so101"
323+
f"Unsupported robot type: {robot_type}. Supported types: franka, panda, kinova, iiwa, so101, dynaarm"
289324
)

examples/example_dynaarm.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Example script to control the DynaArm robot using crisp_py."""
2+
from pathlib import Path
3+
import time
4+
import numpy as np
5+
6+
from crisp_py.robot import make_robot
7+
8+
robot = make_robot("dynaarm")
9+
robot.wait_until_ready() # Wait until the robot is ready to receive commands
10+
11+
#%%
12+
13+
print(f"Starting pose: {robot.end_effector_pose}")
14+
print(f"Starting joint values: {robot.joint_values}")
15+
16+
#%%
17+
18+
print("Going to home position...")
19+
robot.home() # This requires the joint_trajectory_controller to be active
20+
homing_pose = robot.end_effector_pose.copy()
21+
22+
print(f"Homing pose: {homing_pose}")
23+
24+
#%%
25+
26+
print("Switching to Cartesian Impedance Controller...")
27+
print("This will unload other controllers if necessary.")
28+
29+
# path = Path("config/control/dynaarm_gravity.yaml")
30+
path = Path("config/control/dynaarm_cic_soft.yaml")
31+
32+
robot.cartesian_controller_parameters_client.load_param_config(path)
33+
# robot.cartesian_controller_parameters_client.save_param_config("config/control/dynaarm_cic_soft.yaml")
34+
35+
robot.controller_switcher_client.switch_controller(
36+
"crisp_cartesian_controller",
37+
controllers_that_should_be_active=["freedrive_controller", "safety_monitor_controller"]
38+
)
39+
40+
robot.reset_targets()
41+
#%%
42+
43+
target_pose = robot.end_effector_pose
44+
target_pose.position[2] -= 0.1
45+
46+
#%%
47+
48+
# we apply a sine wave in z direction for 10 seconds from -0.15 to 0.15 m
49+
50+
ee_poses = []
51+
target_poses = []
52+
ts = []
53+
54+
start_time = time.time()
55+
while True:
56+
t = time.time()
57+
position = target_pose.position.copy()
58+
# position[0] = target_pose.position[0] + 0.2 * np.sin(2 * np.pi * 0.6 * (t - start_time)) # 0.1 Hz sine wave
59+
position[2] = target_pose.position[2] + 0.15 * np.sin(2 * np.pi * 0.3 * (t - start_time)) # 0.05 Hz sine wave
60+
robot.set_target(position=position)
61+
if t - start_time > 10.0:
62+
break
63+
ee_poses.append(robot.end_effector_pose.copy())
64+
target_poses.append(robot.target_pose.copy())
65+
ts.append(t - start_time)
66+
time.sleep(0.01) # Sleep for a short time to avoid busy waiting
67+
68+
69+
#%%
70+
71+
import matplotlib.pyplot as plt
72+
x_t = [target_pose_sample.position[0] for target_pose_sample in target_poses]
73+
y_t = [target_pose_sample.position[1] for target_pose_sample in target_poses]
74+
z_t = [target_pose_sample.position[2] for target_pose_sample in target_poses]
75+
76+
x_ee = [ee_pose.position[0] for ee_pose in ee_poses]
77+
y_ee = [ee_pose.position[1] for ee_pose in ee_poses]
78+
z_ee = [ee_pose.position[2] for ee_pose in ee_poses]
79+
80+
fig, ax = plt.subplots(1, 2, figsize=(10, 5))
81+
ax[0].plot(ts, x_ee, label="current")
82+
ax[0].plot(ts, x_t, label="target", linestyle="--")
83+
ax[0].set_ylabel("$x$")
84+
ax[0].set_xlabel("$t$")
85+
ax[0].legend()
86+
87+
ax[1].plot(ts, z_ee, label="current")
88+
ax[1].plot(ts, z_t, label="target", linestyle="--")
89+
ax[1].set_ylabel("$z$")
90+
ax[1].set_xlabel("$t$")
91+
ax[1].legend()
92+
93+
94+
for a in ax:
95+
a.grid()
96+
97+
fig.tight_layout()
98+
99+
plt.show()
100+
#%%
101+
print(f"End pose: {robot.end_effector_pose}")
102+
print(f"End joint values: {robot.joint_values}")
103+
104+
#%%
105+
106+
print(f"Shutting down connection in 2 seconds... (robot will stay in place).")
107+
time.sleep(2.0)
108+
109+
robot.shutdown()
110+
111+
# %%
112+
113+
# robot.controller_switcher_client.switch_controller(
114+
# "gravity_compensation_controller",
115+
# controllers_that_should_be_active=["freedrive_controller", "safety_monitor_controller"]
116+
# )

0 commit comments

Comments
 (0)