Skip to content

Commit 7f6ae90

Browse files
authored
Migrate Urscript interface to primary client (#1833)
* Switch to primary client and implement action server * Documentation of action server * Update ur_robot_driver/doc/usage/script_code.rst
1 parent 08bab9a commit 7f6ae90

4 files changed

Lines changed: 295 additions & 34 deletions

File tree

ur_robot_driver/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ add_executable(urscript_interface
151151
)
152152
target_link_libraries(urscript_interface PUBLIC
153153
rclcpp::rclcpp
154+
rclcpp_action::rclcpp_action
154155
${std_msgs_TARGETS}
156+
${ur_msgs_TARGETS}
157+
rclcpp::rclcpp
155158
ur_client_library::urcl
156159
)
157160

ur_robot_driver/doc/usage/script_code.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,51 @@ To prevent interrupting the main program, you can send certain commands as `seco
6565
textmsg(\"This is a log message\")
6666
6767
end"}'
68+
69+
Scripts with execution monitoring
70+
---------------------------------
71+
The action server at ``/urscript_interface/execute_script`` allows executing script programs and getting the information when execution is done and whether there was an error during execution.
72+
73+
The ``SendScript`` action definition can be seen in the `ur_msgs action definitions <https://docs.ros.org/en/rolling/p/ur_msgs/__action_definitions.html/>`_
74+
75+
This action server is a ROS wrapper around the `URCL primary client's <https://github.com/UniversalRobots/Universal_Robots_Client_Library/blob/master/doc/architecture/primary_client.rst/>`_
76+
SendScriptBlocking method, and the meaning of parameters can be seen there.
77+
The action will be reported as successful if no errors occur during script execution. See :ref:`script-cancel` for additional info about when the action might be reported as successful.
78+
If an error is detected, a message explaining what went wrong will be made available in the action result.
79+
While the action server is waiting for a script to finish execution, it will reject any further action goals, and the ``/urscript_interface/script_command`` topic will also be ignored during this time.
80+
81+
.. note::
82+
The previous note about restarting the external control URCap also applies when using the action server.
83+
84+
.. _script-cancel:
85+
86+
Script cancellation
87+
^^^^^^^^^^^^^^^^^^^
88+
If script execution is canceled through the ROS2 action interface, the running script will be stopped, and the action will be reported as canceled. If script execution is stopped via the teach pendant or **any** other non-ROS means, the corresponding ROS action will be reported as successful, as the action server can not distinguish between successful programs and programs that were stopped outside of ROS.
89+
90+
Command line example
91+
^^^^^^^^^^^^^^^^^^^^
92+
The action server can be called from the command line like this:
93+
94+
.. code-block:: bash
95+
96+
ros2 action send_goal /urscript_interface/execute_script ur_msgs/action/SendScript '{
97+
program: "popup(\"cmd line example\")",
98+
script_name: "cmd_line_example",
99+
start_timeout: {sec: 1.0, nanosec: 0},
100+
fail_on_warnings: true}'
101+
102+
Parameters
103+
^^^^^^^^^^
104+
- ``robot_ip`` (string, **required**)
105+
106+
The IP address at which the robot is reachable.
107+
108+
- ``retry_on_readonly_interface`` (boolean, default: ``true``)
109+
110+
When the client is currently connected to a read-only interface (because the robot is not in
111+
remote_control mode / was put to local control mode since the client connected) script code will
112+
not be accepted by the primary interface.
113+
114+
When set to ``true`` the client will attempt to reconnect to the primary interface and send the
115+
script again once. This mechanism is only active when using the action interface.

ur_robot_driver/src/urscript_interface.cpp

Lines changed: 155 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,53 +35,174 @@
3535
*/
3636
//----------------------------------------------------------------------
3737

38-
#include <ur_client_library/comm/stream.h>
39-
#include <ur_client_library/primary/primary_package.h>
38+
#include <ur_client_library/primary/primary_client.h>
4039

41-
#include <memory>
40+
#include <chrono>
41+
#include <thread>
42+
43+
#include <ur_msgs/action/send_script.hpp>
4244

4345
#include <rclcpp/rclcpp.hpp>
46+
#include "rclcpp_action/rclcpp_action.hpp"
4447
#include <std_msgs/msg/string.hpp>
4548

