Skip to content

Commit ee5e57b

Browse files
authored
Trajectory until node (backport of #1461) (#1522)
The trajectory until node allows a user to execute a trajectory with an "until" condition enabled (currently only tool contact is available) without having to call 2 actions at the same time.
1 parent 4008da7 commit ee5e57b

File tree

9 files changed

+898
-7
lines changed

9 files changed

+898
-7
lines changed

ur_robot_driver/CMakeLists.txt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,27 @@ add_executable(urscript_interface
126126
)
127127
ament_target_dependencies(urscript_interface ${${PROJECT_NAME}_EXPORTED_TARGETS} ${THIS_PACKAGE_INCLUDE_DEPENDS})
128128

129+
#
130+
# trajectory_until_node
131+
#
132+
add_executable(trajectory_until_node
133+
src/trajectory_until_node.cpp
134+
)
135+
target_link_libraries(trajectory_until_node PUBLIC
136+
rclcpp::rclcpp
137+
${std_msgs_TARGETS}
138+
rclcpp_action::rclcpp_action
139+
${ur_msgs_TARGETS}
140+
)
141+
129142
install(
130-
TARGETS dashboard_client ur_ros2_control_node controller_stopper_node urscript_interface robot_state_helper
143+
TARGETS
144+
controller_stopper_node
145+
dashboard_client
146+
robot_state_helper
147+
trajectory_until_node
148+
ur_ros2_control_node
149+
urscript_interface
131150
DESTINATION lib/${PROJECT_NAME}
132151
)
133152

@@ -177,6 +196,7 @@ install(PROGRAMS
177196
scripts/start_ursim.sh
178197
examples/examples.py
179198
examples/force_mode.py
199+
examples/move_until_example.py
180200
DESTINATION lib/${PROJECT_NAME}
181201
)
182202

@@ -246,5 +266,9 @@ if(BUILD_TESTING)
246266
TIMEOUT
247267
800
248268
)
269+
add_launch_test(test/integration_test_trajectory_until.py
270+
TIMEOUT
271+
800
272+
)
249273
endif()
250274
endif()

