Skip to content

Commit bb6d99e

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

2 files changed

Lines changed: 244 additions & 34 deletions

File tree

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