Skip to content

Commit cd0db17

Browse files
committed
Address thread safety and shutdown behavior of RobotStateHelper
1 parent 9c7ebd0 commit cd0db17

4 files changed

Lines changed: 346 additions & 20 deletions

File tree

ur_robot_driver/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,14 @@ if(BUILD_TESTING)
285285
ur_robot_driver_plugin
286286
)
287287

288+
ament_add_gmock(test_robot_state_helper
289+
test/test_robot_state_helper.cpp
290+
)
291+
target_link_libraries(test_robot_state_helper
292+
robot_state_helper_lib
293+
rclcpp::rclcpp
294+
)
295+
288296
add_launch_test(test/test_mock_hardware.py
289297
TIMEOUT
290298
800

ur_robot_driver/include/ur_robot_driver/robot_state_helper.hpp

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
#ifndef UR_ROBOT_DRIVER__ROBOT_STATE_HELPER_HPP_
3030
#define UR_ROBOT_DRIVER__ROBOT_STATE_HELPER_HPP_
3131

32+
#include <optional>
3233
#include <string>
3334
#include <memory>
35+
#include <thread>
3436

3537
#include "rclcpp/node.hpp"
3638
#include "rclcpp_action/server.hpp"
@@ -51,10 +53,25 @@ class RobotStateHelper : public rclcpp::Node
5153
using SetModeGoalHandle = rclcpp_action::ServerGoalHandle<ur_dashboard_msgs::action::SetMode>;
5254

5355
explicit RobotStateHelper(const rclcpp::NodeOptions& options);
54-
RobotStateHelper() = delete;
55-
virtual ~RobotStateHelper() = default;
56+
~RobotStateHelper();
57+
58+
protected:
59+
// Used only by unit tests to create an instance without connecting to
60+
// hardware. Inheriting from rclcpp::Node still requires a live ROS context,
61+
// so callers must call rclcpp::init() before constructing this object.
62+
RobotStateHelper()
63+
: rclcpp::Node("robot_state_helper")
64+
, robot_mode_(urcl::RobotMode::UNKNOWN)
65+
, safety_mode_(urcl::SafetyMode::UNDEFINED_SAFETY_MODE)
66+
, in_action_(false)
67+
{
68+
}
5669

5770
private:
71+
// Grant the test wrapper access to private state (robot_mode_, safety_mode_, in_action_)
72+
// and the goal callback.
73+
friend class RobotStateHelperTestWrapper;
74+
5875
void robotModeCallback(ur_dashboard_msgs::msg::RobotMode::SharedPtr msg);
5976
void safetyModeCallback(ur_dashboard_msgs::msg::SafetyMode::SharedPtr msg);
6077

@@ -64,7 +81,7 @@ class RobotStateHelper : public rclcpp::Node
6481
bool doTransition(const urcl::RobotMode target_mode);
6582
bool jumpToRobotMode(const urcl::RobotMode target_mode);
6683

67-
bool safeDashboardTrigger(rclcpp::Client<std_srvs::srv::Trigger>::SharedPtr srv);
84+
std::optional<bool> safeDashboardTrigger(rclcpp::Client<std_srvs::srv::Trigger>::SharedPtr srv);
6885

6986
bool stopProgram();
7087

@@ -73,6 +90,8 @@ class RobotStateHelper : public rclcpp::Node
7390
std::shared_ptr<const ur_dashboard_msgs::action::SetMode::Goal> goal);
7491
rclcpp_action::CancelResponse setModeCancelCallback(const std::shared_ptr<SetModeGoalHandle> goal_handle);
7592

93+
void handleStopRequested();
94+
7695
void setModeExecute(const std::shared_ptr<SetModeGoalHandle> goal_handle);
7796

7897
bool headless_mode_;
@@ -87,6 +106,10 @@ class RobotStateHelper : public rclcpp::Node
87106
std::atomic<bool> error_ = false;
88107
std::atomic<bool> in_action_;
89108
std::atomic<bool> program_running_;
109+
110+
// workers poll this to exit their wait loops and dashboard-service calls before the helper is torn down.
111+
std::atomic<bool> stop_requested_ = false;
112+
std::thread worker_thread_;
90113
std::mutex goal_mutex_;
91114