ur_robot_driver/doc/index.rst

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,4 @@ Welcome to ur_robot_driver's documentation!
1818
generated/index
1919
robot_state_helper
2020
controller_stopper
21-
22-
23-
Indices and tables
24-
==================
25-
26-
* :ref:`genindex`
21+
trajectory_until_node
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.. _trajectory_until_node:
2+
3+
Trajectory until node
4+
=====================
5+
6+
The trajectory until node allows a user to execute a trajectory with an "until" condition enabled (currently only tool contact is available) without having to call 2 actions at the same time. This means that the trajectory will execute until either the trajectory is finished or the "until" condition has been triggered. Both scenarios will result in the trajectory being reported as successful.
7+
8+
Action interface / usage
9+
""""""""""""""""""""""""
10+
The node provides an action to execute a trajectory with tool contact enabled. For the node to accept action goals, both the motion controller and the tool contact controller need to be in ``active`` state.
11+
12+
* ``/trajectory_until_node/execute [ur_msgs/action/TrajectoryUntil]``
13+
14+
The action contains all the same fields as the ordinary `FollowJointTrajectory <http://docs.ros.org/en/noetic/api/control_msgs/html/action/FollowJointTrajectory.html>`_ action, but has two additional fields.
15+
One in the goal section called ``until_type``, which is used to choose between different conditions that can stop the trajectory. Currently only tool contact is available.
16+
The result section contains the other new field called ``until_condition_result``, which reports whether the chosen condition was triggered or not.
17+
18+
Implementation details
19+
""""""""""""""""""""""
20+
Upon instantiation of the node, the internal trajectory action client will connect to an action named ``motion_controller/follow_joint_trajectory``.
21+
This action does not exist, but upon launch of the driver, the node is remapped to connect to the ``initial_joint_controller``, default is ``scaled_joint_trajectory_controller``.
22+
If you wish to use the node with another motion controller use the launch argument ``initial_joint_controller:=<your_motion_controller>`` when launching the driver.
23+
The node is only compatible with motion controllers that use the FollowJointTrajectory action interface.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env python3
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 time
31+
32+
import rclpy
33+
from rclpy.action import ActionClient
34+
35+
from builtin_interfaces.msg import Duration
36+
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
37+
from ur_msgs.action import FollowJointTrajectoryUntil
38+
39+
ROBOT_JOINTS = [
40+
"elbow_joint",
41+
"shoulder_lift_joint",
42+
"shoulder_pan_joint",
43+
"wrist_1_joint",
44+
"wrist_2_joint",
45+
"wrist_3_joint",
46+
]
47+
48+
49+
class MoveUntilExample(rclpy.node.Node):
50+
def __init__(self):
51+
super().__init__("move_until_example")
52+
53+
self._send_goal_future = None
54+
self._get_result_future = None
55+
self._goal_handle = None
56+
self._action_client = ActionClient(
57+
self, FollowJointTrajectoryUntil, "/trajectory_until_node/execute"
58+
)
59+
self._action_client.wait_for_server()
60+
self.test_traj = {
61+
"waypts": [[1.5, -1.5, 0.0, -1.5, -1.5, -1.5], [2.1, -1.2, 0.0, -2.4, -1.5, -1.5]],
62+
"time_vec": [Duration(sec=3, nanosec=0), Duration(sec=6, nanosec=0)],
63+
}
64+
self.get_logger().info("Initialized")
65+
66+
def cancel_goal(self):
67+
if self._goal_handle is not None:
68+
self.get_logger().info("Cancelling goal")
69+
future = self._goal_handle.cancel_goal_async()
70+
future.add_done_callback(self.cancel_done)
71+
72+
def cancel_done(self, future):
73+
try:
74+
future.result()
75+
self.get_logger().info("Goal cancelled successfully")
76+
except Exception as e:
77+
self.get_logger().error(f"Failed to cancel goal: {e}")
78+
79+
def process(self):
80+
trajectory = JointTrajectory()
81+
trajectory.joint_names = ROBOT_JOINTS
82+
83+
trajectory.points = [
84+
JointTrajectoryPoint(
85+
positions=self.test_traj["waypts"][i], time_from_start=self.test_traj["time_vec"][i]
86+
)
87+
for i in range(len(self.test_traj["waypts"]))
88+
]
89+
goal = FollowJointTrajectoryUntil.Goal(
90+
trajectory=trajectory, until_type=FollowJointTrajectoryUntil.Goal.TOOL_CONTACT
91+
)
92+
self._send_goal_future = self._action_client.send_goal_async(goal)
93+
rclpy.spin_until_future_complete(self, self._send_goal_future)
94+
self._goal_handle = self._send_goal_future.result()
95+
if not self._goal_handle.accepted:
96+
self.get_logger().error("Goal rejected :(")
97+
raise RuntimeError("Goal rejected :(")
98+
99+
self.get_logger().info(
100+
f"Goal accepted with ID: {bytes(self._goal_handle.goal_id.uuid).hex()}\n"
101+
)
102+
103+
result_future = self._goal_handle.get_result_async()
104+
rclpy.spin_until_future_complete(self, result_future)
105+
result = result_future.result().result
106+
print(result)
107+
if result is None:
108+
self.get_logger().error("Result is None")
109+
return
110+
if result.error_code != FollowJointTrajectoryUntil.Result.SUCCESSFUL:
111+
self.get_logger().error(f"Result error code: {result.error_code}")
112+
return
113+
if result.until_condition_result == FollowJointTrajectoryUntil.Result.NOT_TRIGGERED:
114+
self.get_logger().info("Trajectory executed without tool contact trigger")
115+
else:
116+
self.get_logger().info("Trajectory executed with tool contact trigger")
117+
118+
self.get_logger().info("Trajectory executed successfully with tool contact condition")
119+
120+
121+
if __name__ == "__main__":
122+
rclpy.init()
123+
124+
node = MoveUntilExample()
125+
try:
126+
node.process()
127+
except KeyboardInterrupt:
128+
node.get_logger().info("Interrupted")
129+
node.cancel_goal()
130+
time.sleep(2)
131+
132+
rclpy.shutdown()
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2025, Universal Robots A/S
2+
//
3+
// Redistribution and use in source and binary forms, with or without
4+
// modification, are permitted provided that the following conditions are met:
5+
//
6+
// * Redistributions of source code must retain the above copyright
7+
// notice, this list of conditions and the following disclaimer.
8+
//
9+
// * Redistributions in binary form must reproduce the above copyright
10+
// notice, this list of conditions and the following disclaimer in the
11+
// documentation and/or other materials provided with the distribution.
12+
//
13+
// * Neither the name of the {copyright_holder} nor the names of its
14+
// contributors may be used to endorse or promote products derived from
15+
// this software without specific prior written permission.
16+
//
17+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21+
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
// POSSIBILITY OF SUCH DAMAGE.
28+
29+
//----------------------------------------------------------------------
30+
/*!\file
31+
*
32+
* \author Jacob Larsen [email protected]
33+
* \date 2025-01-17
34+
*
35+
*
36+
*
37+
*
38+
*/
39+
//----------------------------------------------------------------------
40+
41+
#ifndef UR_ROBOT_DRIVER__TRAJECTORY_UNTIL_NODE_HPP_
42+
#define UR_ROBOT_DRIVER__TRAJECTORY_UNTIL_NODE_HPP_
43+
44+
#include <memory>
45+
#include <string>
46+
#include <variant>
47+
48+
#include <rclcpp/rclcpp.hpp>
49+
#include <std_msgs/msg/bool.hpp>
50+
#include <rclcpp_action/server.hpp>
51+
#include <rclcpp_action/rclcpp_action.hpp>
52+
#include <rclcpp_action/create_server.hpp>
53+
#include <rclcpp_action/server_goal_handle.hpp>
54+
#include <rclcpp/duration.hpp>
55+
#include <ur_msgs/action/tool_contact.hpp>
56+
#include <ur_msgs/action/follow_joint_trajectory_until.hpp>
57+
#include <control_msgs/action/follow_joint_trajectory.hpp>
58+
59+
namespace ur_robot_driver
60+
{
61+
class TrajectoryUntilNode : public rclcpp::Node
62+
{
63+
public:
64+
explicit TrajectoryUntilNode(const rclcpp::NodeOptions& options = rclcpp::NodeOptions());
65+
~TrajectoryUntilNode();
66+
67+
private:
68+
using TrajectoryUntilAction = ur_msgs::action::FollowJointTrajectoryUntil;
69+
70+
rclcpp_action::Server<TrajectoryUntilAction>::SharedPtr action_server_;
71+
rclcpp_action::Client<control_msgs::action::FollowJointTrajectory>::SharedPtr trajectory_action_client_;
72+
std::string trajectory_action_uri_;
73+
74+
// Add new until types here, when available
75+
using TCClient = rclcpp_action::Client<ur_msgs::action::ToolContact>::SharedPtr;
76+
std::variant<TCClient> until_action_client_variant;
77+
std::string until_action_uri_;
78+
79+
rclcpp::CallbackGroup::SharedPtr server_callback_group;
80+
rclcpp::CallbackGroup::SharedPtr clients_callback_group;
81+
82+
std::shared_ptr<rclcpp_action::ServerGoalHandle<TrajectoryUntilAction>> server_goal_handle_;
83+
84+
template <class ActionType, class ClientType>
85+
void send_until_goal(std::shared_ptr<const TrajectoryUntilAction::Goal> goal);
86+
87+
template <class T>
88+
void until_response_callback(const typename rclcpp_action::ClientGoalHandle<T>::SharedPtr& goal_handle);
89+
90+
template <class T>
91+
void until_result_callback(const typename rclcpp_action::ClientGoalHandle<T>::WrappedResult& result);
92+
93+
void cancel_until_goal();
94+
95+
void trajectory_response_callback(
96+
const rclcpp_action::ClientGoalHandle<control_msgs::action::FollowJointTrajectory>::SharedPtr& goal_handle);
97+
void trajectory_feedback_callback(
98+
const rclcpp_action::ClientGoalHandle<control_msgs::action::FollowJointTrajectory>::SharedPtr& goal_handle,
99+
const std::shared_ptr<const control_msgs::action::FollowJointTrajectory::Feedback> feedback);
100+
void trajectory_result_callback(
101+
const rclcpp_action::ClientGoalHandle<control_msgs::action::FollowJointTrajectory>::WrappedResult& result);
102+
103+
bool assign_until_action_client(std::shared_ptr<const TrajectoryUntilAction::Goal> goal);
104+
105+
rclcpp_action::GoalResponse goal_received_callback(const rclcpp_action::GoalUUID& uuid,
106+
std::shared_ptr<const TrajectoryUntilAction::Goal> goal);
107+
void
108+
goal_accepted_callback(const std::shared_ptr<rclcpp_action::ServerGoalHandle<TrajectoryUntilAction>> goal_handle);
109+
rclcpp_action::CancelResponse
110+
goal_cancelled_callback(const std::shared_ptr<rclcpp_action::ServerGoalHandle<TrajectoryUntilAction>> goal_handle);
111+
112+
void send_trajectory_goal(std::shared_ptr<const TrajectoryUntilAction::Goal> goal);
113+
114+
void report_goal(rclcpp_action::ClientGoalHandle<control_msgs::action::FollowJointTrajectory>::WrappedResult result);
115+
116+
template <typename UntilResult>
117+
void report_goal(UntilResult result);
118+
119+
void reset_node();
120+
121+
rclcpp_action::ClientGoalHandle<control_msgs::action::FollowJointTrajectory>::SharedPtr
122+
current_trajectory_goal_handle_;
123+
rclcpp_action::ClientGoalHandle<ur_msgs::action::ToolContact>::SharedPtr current_until_goal_handle_;
124+
125+
std::shared_ptr<TrajectoryUntilAction::Result> prealloc_res_;
126+
127+
std::shared_ptr<TrajectoryUntilAction::Feedback> prealloc_fb_;
128+
129+
std::atomic<bool> trajectory_accepted_;
130+
std::atomic<bool> until_accepted_;
131+
132+
std::condition_variable cv_until_;
133+
std::condition_variable cv_trajectory_;
134+
std::mutex mutex_until;
135+
std::mutex mutex_trajectory;
136+
};
137+
138+
} // namespace ur_robot_driver
139+
140+
#endif // UR_ROBOT_DRIVER__TRAJECTORY_UNTIL_NODE_HPP_

ur_robot_driver/launch/ur_control.launch.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,19 @@ def launch_setup(context, *args, **kwargs):
330330
arguments=["-d", rviz_config_file],
331331
)
332332

333+
trajectory_until_node = Node(
334+
package="ur_robot_driver",
335+
executable="trajectory_until_node",
336+
name="trajectory_until_node",
337+
output="screen",
338+
parameters=[
339+
{
340+
"motion_controller_uri": f"/{initial_joint_controller.perform(context)}/follow_joint_trajectory",
341+
"until_action_uri": "tool_contact_controller/detect_tool_contact",
342+
},
343+
],
344+
)
345+
333346
# Spawn controllers
334347
def controller_spawner(controllers, active=True):
335348
inactive_flags = ["--inactive"] if not active else []
@@ -386,6 +399,7 @@ def controller_spawner(controllers, active=True):
386399
urscript_interface,
387400
robot_state_publisher_node,
388401
rviz_node,
402+
trajectory_until_node,
389403
] + controller_spawners
390404

391405
return nodes_to_start

0 commit comments

Comments
 (0)