@@ -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+
110118void 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
257275void 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
262294bool RobotStateHelper::stopProgram ()
@@ -272,14 +304,23 @@ bool RobotStateHelper::stopProgram()
272304
273305void 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+
388480rclcpp_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
406511rclcpp_action::CancelResponse
407512RobotStateHelper::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