Skip to content

Commit 44cb860

Browse files
urfeexmergify[bot]
authored andcommitted
Wait for used controllers in test setup (#1519)
* Wait for used controllers in test setup Otherwise initial controller switches may fail * Fix typo (cherry picked from commit bcbcac7) # Conflicts: # ur_robot_driver/test/integration_test_passthrough_controller.py # ur_robot_driver/test/integration_test_scaled_joint_controller.py
1 parent 07902d7 commit 44cb860

File tree

3 files changed

+511
-0
lines changed

3 files changed

+511
-0
lines changed

ur_robot_driver/test/integration_test_force_mode.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ def init_robot(self):
114114
FollowJointTrajectory,
115115
)
116116

117+
self._controller_manager_interface.wait_for_controller("force_mode_controller")
118+
self._controller_manager_interface.wait_for_controller("scaled_joint_trajectory_controller")
119+
self._controller_manager_interface.wait_for_controller("joint_trajectory_controller")
120+
117121
def setUp(self):
118122
self._dashboard_interface.start_robot()
119123
time.sleep(1)
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
#!/usr/bin/env python
2+
# Copyright 2025, Universal Robots A/S
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
#
7+
# * Redistributions of source code must retain the above copyright
8+
# notice, this list of conditions and the following disclaimer.
9+
#
10+
# * Redistributions in binary form must reproduce the above copyright
11+
# notice, this list of conditions and the following disclaimer in the
12+
# documentation and/or other materials provided with the distribution.
13+
#
14+
# * Neither the name of the {copyright_holder} nor the names of its
15+
# contributors may be used to endorse or promote products derived from
16+
# this software without specific prior written permission.
17+
#
18+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
# POSSIBILITY OF SUCH DAMAGE.
29+
30+
import os
31+
import sys
32+
import time
33+
import unittest
34+
35+
from math import pi
36+
import launch_testing
37+
import pytest
38+
import rclpy
39+
from builtin_interfaces.msg import Duration
40+
from control_msgs.action import FollowJointTrajectory
41+
from control_msgs.msg import JointTolerance
42+
from controller_manager_msgs.srv import SwitchController
43+
from rclpy.node import Node
44+
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
45+
46+
sys.path.append(os.path.dirname(__file__))
47+
from test_common import ( # noqa: E402
48+
ActionInterface,
49+
ControllerManagerInterface,
50+
DashboardInterface,
51+
IoStatusInterface,
52+
generate_driver_test_description,
53+
ROBOT_JOINTS,
54+
TIMEOUT_EXECUTE_TRAJECTORY,
55+
)
56+
57+
58+
# Mock hardware does not work with passthrough controller, so dont test with it
59+
@pytest.mark.launch_test
60+
@launch_testing.parametrize("tf_prefix", [(""), ("my_ur_")])
61+
def generate_test_description(tf_prefix):
62+
return generate_driver_test_description(tf_prefix=tf_prefix)
63+
64+
65+
HOME = {
66+
"elbow_joint": 0.0,
67+
"shoulder_lift_joint": -1.5708,
68+
"shoulder_pan_joint": 0.0,
69+
"wrist_1_joint": -1.5708,
70+
"wrist_2_joint": 0.0,
71+
"wrist_3_joint": 0.0,
72+
}
73+
waypts = [[HOME[joint] + i * pi / 4 for joint in ROBOT_JOINTS] for i in [0, -1, 1]]
74+
time_vec = [
75+
Duration(sec=4, nanosec=0),
76+
Duration(sec=8, nanosec=0),
77+
Duration(sec=12, nanosec=0),
78+
]
79+
TEST_TRAJECTORY = [(time_vec[i], waypts[i]) for i in range(len(waypts))]
80+
81+
82+
class PassthroughControllerTest(unittest.TestCase):
83+
@classmethod
84+
def setUpClass(cls):
85+
# Initialize the ROS context
86+
rclpy.init()
87+
cls.node = Node("passthrough_controller_test")
88+
time.sleep(1)
89+
cls.init_robot(cls)
90+
91+
@classmethod
92+
def tearDownClass(cls):
93+
# Shutdown the ROS context
94+
cls.node.destroy_node()
95+
rclpy.shutdown()
96+
97+
def init_robot(self):
98+
99+
self._dashboard_interface = DashboardInterface(self.node)
100+
101+
self._controller_manager_interface = ControllerManagerInterface(self.node)
102+
self._io_status_controller_interface = IoStatusInterface(self.node)
103+
104+
self._passthrough_forward_joint_trajectory = ActionInterface(
105+
self.node,
106+
"/passthrough_trajectory_controller/follow_joint_trajectory",
107+
FollowJointTrajectory,
108+
)
109+
110+
# Wait for all controllers needed below, as controller manager services might fail
111+
# e.g. when attempting to deactivate an unknown controller
112+
self._controller_manager_interface.wait_for_controller("scaled_joint_trajectory_controller")
113+
114+
def setUp(self):
115+
self._dashboard_interface.start_robot()
116+
time.sleep(1)
117+
self.assertTrue(self._io_status_controller_interface.resend_robot_program().success)
118+
119+
#
120+
# Test functions
121+
#
122+
123+
def test_start_passthrough_controller(self):
124+
self.assertTrue(
125+
self._controller_manager_interface.switch_controller(
126+
strictness=SwitchController.Request.BEST_EFFORT,
127+
activate_controllers=["passthrough_trajectory_controller"],
128+
deactivate_controllers=["scaled_joint_trajectory_controller"],
129+
).ok
130+
)
131+
132+
def test_passthrough_trajectory(self, tf_prefix):
133+
self.assertTrue(
134+
self._controller_manager_interface.switch_controller(
135+
strictness=SwitchController.Request.BEST_EFFORT,
136+
activate_controllers=["passthrough_trajectory_controller"],
137+
deactivate_controllers=["scaled_joint_trajectory_controller"],
138+
).ok
139+
)
140+
141+
goal_tolerance = [
142+
JointTolerance(position=0.01, name=tf_prefix + joint) for joint in ROBOT_JOINTS
143+
]
144+
goal_time_tolerance = Duration(sec=1, nanosec=0)
145+
trajectory = JointTrajectory(
146+
points=[
147+
JointTrajectoryPoint(positions=pos, time_from_start=times)
148+
for (times, pos) in TEST_TRAJECTORY
149+
],
150+
joint_names=[tf_prefix + joint for joint in ROBOT_JOINTS],
151+
)
152+
goal_handle = self._passthrough_forward_joint_trajectory.send_goal(
153+
trajectory=trajectory,
154+
goal_time_tolerance=goal_time_tolerance,
155+
goal_tolerance=goal_tolerance,
156+
)
157+
self.assertTrue(goal_handle.accepted)
158+
if goal_handle.accepted:
159+
result = self._passthrough_forward_joint_trajectory.get_result(
160+
goal_handle, TIMEOUT_EXECUTE_TRAJECTORY
161+
)
162+
self.assertEqual(result.error_code, FollowJointTrajectory.Result.SUCCESSFUL)
163+
164+
def test_quintic_trajectory(self, tf_prefix):
165+
# Full quintic trajectory
166+
self.assertTrue(
167+
self._controller_manager_interface.switch_controller(
168+
strictness=SwitchController.Request.BEST_EFFORT,
169+
activate_controllers=["passthrough_trajectory_controller"],
170+
deactivate_controllers=["scaled_joint_trajectory_controller"],
171+
).ok
172+
)
173+
trajectory = JointTrajectory(
174+
points=[
175+
JointTrajectoryPoint(
176+
positions=pos,
177+
time_from_start=times,
178+
velocities=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
179+
accelerations=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
180+
)
181+
for (times, pos) in TEST_TRAJECTORY
182+
],
183+
joint_names=[tf_prefix + joint for joint in ROBOT_JOINTS],
184+
)
185+
goal_time_tolerance = Duration(sec=1, nanosec=0)
186+
goal_tolerance = [
187+
JointTolerance(position=0.01, name=tf_prefix + joint) for joint in ROBOT_JOINTS
188+
]
189+
goal_handle = self._passthrough_forward_joint_trajectory.send_goal(
190+
trajectory=trajectory,
191+
goal_time_tolerance=goal_time_tolerance,
192+
goal_tolerance=goal_tolerance,
193+
)
194+
195+
self.assertTrue(goal_handle.accepted)
196+
if goal_handle.accepted:
197+
result = self._passthrough_forward_joint_trajectory.get_result(
198+
goal_handle, TIMEOUT_EXECUTE_TRAJECTORY
199+
)
200+
self.assertEqual(result.error_code, FollowJointTrajectory.Result.SUCCESSFUL)
201+
202+
def test_impossible_goal_tolerance_fails(self, tf_prefix):
203+
# Test impossible goal tolerance, should fail.
204+
self.assertTrue(
205+
self._controller_manager_interface.switch_controller(
206+
strictness=SwitchController.Request.BEST_EFFORT,
207+
activate_controllers=["passthrough_trajectory_controller"],
208+
deactivate_controllers=["scaled_joint_trajectory_controller"],
209+
).ok
210+
)
211+
trajectory = JointTrajectory(
212+
points=[
213+
JointTrajectoryPoint(positions=pos, time_from_start=times)
214+
for (times, pos) in TEST_TRAJECTORY
215+
],
216+
joint_names=[tf_prefix + joint for joint in ROBOT_JOINTS],
217+
)
218+
goal_tolerance = [
219+
JointTolerance(position=0.000000001, name=tf_prefix + joint) for joint in ROBOT_JOINTS
220+
]
221+
goal_time_tolerance = Duration(sec=1, nanosec=0)
222+
goal_handle = self._passthrough_forward_joint_trajectory.send_goal(
223+
trajectory=trajectory,
224+
goal_time_tolerance=goal_time_tolerance,
225+
goal_tolerance=goal_tolerance,
226+
)
227+
self.assertTrue(goal_handle.accepted)
228+
if goal_handle.accepted:
229+
result = self._passthrough_forward_joint_trajectory.get_result(
230+
goal_handle, TIMEOUT_EXECUTE_TRAJECTORY
231+
)
232+
self.assertEqual(
233+
result.error_code, FollowJointTrajectory.Result.GOAL_TOLERANCE_VIOLATED
234+
)
235+
236+
def test_impossible_goal_time_tolerance_fails(self, tf_prefix):
237+
# Test impossible goal time
238+
self.assertTrue(
239+
self._controller_manager_interface.switch_controller(
240+
strictness=SwitchController.Request.BEST_EFFORT,
241+
activate_controllers=["passthrough_trajectory_controller"],
242+
deactivate_controllers=["scaled_joint_trajectory_controller"],
243+
).ok
244+
)
245+
246+
goal_tolerance = [
247+
JointTolerance(position=0.01, name=tf_prefix + joint) for joint in ROBOT_JOINTS
248+
]
249+
goal_time_tolerance = Duration(sec=0, nanosec=10)
250+
trajectory = JointTrajectory(
251+
points=[
252+
JointTrajectoryPoint(positions=pos, time_from_start=times)
253+
for (times, pos) in TEST_TRAJECTORY
254+
],
255+
joint_names=[tf_prefix + joint for joint in ROBOT_JOINTS],
256+
)
257+
goal_handle = self._passthrough_forward_joint_trajectory.send_goal(
258+
trajectory=trajectory,
259+
goal_time_tolerance=goal_time_tolerance,
260+
goal_tolerance=goal_tolerance,
261+
)
262+
self.assertTrue(goal_handle.accepted)
263+
if goal_handle.accepted:
264+
result = self._passthrough_forward_joint_trajectory.get_result(
265+
goal_handle, TIMEOUT_EXECUTE_TRAJECTORY
266+
)
267+
self.assertEqual(
268+
result.error_code, FollowJointTrajectory.Result.GOAL_TOLERANCE_VIOLATED
269+
)
270+
self.assertTrue(
271+
self._controller_manager_interface.switch_controller(
272+
strictness=SwitchController.Request.BEST_EFFORT,
273+
deactivate_controllers=["passthrough_trajectory_controller"],
274+
activate_controllers=["scaled_joint_trajectory_controller"],
275+
).ok
276+
)

0 commit comments

Comments
 (0)