Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
bt_action_node.hpp
1 // Copyright (c) 2018 Intel Corporation
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #ifndef NAV2_BEHAVIOR_TREE__BT_ACTION_NODE_HPP_
16 #define NAV2_BEHAVIOR_TREE__BT_ACTION_NODE_HPP_
17 
18 #include <memory>
19 #include <string>
20 #include <chrono>
21 #include <cstdint>
22 
23 #include "behaviortree_cpp/action_node.h"
24 #include "behaviortree_cpp/json_export.h"
25 #include "nav2_ros_common/node_utils.hpp"
26 #include "rclcpp_action/rclcpp_action.hpp"
27 #include "nav2_behavior_tree/bt_utils.hpp"
28 #include "nav2_behavior_tree/json_utils.hpp"
29 
30 namespace nav2_behavior_tree
31 {
32 
33 using namespace std::chrono_literals; // NOLINT
34 
40 template<class ActionT>
41 class BtActionNode : public BT::ActionNodeBase
42 {
43 public:
44  using ActionResult = typename ActionT::Result;
45 
53  const std::string & xml_tag_name,
54  const std::string & action_name,
55  const BT::NodeConfiguration & conf)
56  : BT::ActionNodeBase(xml_tag_name, conf), action_name_(action_name), should_send_goal_(true)
57  {
58  node_ = config().blackboard->template get<nav2::LifecycleNode::SharedPtr>("node");
59  callback_group_ = node_->create_callback_group(
60  rclcpp::CallbackGroupType::MutuallyExclusive,
61  false);
62  callback_group_executor_.add_callback_group(callback_group_, node_->get_node_base_interface());
63 
64  // Get the required items from the blackboard
65  auto bt_loop_duration =
66  config().blackboard->template get<std::chrono::milliseconds>("bt_loop_duration");
67  getInputOrBlackboard("server_timeout", server_timeout_);
68  getInputOrBlackboard("cancel_timeout", cancel_timeout_);
69  wait_for_service_timeout_ =
70  config().blackboard->template get<std::chrono::milliseconds>("wait_for_service_timeout");
71 
72  // timeout should be less than bt_loop_duration to be able to finish the current tick
73  max_timeout_ = std::chrono::duration_cast<std::chrono::milliseconds>(bt_loop_duration * 0.5);
74 
75  // Initialize the input and output messages
76  goal_ = typename ActionT::Goal();
77  result_ = typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult();
78 
79  if constexpr (
80  !requires {ActionT::Result::TIMEOUT;} ||
81  !requires {ActionT::Result::GOAL_REJECTED;} ||
82  !requires {ActionT::Result::SEND_GOAL_FAILURE;})
83  {
84  if constexpr (requires {ActionT::Result::UNKNOWN;}) {
85  RCLCPP_WARN(
86  node_->get_logger(),
87  "Action type for \"%s\" does not define one or more of the TIMEOUT, "
88  "GOAL_REJECTED, and SEND_GOAL_FAILURE error codes. UNKNOWN will be "
89  "used for unavailable errors.",
90  xml_tag_name.c_str());
91  } else {
92  RCLCPP_WARN(
93  node_->get_logger(),
94  "Action type for \"%s\" does not define one or more of the TIMEOUT, "
95  "GOAL_REJECTED, and SEND_GOAL_FAILURE error codes. The error_code_id "
96  "output will not be set for unavailable errors.",
97  xml_tag_name.c_str());
98  }
99  }
100 
101  std::string remapped_action_name;
102  if (getInput("server_name", remapped_action_name)) {
103  action_name_ = remapped_action_name;
104  }
105  createActionClient(action_name_);
106 
107  // Give the derive class a chance to do any initialization
108  RCLCPP_DEBUG(node_->get_logger(), "\"%s\" BtActionNode initialized", xml_tag_name.c_str());
109  }
110 
111  BtActionNode() = delete;
112 
113  virtual ~BtActionNode()
114  {
115  }
116 
121  void createActionClient(const std::string & action_name)
122  {
123  // Now that we have the ROS node to use, create the action client for this BT action
124  action_client_ = node_->create_action_client<ActionT>(action_name, callback_group_);
125 
126  // Make sure the server is actually there before continuing
127  RCLCPP_DEBUG(node_->get_logger(), "Waiting for \"%s\" action server", action_name.c_str());
128  if (!action_client_->wait_for_action_server(wait_for_service_timeout_)) {
129  RCLCPP_ERROR(
130  node_->get_logger(), "\"%s\" action server not available after waiting for %.2fs",
131  action_name.c_str(),
132  wait_for_service_timeout_.count() / 1000.0);
133  throw std::runtime_error(
134  std::string("Action server ") + action_name +
135  std::string(" not available"));
136  }
137  }
138 
145  static BT::PortsList providedBasicPorts(BT::PortsList addition)
146  {
147  BT::PortsList basic = {
148  BT::InputPort<std::string>("server_name", "Action server name"),
149  BT::InputPort<std::chrono::milliseconds>("server_timeout"),
150  BT::OutputPort<uint16_t>("error_code_id", "The action error code"),
151  BT::OutputPort<std::string>("error_msg", "The action error message")
152  };
153  basic.insert(addition.begin(), addition.end());
154 
155  return basic;
156  }
157 
162  static BT::PortsList providedPorts()
163  {
164  return providedBasicPorts({});
165  }
166 
167  // Derived classes can override any of the following methods to hook into the
168  // processing for the action: on_tick, on_wait_for_result, and on_success
169 
174  virtual void on_tick()
175  {
176  }
177 
185  virtual void on_wait_for_result(std::shared_ptr<const typename ActionT::Feedback>/*feedback*/)
186  {
187  }
188 
194  virtual BT::NodeStatus on_success()
195  {
196  return BT::NodeStatus::SUCCESS;
197  }
198 
203  virtual BT::NodeStatus on_aborted()
204  {
205  return BT::NodeStatus::FAILURE;
206  }
207 
212  virtual BT::NodeStatus on_cancelled()
213  {
214  return BT::NodeStatus::SUCCESS;
215  }
216 
221  virtual void on_timeout()
222  {
223  if constexpr (requires {ActionT::Result::TIMEOUT;}) {
224  setOutput("error_code_id", ActionResult::TIMEOUT);
225  } else if constexpr (requires {ActionT::Result::UNKNOWN;}) {
226  setOutput("error_code_id", ActionResult::UNKNOWN);
227  }
228  setOutput("error_msg", "Behavior Tree action client timed out waiting.");
229  }
230 
235  virtual void on_goal_rejected()
236  {
237  if constexpr (requires {ActionT::Result::GOAL_REJECTED;}) {
238  setOutput("error_code_id", ActionResult::GOAL_REJECTED);
239  } else if constexpr (requires {ActionT::Result::UNKNOWN;}) {
240  setOutput("error_code_id", ActionResult::UNKNOWN);
241  }
242  setOutput("error_msg", "Goal was rejected by the action server.");
243  }
244 
249  virtual void on_send_goal_failure()
250  {
251  if constexpr (requires {ActionT::Result::SEND_GOAL_FAILURE;}) {
252  setOutput("error_code_id", ActionResult::SEND_GOAL_FAILURE);
253  } else if constexpr (requires {ActionT::Result::UNKNOWN;}) {
254  setOutput("error_code_id", ActionResult::UNKNOWN);
255  }
256  setOutput("error_msg", "Failed to send goal to the action server.");
257  }
258 
263  BT::NodeStatus tick() override
264  {
265  // first step to be done only at the beginning of the Action
266  if (!BT::isStatusActive(status())) {
267  // reset the flag to send the goal or not, allowing the user the option to set it in on_tick
268  should_send_goal_ = true;
269 
270  // Clear the input and output messages to make sure we have no leftover from previous calls
271  goal_ = typename ActionT::Goal();
272  result_ = typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult();
273 
274  // user defined callback, may modify "should_send_goal_".
275  on_tick();
276 
277  // setting the status to RUNNING to notify the BT Loggers (if any)
278  setStatus(BT::NodeStatus::RUNNING);
279 
280  if (!should_send_goal_) {
281  return BT::NodeStatus::FAILURE;
282  }
283  send_new_goal();
284  }
285 
286  try {
287  // if new goal was sent and action server has not yet responded
288  // check the future goal handle
289  if (future_goal_handle_) {
290  auto elapsed =
291  (node_->now() - time_goal_sent_).template to_chrono<std::chrono::milliseconds>();
292  if (!is_future_goal_handle_complete(elapsed)) {
293  // return RUNNING if there is still some time before timeout happens
294  if (elapsed < server_timeout_) {
295  return BT::NodeStatus::RUNNING;
296  }
297  handle_goal_response_timeout();
298  return BT::NodeStatus::FAILURE;
299  }
300  }
301 
302  // The following code corresponds to the "RUNNING" loop
303  if (rclcpp::ok() && !goal_result_available_) {
304  // user defined callback. May modify the value of "goal_updated_"
305  on_wait_for_result(feedback_);
306 
307  // reset feedback to avoid stale information
308  feedback_.reset();
309 
310  auto goal_status = goal_handle_->get_status();
311  if (goal_updated_ &&
312  (goal_status == action_msgs::msg::GoalStatus::STATUS_EXECUTING ||
313  goal_status == action_msgs::msg::GoalStatus::STATUS_ACCEPTED))
314  {
315  goal_updated_ = false;
316  send_new_goal();
317  auto elapsed =
318  (node_->now() - time_goal_sent_).template to_chrono<std::chrono::milliseconds>();
319  if (!is_future_goal_handle_complete(elapsed)) {
320  if (elapsed < server_timeout_) {
321  return BT::NodeStatus::RUNNING;
322  }
323  handle_goal_response_timeout();
324  return BT::NodeStatus::FAILURE;
325  }
326  }
327 
328  callback_group_executor_.spin_some();
329 
330  // check if, after invoking spin_some(), we finally received the result
331  if (!goal_result_available_) {
332  // Yield this Action, returning RUNNING
333  return BT::NodeStatus::RUNNING;
334  }
335  }
336  } catch (const std::runtime_error & e) {
337  if (e.what() == std::string("Goal was rejected by the action server")) {
338  on_goal_rejected();
339  return BT::NodeStatus::FAILURE;
340  } else if (e.what() == std::string("send_goal failed")) {
341  on_send_goal_failure();
342  // Action related failure that should not fail the tree, but the node
343  return BT::NodeStatus::FAILURE;
344  } else {
345  // Internal exception to propagate to the tree
346  throw e;
347  }
348  }
349 
350  BT::NodeStatus status;
351  switch (result_.code) {
352  case rclcpp_action::ResultCode::SUCCEEDED:
353  status = on_success();
354  break;
355 
356  case rclcpp_action::ResultCode::ABORTED:
357  status = on_aborted();
358  break;
359 
360  case rclcpp_action::ResultCode::CANCELED:
361  status = on_cancelled();
362  break;
363 
364  default:
365  throw std::logic_error("BtActionNode::Tick: invalid status value");
366  }
367 
368  goal_handle_.reset();
369  return status;
370  }
371 
376  void halt() override
377  {
378  // Resolve a pending goal response before halting so an accepted goal can be
379  // cancelled instead of being orphaned, but only within the goal's remaining
380  // server_timeout_ budget.
381  if (future_goal_handle_) {
382  auto elapsed =
383  (node_->now() - time_goal_sent_).template to_chrono<std::chrono::milliseconds>();
384  auto remaining = server_timeout_ - elapsed;
385  if (remaining > std::chrono::milliseconds(0)) {
386  if (
387  callback_group_executor_.spin_until_future_complete(
388  *future_goal_handle_, remaining) ==
389  rclcpp::FutureReturnCode::SUCCESS)
390  {
391  goal_handle_ = future_goal_handle_->get();
392  }
393  }
394 
395  future_goal_handle_.reset();
396  }
397 
398  if (should_cancel_goal()) {
399  auto future_result = action_client_->async_get_result(goal_handle_);
400  auto future_cancel = action_client_->async_cancel_goal(goal_handle_);
401  if (callback_group_executor_.spin_until_future_complete(future_cancel, server_timeout_) !=
402  rclcpp::FutureReturnCode::SUCCESS)
403  {
404  RCLCPP_ERROR(
405  node_->get_logger(),
406  "Failed to cancel action server for %s", action_name_.c_str());
407  }
408 
409  if (callback_group_executor_.spin_until_future_complete(future_result, cancel_timeout_) !=
410  rclcpp::FutureReturnCode::SUCCESS)
411  {
412  RCLCPP_ERROR(
413  node_->get_logger(),
414  "Failed to get result for %s in node halt!", action_name_.c_str());
415  }
416 
417  on_cancelled();
418  }
419 
420  // this is probably redundant, since the parent node
421  // is supposed to call it, but we keep it, just in case
422  resetStatus();
423  }
424 
425 protected:
430  {
431  RCLCPP_WARN(
432  node_->get_logger(),
433  "Timed out waiting for action server to acknowledge goal request for %s, "
434  "canceling all goals",
435  action_name_.c_str());
436  auto future_cancel = action_client_->async_cancel_all_goals();
437  if (callback_group_executor_.spin_until_future_complete(
438  future_cancel, cancel_timeout_) != rclcpp::FutureReturnCode::SUCCESS)
439  {
440  RCLCPP_WARN(
441  node_->get_logger(),
442  "Timed out while waiting for action server to cancel all goals for %s",
443  action_name_.c_str());
444  }
445  future_goal_handle_.reset();
446  on_timeout();
447  }
448 
454  {
455  // Shut the node down if it is currently running
456  if (status() != BT::NodeStatus::RUNNING) {
457  return false;
458  }
459 
460  // No need to cancel the goal if goal handle is invalid
461  if (!goal_handle_) {
462  return false;
463  }
464 
465  callback_group_executor_.spin_some();
466  auto status = goal_handle_->get_status();
467 
468  // Check if the goal is still executing
469  return status == action_msgs::msg::GoalStatus::STATUS_ACCEPTED ||
470  status == action_msgs::msg::GoalStatus::STATUS_EXECUTING;
471  }
472 
477  {
478  goal_result_available_ = false;
479  auto send_goal_options = typename nav2::ActionClient<ActionT>::SendGoalOptions();
480  send_goal_options.result_callback =
481  [this](const typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult & result) {
482  if (future_goal_handle_) {
483  RCLCPP_DEBUG(
484  node_->get_logger(),
485  "Goal result for %s available, but it hasn't received the goal response yet. "
486  "It's probably a goal result for the last goal request", action_name_.c_str());
487  return;
488  }
489 
490  // TODO(#1652): a work around until rcl_action interface is updated
491  // if goal ids are not matched, the older goal call this callback so ignore the result
492  // if matched, it must be processed (including aborted)
493  if (this->goal_handle_->get_goal_id() == result.goal_id) {
494  goal_result_available_ = true;
495  result_ = result;
496  emitWakeUpSignal();
497  }
498  };
499  send_goal_options.feedback_callback =
500  [this](typename rclcpp_action::ClientGoalHandle<ActionT>::SharedPtr,
501  const std::shared_ptr<const typename ActionT::Feedback> feedback) {
502  feedback_ = feedback;
503  emitWakeUpSignal();
504  };
505 
506  future_goal_handle_ = std::make_shared<
507  std::shared_future<typename rclcpp_action::ClientGoalHandle<ActionT>::SharedPtr>>(
508  action_client_->async_send_goal(goal_, send_goal_options));
509  time_goal_sent_ = node_->now();
510  }
511 
518  bool is_future_goal_handle_complete(std::chrono::milliseconds & elapsed)
519  {
520  auto remaining = server_timeout_ - elapsed;
521 
522  // server has already timed out, no need to sleep
523  if (remaining <= std::chrono::milliseconds(0)) {
524  return false;
525  }
526 
527  auto timeout = remaining > max_timeout_ ? max_timeout_ : remaining;
528  auto result =
529  callback_group_executor_.spin_until_future_complete(*future_goal_handle_, timeout);
530  elapsed += timeout;
531 
532  if (result == rclcpp::FutureReturnCode::INTERRUPTED) {
533  future_goal_handle_.reset();
534  throw std::runtime_error("send_goal failed");
535  }
536 
537  if (result == rclcpp::FutureReturnCode::SUCCESS) {
538  goal_handle_ = future_goal_handle_->get();
539  future_goal_handle_.reset();
540  if (!goal_handle_) {
541  throw std::runtime_error("Goal was rejected by the action server");
542  }
543  return true;
544  }
545 
546  return false;
547  }
548 
553  {
554  int recovery_count = 0;
555  [[maybe_unused]] auto res = config().blackboard->get("number_recoveries", recovery_count); // NOLINT
556  recovery_count += 1;
557  config().blackboard->set("number_recoveries", recovery_count); // NOLINT
558  }
559 
560  std::string action_name_;
561  typename nav2::ActionClient<ActionT>::SharedPtr action_client_;
562 
563  // All ROS2 actions have a goal and a result
564  typename ActionT::Goal goal_;
565  bool goal_updated_{false};
566  bool goal_result_available_{false};
567  typename rclcpp_action::ClientGoalHandle<ActionT>::SharedPtr goal_handle_;
568  typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult result_;
569 
570  // To handle feedback from action server
571  std::shared_ptr<const typename ActionT::Feedback> feedback_;
572 
573  // The node that will be used for any ROS operations
574  nav2::LifecycleNode::SharedPtr node_;
575  rclcpp::CallbackGroup::SharedPtr callback_group_;
576  rclcpp::executors::SingleThreadedExecutor callback_group_executor_;
577 
578  // The timeout value while waiting for response from a server when a
579  // new action goal is sent or canceled
580  std::chrono::milliseconds server_timeout_;
581 
582  // The timeout value when cancelling actions
583  std::chrono::milliseconds cancel_timeout_;
584 
585  // The timeout value for BT loop execution
586  std::chrono::milliseconds max_timeout_;
587 
588  // The timeout value for waiting for a service to response
589  std::chrono::milliseconds wait_for_service_timeout_;
590 
591  // To track the action server acknowledgement when a new goal is sent
592  std::shared_ptr<std::shared_future<typename rclcpp_action::ClientGoalHandle<ActionT>::SharedPtr>>
593  future_goal_handle_;
594  rclcpp::Time time_goal_sent_;
595 
596  // Can be set in on_tick or on_wait_for_result to indicate if a goal should be sent.
597  bool should_send_goal_;
598 };
599 
600 } // namespace nav2_behavior_tree
601 
602 #endif // NAV2_BEHAVIOR_TREE__BT_ACTION_NODE_HPP_
Abstract class representing an action based BT node.
BT::NodeStatus tick() override
The main override required by a BT action.
bool is_future_goal_handle_complete(std::chrono::milliseconds &elapsed)
Function to check if the action server acknowledged a new goal.
virtual BT::NodeStatus on_cancelled()
Function to perform some user-defined operation when the action is cancelled.
virtual void on_send_goal_failure()
Function to perform work when sending a goal to the action server fails Such as setting the error cod...
virtual BT::NodeStatus on_success()
Function to perform some user-defined operation upon successful completion of the action....
void halt() override
The other (optional) override required by a BT action. In this case, we make sure to cancel the ROS2 ...
virtual void on_timeout()
Function to perform work in a BT Node when the action server times out Such as setting the error code...
void handle_goal_response_timeout()
Handle a timeout while waiting for a goal response.
virtual BT::NodeStatus on_aborted()
Function to perform some user-defined operation when the action is aborted.
static BT::PortsList providedBasicPorts(BT::PortsList addition)
Any subclass of BtActionNode that accepts parameters must provide a providedPorts method and call pro...
virtual void on_tick()
Function to perform some user-defined operation on tick Could do dynamic checks, such as getting upda...
virtual void on_goal_rejected()
Function to perform work in a BT Node when the action server rejects a goal Such as setting the error...
void createActionClient(const std::string &action_name)
Create instance of an action client.
virtual void on_wait_for_result(std::shared_ptr< const typename ActionT::Feedback >)
Function to perform some user-defined operation after a timeout waiting for a result that hasn't been...
void send_new_goal()
Function to send new goal to action server.
BtActionNode(const std::string &xml_tag_name, const std::string &action_name, const BT::NodeConfiguration &conf)
A nav2_behavior_tree::BtActionNode constructor.
static BT::PortsList providedPorts()
Creates list of BT ports.
bool should_cancel_goal()
Function to check if current goal should be cancelled.
void increment_recovery_count()
Function to increment recovery count on blackboard if this node wraps a recovery.