92115
std::string robot_ip_;

ur_robot_driver/src/robot_state_helper.cpp

Lines changed: 127 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ RobotStateHelper::RobotStateHelper(const rclcpp::NodeOptions& options)
107107
std::bind(&RobotStateHelper::setModeAcceptCallback, this, std::placeholders::_1));
108108
}
109109

110+
RobotStateHelper::~RobotStateHelper()
111+
{
112+
stop_requested_ = true;
113+
if (worker_thread_.joinable()) {
114+
worker_thread_.join();
115+
}
116+
}
117+
110118
void RobotStateHelper::robotModeCallback(ur_dashboard_msgs::msg::RobotMode::SharedPtr msg)
111119
{
112120
if (robot_mode_ != static_cast<urcl::RobotMode>(msg->mode)) {
@@ -155,7 +163,12 @@ bool RobotStateHelper::recoverFromSafety()
155163
case urcl::SafetyMode::VIOLATION:;
156164
case urcl::SafetyMode::FAULT:
157165
if (restart_safety_srv_ != nullptr) {
158-
return safeDashboardTrigger(this->restart_safety_srv_);
166+
auto call_result = safeDashboardTrigger(restart_safety_srv_);
167+
if (!call_result.has_value()) {
168+
RCLCPP_WARN_STREAM(get_logger(), "The safety restart service call was interrupted by a shutdown request.");
169+
return false;
170+
}
171+
return call_result.value();
159172
} else {
160173
return false;
161174
}
@@ -243,20 +256,39 @@ bool RobotStateHelper::doTransition(const urcl::RobotMode target_mode)
243256
return false;
244257
}
245258

246-
bool RobotStateHelper::safeDashboardTrigger(rclcpp::Client<std_srvs::srv::Trigger>::SharedPtr srv)
259+
std::optional<bool> RobotStateHelper::safeDashboardTrigger(rclcpp::Client<std_srvs::srv::Trigger>::SharedPtr srv)
247260
{
248261
assert(srv != nullptr);
249262
auto request = std::make_shared<std_srvs::srv::Trigger::Request>();
250263
auto future = srv->async_send_request(request);
251-
future.wait();
264+
// Poll in short intervals so a shutdown request is honoured promptly.
265+
while (future.wait_for(std::chrono::milliseconds(100)) != std::future_status::ready) {
266+
if (stop_requested_) {
267+
return std::nullopt;
268+
}
269+
}
252270
auto result = future.get();
253271
RCLCPP_INFO_STREAM(get_logger(), "Service response received: " << result->message);
254272
return result->success;
255273
}
256274

257275
void RobotStateHelper::setModeAcceptCallback(const std::shared_ptr<RobotStateHelper::SetModeGoalHandle> goal_handle)
258276
{
259-
std::thread{ std::bind(&RobotStateHelper::setModeExecute, this, std::placeholders::_1), goal_handle }.detach();
277+
// Join any previously completed worker before reusing worker_thread_. The goal
278+
// callback has already reserved in_action_; the previous worker cleared that
279+
// flag when it left setModeExecute, so this join is non-blocking.
280+
if (worker_thread_.joinable()) {
281+
worker_thread_.join();
282+
}
283+
{
284+
std::scoped_lock lock(goal_mutex_);
285+
// Reset stop state and install the new handle atomically so a cancel cannot
286+
// be lost to a later clear, and a stale cancel cannot latch onto this goal.
287+
stop_requested_ = false;
288+
current_goal_handle_ = goal_handle;
289+
}
290+
worker_thread_ =
291+
std::thread{ std::bind(&RobotStateHelper::setModeExecute, this, std::placeholders::_1), goal_handle };
260292
}
261293

262294
bool RobotStateHelper::stopProgram()
@@ -272,14 +304,23 @@ bool RobotStateHelper::stopProgram()
272304

273305
void RobotStateHelper::setModeExecute(const std::shared_ptr<RobotStateHelper::SetModeGoalHandle> goal_handle)
274306
{
307+
// Ensure in_action_ is always cleared when this function returns, regardless
308+
// of which exit path is taken.
309+
struct InActionGuard
275310
{
276-
std::scoped_lock lock(goal_mutex_);
277-
current_goal_handle_ = goal_handle;
278-
}
279-
in_action_ = true;
311+
std::atomic<bool>& flag;
312+
~InActionGuard()
313+
{
314+
flag = false;
315+
}
316+
} in_action_guard{ in_action_ };
280317
const auto goal = goal_handle->get_goal();
281318
this->goal_ = goal;
282319
urcl::RobotMode target_mode;
320+
if (stop_requested_ || !rclcpp::ok()) {
321+
handleStopRequested();
322+
return;
323+
}
283324
try {
284325
target_mode = static_cast<urcl::RobotMode>(goal->target_robot_mode);
285326
switch (target_mode) {
@@ -294,16 +335,28 @@ void RobotStateHelper::setModeExecute(const std::shared_ptr<RobotStateHelper::Se
294335
current_goal_handle_->abort(result_);
295336
return;
296337
}
338+
if (stop_requested_ || !rclcpp::ok()) {
339+
handleStopRequested();
340+
return;
341+
}
297342
}
298343
if (robot_mode_ != target_mode || safety_mode_ > urcl::SafetyMode::REDUCED) {
299344
RCLCPP_INFO_STREAM(get_logger(), "Target mode was set to " << robotModeString(target_mode) << ".");
300345
if (!doTransition(target_mode)) {
346+
if (stop_requested_ || !rclcpp::ok()) {
347+
handleStopRequested();
348+
return;
349+
}
301350
result_->message = "Transition to target mode failed.";
302351
result_->success = false;
303352
std::scoped_lock lock(goal_mutex_);
304353
current_goal_handle_->abort(result_);
305354
return;
306355
}
356+
if (stop_requested_ || !rclcpp::ok()) {
357+
handleStopRequested();
358+
return;
359+
}
307360
}
308361
break;
309362
case urcl::RobotMode::NO_CONTROLLER:
@@ -342,27 +395,53 @@ void RobotStateHelper::setModeExecute(const std::shared_ptr<RobotStateHelper::Se
342395
return;
343396
}
344397

345-
// Wait until the robot reached the target mode or something went wrong
346-
while (robot_mode_ != target_mode && !error_) {
398+
// Wait until the robot reached the target mode or something went wrong.
399+
while (robot_mode_ != target_mode && !error_ && !stop_requested_ && rclcpp::ok()) {
347400
RCLCPP_INFO(get_logger(), "Waiting for robot to reach target mode... Current_mode: %s",
348401
robotModeString(robot_mode_).c_str());
349402
std::this_thread::sleep_for(std::chrono::milliseconds(500));
350403
}
351404

405+
if (stop_requested_ || !rclcpp::ok()) {
406+
handleStopRequested();
407+
return;
408+
}
409+
352410
if (robot_mode_ == target_mode) {
353411
result_->success = true;
354412
result_->message = "Reached target robot mode.";
355413
if (robot_mode_ == urcl::RobotMode::RUNNING && goal_->play_program && !program_running_) {
356414
if (headless_mode_) {
357-
result_->success = safeDashboardTrigger(this->resend_robot_program_srv_);
415+
auto call_result = safeDashboardTrigger(this->resend_robot_program_srv_);
416+
if (!call_result.has_value()) {
417+
handleStopRequested();
418+
return;
419+
} else if (!call_result.value()) {
420+
result_->success = false;
421+
result_->message = "Resending the robot program failed.";
422+
}
358423
} else {
359424
if (play_program_srv_ == nullptr) {
360425
result_->success = false;
361426
result_->message = "Play program service not available on this robot.";
362427
} else {
363-
// The dashboard denies playing immediately after switching the mode to RUNNING
364-
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
365-
result_->success = safeDashboardTrigger(this->play_program_srv_);
428+
// The dashboard denies playing immediately after switching the mode to RUNNING.
429+
// Poll so cancel/shutdown is honoured during this delay.
430+
for (int i = 0; i < 10 && !stop_requested_ && rclcpp::ok(); ++i) {
431+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
432+
}
433+
if (stop_requested_ || !rclcpp::ok()) {
434+
handleStopRequested();
435+
return;
436+
}
437+
auto call_result = safeDashboardTrigger(this->play_program_srv_);
438+
if (!call_result.has_value()) {
439+
handleStopRequested();
440+
return;
441+
} else if (!call_result.value()) {
442+
result_->success = false;
443+
result_->message = "Starting the robot program failed.";
444+
}
366445
}
367446
}
368447
}
@@ -385,10 +464,25 @@ void RobotStateHelper::setModeExecute(const std::shared_ptr<RobotStateHelper::Se
385464
}
386465
}
387466

467+
void RobotStateHelper::handleStopRequested()
468+
{
469+
std::scoped_lock lock(goal_mutex_);
470+
result_->success = false;
471+
if (current_goal_handle_->is_canceling()) {
472+
result_->message = "SetMode action cancelled by user request.";
473+
current_goal_handle_->canceled(result_);
474+
} else {
475+
result_->message = "SetMode action stopped.";
476+
current_goal_handle_->abort(result_);
477+
}
478+
}
479+
388480
rclcpp_action::GoalResponse RobotStateHelper::setModeGoalCallback(
389481
const rclcpp_action::GoalUUID& uuid, std::shared_ptr<const ur_dashboard_msgs::action::SetMode::Goal> goal)
390482
{
391483
(void)uuid;
484+
(void)goal;
485+
392486
if (robot_mode_ == urcl::RobotMode::UNKNOWN) {
393487
RCLCPP_ERROR_STREAM(get_logger(), "Robot mode is unknown. Cannot accept goal, yet. Is "
394488
"the robot switched on and connected to the driver?");
@@ -400,15 +494,31 @@ rclcpp_action::GoalResponse RobotStateHelper::setModeGoalCallback(
400494
"the robot switched on and connected to the driver?");
401495
return rclcpp_action::GoalResponse::REJECT;
402496
}
497+
498+
{
499+
// Reserve the slot before returning ACCEPT so a second goal callback cannot
500+
// also accept while this goal is waiting for its accept callback to run.
501+
std::scoped_lock lock(goal_mutex_);
502+
if (in_action_) {
503+
RCLCPP_WARN_STREAM(get_logger(), "A SetMode action is already in progress. Rejecting new goal.");
504+
return rclcpp_action::GoalResponse::REJECT;
505+
}
506+
in_action_ = true;
507+
}
403508
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
404509
}
405510

406511
rclcpp_action::CancelResponse
407512
RobotStateHelper::setModeCancelCallback(const std::shared_ptr<RobotStateHelper::SetModeGoalHandle> goal_handle)
408513
{
409-
RCLCPP_INFO(get_logger(), "Received request to cancel goal");
410-
(void)goal_handle;
411-
return rclcpp_action::CancelResponse::REJECT;
514+
std::scoped_lock lock(goal_mutex_);
515+
if (!in_action_ || !current_goal_handle_ || goal_handle->get_goal_id() != current_goal_handle_->get_goal_id()) {
516+
RCLCPP_WARN(get_logger(), "No matching SetMode action is currently running. Cannot cancel.");
517+
return rclcpp_action::CancelResponse::REJECT;
518+
}
519+
RCLCPP_INFO(get_logger(), "Cancelling the current SetMode action.");
520+
stop_requested_ = true;
521+
return rclcpp_action::CancelResponse::ACCEPT;
412522
}
413523

414524
} // namespace ur_robot_driver

0 commit comments

Comments
 (0)