49+
using SendScript = ur_msgs::action::SendScript;
50+
4651
class URScriptInterface : public rclcpp::Node
4752
{
4853
public:
49-
URScriptInterface()
50-
: Node("urscript_interface")
51-
, m_script_sub(this->create_subscription<std_msgs::msg::String>(
52-
"~/script_command", 1, [this](const std_msgs::msg::String::SharedPtr msg) {
53-
auto program_with_newline = msg->data + '\n';
54-
55-
RCLCPP_INFO_STREAM(this->get_logger(), program_with_newline);
56-
57-
size_t len = program_with_newline.size();
58-
const auto* data = reinterpret_cast<const uint8_t*>(program_with_newline.c_str());
59-
size_t written;
60-
61-
if (m_secondary_stream->write(data, len, written)) {
62-
URCL_LOG_INFO("Sent program to robot:\n%s", program_with_newline.c_str());
63-
return true;
64-
}
65-
URCL_LOG_ERROR("Could not send program to robot");
66-
return false;
67-
}))
54+
URScriptInterface() : Node("urscript_interface")
6855
{
6956
this->declare_parameter("robot_ip", rclcpp::PARAMETER_STRING);
70-
m_secondary_stream = std::make_unique<urcl::comm::URStream<urcl::primary_interface::PrimaryPackage>>(
71-
this->get_parameter("robot_ip").as_string(), urcl::primary_interface::UR_SECONDARY_PORT);
72-
m_secondary_stream->connect();
73-
74-
auto program_with_newline = std::string("sec urscript_interface_initialization:\ntextmsg(\"urscript_interface "
75-
"connected\")\nend\n");
76-
size_t len = program_with_newline.size();
77-
const auto* data = reinterpret_cast<const uint8_t*>(program_with_newline.c_str());
78-
size_t written;
79-
m_secondary_stream->write(data, len, written);
57+
this->declare_parameter("retry_on_readonly_interface", true);
58+
59+
primary_client_ =
60+
std::make_unique<urcl::primary_interface::PrimaryClient>(this->get_parameter("robot_ip").as_string(), notif_);
61+
62+
primary_client_->start(10, std::chrono::seconds(10));
63+
64+
script_sub_ = create_subscription<std_msgs::msg::String>(
65+
"~/script_command", 1, std::bind(&URScriptInterface::script_callback, this, std::placeholders::_1));
66+
67+
send_script_server_ = rclcpp_action::create_server<SendScript>(
68+
this, "~/execute_script",
69+
std::bind(&URScriptInterface::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
70+
std::bind(&URScriptInterface::handle_cancel, this, std::placeholders::_1),
71+
std::bind(&URScriptInterface::handle_accepted, this, std::placeholders::_1));
72+
}
73+
74+
~URScriptInterface() override
75+
{
76+
if (execute_thread_.joinable()) {
77+
execute_thread_.join();
78+
}
8079
}
8180

8281
private:
83-
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr m_script_sub;
84-
std::unique_ptr<urcl::comm::URStream<urcl::primary_interface::PrimaryPackage>> m_secondary_stream;
82+
// Use non-blocking interface
83+
bool script_callback(const std_msgs::msg::String::SharedPtr msg)
84+
{
85+
if (primary_client_ == nullptr) {
86+
RCLCPP_ERROR(get_logger(), "Primary client not initialized yet");
87+
return false;
88+
}
89+
if (busy) {
90+
RCLCPP_ERROR(get_logger(), "Script interface is executing action, ignoring topic");
91+
return false;
92+
}
93+
if (primary_client_->sendScript(msg->data)) {
94+
URCL_LOG_INFO("Sent program to robot:\n%s", msg->data.c_str());
95+
return true;
96+
} else {
97+
URCL_LOG_ERROR("Could not send program to robot");
98+
return false;
99+
}
100+
}
101+
102+
rclcpp_action::GoalResponse handle_goal(const rclcpp_action::GoalUUID& uuid,
103+
std::shared_ptr<const SendScript::Goal> goal)
104+
{
105+
if (primary_client_ == nullptr) {
106+
RCLCPP_ERROR(get_logger(), "Primary client not initialized yet");
107+
return rclcpp_action::GoalResponse::REJECT;
108+
}
109+
110+
if (busy) {
111+
RCLCPP_ERROR(get_logger(), "Action server is busy");
112+
return rclcpp_action::GoalResponse::REJECT;
113+
}
114+
busy = true;
115+
116+
if (execute_thread_.joinable()) {
117+
// Should return immediately, as we are not busy
118+
execute_thread_.join();
119+
}
120+
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
121+
}
122+
123+
rclcpp_action::CancelResponse
124+
handle_cancel(const std::shared_ptr<rclcpp_action::ServerGoalHandle<SendScript>> goal_handle)
125+
{
126+
if (goal_handle->is_executing()) {
127+
try {
128+
primary_client_->commandStop();
129+
} catch (const urcl::UrException& exc) {
130+
RCLCPP_ERROR(get_logger(), "Caught UR exception while trying to cancel goal:");
131+
RCLCPP_ERROR(get_logger(), exc.what());
132+
} catch (const std::exception& exc) {
133+
RCLCPP_ERROR(get_logger(), "Caught unexpected exception while trying to cancel goal:");
134+
RCLCPP_ERROR(get_logger(), exc.what());
135+
}
136+
return rclcpp_action::CancelResponse::ACCEPT;
137+
} else {
138+
RCLCPP_ERROR(get_logger(), "Received cancel request for inactive goal, rejecting");
139+
return rclcpp_action::CancelResponse::REJECT;
140+
}
141+
}
142+
143+
void handle_accepted(const std::shared_ptr<rclcpp_action::ServerGoalHandle<SendScript>> goal_handle)
144+
{
145+
execute_thread_ = std::thread([this, goal_handle]() { this->execute(goal_handle); });
146+
}
147+
148+
void execute(const std::shared_ptr<rclcpp_action::ServerGoalHandle<SendScript>> goal_handle)
149+
{
150+
auto goal = goal_handle->get_goal();
151+
auto timeout = goal->start_timeout;
152+
auto chrono_timeout = std::chrono::duration_cast<std::chrono::milliseconds>(
153+
std::chrono::seconds(timeout.sec) + std::chrono::nanoseconds(timeout.nanosec));
154+
// A ROS duration can be negative, would not make sense here
155+
if (chrono_timeout < std::chrono::milliseconds(0)) {
156+
chrono_timeout = std::chrono::milliseconds(0);
157+
}
158+
159+
try {
160+
primary_client_->sendScriptBlocking(goal->program, goal->script_name, chrono_timeout, goal->fail_on_warnings,
161+
this->get_parameter("retry_on_readonly_interface").as_bool());
162+
}
163+
164+
catch (const urcl::UrException& exc) {
165+
RCLCPP_ERROR(get_logger(), "Script did not execute successfully. Error message:");
166+
RCLCPP_ERROR(get_logger(), exc.what());
167+
auto res = std::make_shared<SendScript::Result>();
168+
res->success = false;
169+
res->message = exc.what();
170+
goal_handle->abort(res);
171+
busy = false;
172+
return;
173+
}
174+
175+
catch (const std::exception& exc) {
176+
RCLCPP_ERROR(get_logger(), "Unexpected exception caught during script execution:");
177+
RCLCPP_ERROR(get_logger(), exc.what());
178+
auto res = std::make_shared<SendScript::Result>();
179+
res->success = false;
180+
res->message = exc.what();
181+
goal_handle->abort(res);
182+
busy = false;
183+
return;
184+
}
185+
186+
auto res = std::make_shared<SendScript::Result>();
187+
if (goal_handle->is_canceling()) {
188+
res->success = false;
189+
res->message = "Script execution canceled";
190+
goal_handle->canceled(res);
191+
} else {
192+
res->success = true;
193+
res->message = "Script executed successfully";
194+
goal_handle->succeed(res);
195+
}
196+
busy = false;
197+
return;
198+
}
199+
200+
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr script_sub_;
201+
rclcpp_action::Server<SendScript>::SharedPtr send_script_server_;
202+
std::unique_ptr<urcl::primary_interface::PrimaryClient> primary_client_;
203+
urcl::comm::INotifier notif_;
204+
std::atomic<bool> busy = false;
205+
std::thread execute_thread_;
85206
};
86207

87208
int main(int argc, char** argv)

ur_robot_driver/test/urscript_interface.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,12 @@
3434
import pytest
3535
import rclpy
3636
import rclpy.node
37+
from rclpy.action import ActionClient
38+
from builtin_interfaces.msg import Duration
3739
from std_msgs.msg import String as StringMsg
3840
from ur_msgs.msg import IOStates
41+
from ur_msgs.action import SendScript
42+
from action_msgs.srv import CancelGoal_Response
3943

4044
sys.path.append(os.path.dirname(__file__))
4145
from test_common import ( # noqa: E402
@@ -75,6 +79,8 @@ def init_robot(self):
7579
self.urscript_pub = self.node.create_publisher(
7680
StringMsg, "/urscript_interface/script_command", 1
7781
)
82+
self.client = ActionClient(self.node, SendScript, "/urscript_interface/execute_script")
83+
self.client.wait_for_server()
7884

7985
def setUp(self):
8086
self._dashboard_interface.start_robot()
@@ -141,3 +147,86 @@ def check_pin_states(self, pins, states):
141147
pin_states[i] = self.io_msg.digital_out_states[pin_id].state
142148
self.assertIsNotNone(self.io_msg, "Did not receive an IO state in requested time.")
143149
self.assertEqual(pin_states, states)
150+
151+
def test_send_script_action(self):
152+
goal_msg = SendScript.Goal()
153+
goal_msg.program = 'textmsg("hello")'
154+
goal_msg.script_name = "test_send_script_action"
155+
goal_msg.start_timeout = Duration(sec=10, nanosec=0)
156+
goal_msg.fail_on_warnings = False
157+
158+
send_goal_future = self.client.send_goal_async(goal_msg)
159+
rclpy.spin_until_future_complete(self.node, send_goal_future, timeout_sec=15)
160+
self.assertIsNotNone(send_goal_future.result(), "Failed to send SendScript goal")
161+
162+
goal_handle = send_goal_future.result()
163+
self.assertTrue(goal_handle.accepted, "SendScript goal was rejected")
164+
165+
result_future = goal_handle.get_result_async()
166+
rclpy.spin_until_future_complete(self.node, result_future, timeout_sec=30)
167+
self.assertIsNotNone(result_future.result(), "No result from SendScript action")
168+
169+
result = result_future.result().result
170+
self.assertTrue(result.success, f"SendScript action failed: {result.message}")
171+
172+
def test_reject_goals_when_busy(self):
173+
goal_msg = SendScript.Goal()
174+
goal_msg.program = "sleep(5.)"
175+
goal_msg.script_name = "test_reject_goals"
176+
goal_msg.start_timeout = Duration(sec=1, nanosec=0)
177+
goal_msg.fail_on_warnings = False
178+
179+
send_goal_future = self.client.send_goal_async(goal_msg)
180+
rclpy.spin_until_future_complete(self.node, send_goal_future, timeout_sec=15)
181+
self.assertIsNotNone(send_goal_future.result(), "Failed to send SendScript goal")
182+
183+
goal_handle = send_goal_future.result()
184+
self.assertTrue(goal_handle.accepted, "SendScript goal was rejected")
185+
time.sleep(1)
186+
187+
goal_msg.program = 'textmsg("Should be rejected")'
188+
send_goal_future = self.client.send_goal_async(goal_msg)
189+
rclpy.spin_until_future_complete(self.node, send_goal_future, timeout_sec=15)
190+
self.assertIsNotNone(send_goal_future.result(), "Failed to send SendScript goal")
191+
192+
goal_handle_reject = send_goal_future.result()
193+
self.assertFalse(goal_handle_reject.accepted)
194+
195+
result_future = goal_handle.get_result_async()
196+
rclpy.spin_until_future_complete(self.node, result_future, timeout_sec=30)
197+
self.assertIsNotNone(result_future.result(), "No result from SendScript action")
198+
199+
result = result_future.result().result
200+
self.assertTrue(result.success, f"SendScript action failed: {result.message}")
201+
202+
def test_cancel_goal(self):
203+
goal_msg = SendScript.Goal()
204+
goal_msg.program = "sleep(5.)"
205+
goal_msg.script_name = "test_reject_cancel"
206+
goal_msg.start_timeout = Duration(sec=1, nanosec=0)
207+
goal_msg.fail_on_warnings = False
208+
209+
send_goal_future = self.client.send_goal_async(goal_msg)
210+
rclpy.spin_until_future_complete(self.node, send_goal_future, timeout_sec=15)
211+
self.assertIsNotNone(send_goal_future.result(), "Failed to send SendScript goal")
212+
213+
goal_handle = send_goal_future.result()
214+
self.assertTrue(goal_handle.accepted, "SendScript goal was rejected")
215+
time.sleep(1)
216+
217+
cancel_future = goal_handle.cancel_goal_async()
218+
rclpy.spin_until_future_complete(self.node, cancel_future)
219+
self.assertEqual(cancel_future.result().return_code, CancelGoal_Response.ERROR_NONE)
220+
221+
result_future = goal_handle.get_result_async()
222+
rclpy.spin_until_future_complete(self.node, result_future, timeout_sec=30)
223+
self.assertIsNotNone(result_future.result(), "No result from SendScript action")
224+
225+
result = result_future.result().result
226+
self.assertFalse(result.success, f"SendScript action failed: {result.message}")
227+
228+
def test_ready_after_cancel(self):
229+
# send goal and cancel
230+
self.test_cancel_goal()
231+
# Test if server is ready again
232+
self.test_send_script_action()

0 commit comments

Comments
 (0)