Nav2 Navigation Stack - jazzy  jazzy
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 
22 #include "behaviortree_cpp/action_node.h"
23 #include "behaviortree_cpp/json_export.h"
24 #include "nav2_util/node_utils.hpp"
25 #include "rclcpp_action/rclcpp_action.hpp"
26 #include "nav2_behavior_tree/bt_utils.hpp"
27 #include "nav2_behavior_tree/json_utils.hpp"
28 
29 namespace nav2_behavior_tree
30 {
31 
32 using namespace std::chrono_literals; // NOLINT
33 
38 template<class ActionT>
39 class BtActionNode : public BT::ActionNodeBase
40 {
41 public:
49  const std::string & xml_tag_name,
50  const std::string & action_name,
51  const BT::NodeConfiguration & conf)
52  : BT::ActionNodeBase(xml_tag_name, conf), action_name_(action_name), should_send_goal_(true)
53  {
54  node_ = config().blackboard->template get<rclcpp::Node::SharedPtr>("node");
55  callback_group_ = node_->create_callback_group(
56  rclcpp::CallbackGroupType::MutuallyExclusive,
57  false);
58  callback_group_executor_.add_callback_group(callback_group_, node_->get_node_base_interface());
59 
60  // Get the required items from the blackboard
61  auto bt_loop_duration =
62  config().blackboard->template get<std::chrono::milliseconds>("bt_loop_duration");
63  getInputOrBlackboard("server_timeout", server_timeout_);
64  getInputOrBlackboard("cancel_timeout", cancel_timeout_);
65  wait_for_service_timeout_ =
66  config().blackboard->template get<std::chrono::milliseconds>("wait_for_service_timeout");
67 
68  // timeout should be less than bt_loop_duration to be able to finish the current tick
69  max_timeout_ = std::chrono::duration_cast<std::chrono::milliseconds>(bt_loop_duration * 0.5);
70 
71  // Initialize the input and output messages
72  goal_ = typename ActionT::Goal();
73  result_ = typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult();
74 
75  std::string remapped_action_name;
76  if (getInput("server_name", remapped_action_name)) {
77  action_name_ = remapped_action_name;
78  }
79  createActionClient(action_name_);
80 
81  // Give the derive class a chance to do any initialization
82  RCLCPP_DEBUG(node_->get_logger(), "\"%s\" BtActionNode initialized", xml_tag_name.c_str());
83  }
84 
85  BtActionNode() = delete;
86 
87  virtual ~BtActionNode()
88  {
89  }
90 
95  void createActionClient(const std::string & action_name)
96  {
97  // Now that we have the ROS node to use, create the action client for this BT action
98  action_client_ = rclcpp_action::create_client<ActionT>(node_, action_name, callback_group_);
99 
100  // Make sure the server is actually there before continuing
101  RCLCPP_DEBUG(node_->get_logger(), "Waiting for \"%s\" action server", action_name.c_str());
102  if (!action_client_->wait_for_action_server(wait_for_service_timeout_)) {
103  RCLCPP_ERROR(
104  node_->get_logger(), "\"%s\" action server not available after waiting for %.2fs",
105  action_name.c_str(),
106  wait_for_service_timeout_.count() / 1000.0);
107  throw std::runtime_error(
108  std::string("Action server ") + action_name +
109  std::string(" not available"));
110  }
111  }
112 
119  static BT::PortsList providedBasicPorts(BT::PortsList addition)
120  {
121  BT::PortsList basic = {
122  BT::InputPort<std::string>("server_name", "Action server name"),
123  BT::InputPort<std::chrono::milliseconds>("server_timeout")
124  };
125  basic.insert(addition.begin(), addition.end());
126 
127  return basic;
128  }
129 
134  static BT::PortsList providedPorts()
135  {
136  return providedBasicPorts({});
137  }
138 
139  // Derived classes can override any of the following methods to hook into the
140  // processing for the action: on_tick, on_wait_for_result, and on_success
141 
146  virtual void on_tick()
147  {
148  }
149 
157  virtual void on_wait_for_result(std::shared_ptr<const typename ActionT::Feedback>/*feedback*/)
158  {
159  }
160 
166  virtual BT::NodeStatus on_success()
167  {
168  return BT::NodeStatus::SUCCESS;
169  }
170 
175  virtual BT::NodeStatus on_aborted()
176  {
177  return BT::NodeStatus::FAILURE;
178  }
179 
184  virtual BT::NodeStatus on_cancelled()
185  {
186  return BT::NodeStatus::SUCCESS;
187  }
188 
193  BT::NodeStatus tick() override
194  {
195  // first step to be done only at the beginning of the Action
196  if (!BT::isStatusActive(status())) {
197  // reset the flag to send the goal or not, allowing the user the option to set it in on_tick
198  should_send_goal_ = true;
199 
200  // Clear the input and output messages to make sure we have no leftover from previous calls
201  goal_ = typename ActionT::Goal();
202  result_ = typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult();
203 
204  // user defined callback, may modify "should_send_goal_".
205  on_tick();
206 
207  // setting the status to RUNNING to notify the BT Loggers (if any)
208  setStatus(BT::NodeStatus::RUNNING);
209 
210  if (!should_send_goal_) {
211  return BT::NodeStatus::FAILURE;
212  }
213  send_new_goal();
214  }
215 
216  try {
217  // if new goal was sent and action server has not yet responded
218  // check the future goal handle
219  if (future_goal_handle_) {
220  auto elapsed =
221  (node_->now() - time_goal_sent_).template to_chrono<std::chrono::milliseconds>();
222  if (!is_future_goal_handle_complete(elapsed)) {
223  // return RUNNING if there is still some time before timeout happens
224  if (elapsed < server_timeout_) {
225  return BT::NodeStatus::RUNNING;
226  }
227  // if server has taken more time than the specified timeout value return FAILURE
228  RCLCPP_WARN(
229  node_->get_logger(),
230  "Timed out while waiting for action server to acknowledge goal request for %s",
231  action_name_.c_str());
232  future_goal_handle_.reset();
233  return BT::NodeStatus::FAILURE;
234  }
235  }
236 
237  // The following code corresponds to the "RUNNING" loop
238  if (rclcpp::ok() && !goal_result_available_) {
239  // user defined callback. May modify the value of "goal_updated_"
240  on_wait_for_result(feedback_);
241 
242  // reset feedback to avoid stale information
243  feedback_.reset();
244 
245  auto goal_status = goal_handle_->get_status();
246  if (goal_updated_ &&
247  (goal_status == action_msgs::msg::GoalStatus::STATUS_EXECUTING ||
248  goal_status == action_msgs::msg::GoalStatus::STATUS_ACCEPTED))
249  {
250  goal_updated_ = false;
251  send_new_goal();
252  auto elapsed =
253  (node_->now() - time_goal_sent_).template to_chrono<std::chrono::milliseconds>();
254  if (!is_future_goal_handle_complete(elapsed)) {
255  if (elapsed < server_timeout_) {
256  return BT::NodeStatus::RUNNING;
257  }
258  RCLCPP_WARN(
259  node_->get_logger(),
260  "Timed out while waiting for action server to acknowledge goal request for %s",
261  action_name_.c_str());
262  future_goal_handle_.reset();
263  return BT::NodeStatus::FAILURE;
264  }
265  }
266 
267  callback_group_executor_.spin_some();
268 
269  // check if, after invoking spin_some(), we finally received the result
270  if (!goal_result_available_) {
271  // Yield this Action, returning RUNNING
272  return BT::NodeStatus::RUNNING;
273  }
274  }
275  } catch (const std::runtime_error & e) {
276  if (e.what() == std::string("send_goal failed") ||
277  e.what() == std::string("Goal was rejected by the action server"))
278  {
279  // Action related failure that should not fail the tree, but the node
280  return BT::NodeStatus::FAILURE;
281  } else {
282  // Internal exception to propagate to the tree
283  throw e;
284  }
285  }
286 
287  BT::NodeStatus status;
288  switch (result_.code) {
289  case rclcpp_action::ResultCode::SUCCEEDED:
290  status = on_success();
291  break;
292 
293  case rclcpp_action::ResultCode::ABORTED:
294  status = on_aborted();
295  break;
296 
297  case rclcpp_action::ResultCode::CANCELED:
298  status = on_cancelled();
299  break;
300 
301  default:
302  throw std::logic_error("BtActionNode::Tick: invalid status value");
303  }
304 
305  goal_handle_.reset();
306  return status;
307  }
308 
313  void halt() override
314  {
315  if (should_cancel_goal()) {
316  auto future_result = action_client_->async_get_result(goal_handle_);
317  auto future_cancel = action_client_->async_cancel_goal(goal_handle_);
318  if (callback_group_executor_.spin_until_future_complete(future_cancel, server_timeout_) !=
319  rclcpp::FutureReturnCode::SUCCESS)
320  {
321  RCLCPP_ERROR(
322  node_->get_logger(),
323  "Failed to cancel action server for %s", action_name_.c_str());
324  }
325 
326  if (callback_group_executor_.spin_until_future_complete(future_result, cancel_timeout_) !=
327  rclcpp::FutureReturnCode::SUCCESS)
328  {
329  RCLCPP_ERROR(
330  node_->get_logger(),
331  "Failed to get result for %s in node halt!", action_name_.c_str());
332  }
333 
334  on_cancelled();
335  }
336 
337  // this is probably redundant, since the parent node
338  // is supposed to call it, but we keep it, just in case
339  resetStatus();
340  }
341 
342 protected:
348  {
349  // Shut the node down if it is currently running
350  if (status() != BT::NodeStatus::RUNNING) {
351  return false;
352  }
353 
354  // No need to cancel the goal if goal handle is invalid
355  if (!goal_handle_) {
356  return false;
357  }
358 
359  callback_group_executor_.spin_some();
360  auto status = goal_handle_->get_status();
361 
362  // Check if the goal is still executing
363  return status == action_msgs::msg::GoalStatus::STATUS_ACCEPTED ||
364  status == action_msgs::msg::GoalStatus::STATUS_EXECUTING;
365  }
366 
371  {
372  goal_result_available_ = false;
373  auto send_goal_options = typename rclcpp_action::Client<ActionT>::SendGoalOptions();
374  send_goal_options.result_callback =
375  [this](const typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult & result) {
376  if (future_goal_handle_) {
377  RCLCPP_DEBUG(
378  node_->get_logger(),
379  "Goal result for %s available, but it hasn't received the goal response yet. "
380  "It's probably a goal result for the last goal request", action_name_.c_str());
381  return;
382  }
383 
384  // TODO(#1652): a work around until rcl_action interface is updated
385  // if goal ids are not matched, the older goal call this callback so ignore the result
386  // if matched, it must be processed (including aborted)
387  if (this->goal_handle_->get_goal_id() == result.goal_id) {
388  goal_result_available_ = true;
389  result_ = result;
390  emitWakeUpSignal();
391  }
392  };
393  send_goal_options.feedback_callback =
394  [this](typename rclcpp_action::ClientGoalHandle<ActionT>::SharedPtr,
395  const std::shared_ptr<const typename ActionT::Feedback> feedback) {
396  feedback_ = feedback;
397  emitWakeUpSignal();
398  };
399 
400  future_goal_handle_ = std::make_shared<
401  std::shared_future<typename rclcpp_action::ClientGoalHandle<ActionT>::SharedPtr>>(
402  action_client_->async_send_goal(goal_, send_goal_options));
403  time_goal_sent_ = node_->now();
404  }
405 
412  bool is_future_goal_handle_complete(std::chrono::milliseconds & elapsed)
413  {
414  auto remaining = server_timeout_ - elapsed;
415 
416  // server has already timed out, no need to sleep
417  if (remaining <= std::chrono::milliseconds(0)) {
418  future_goal_handle_.reset();
419  return false;
420  }
421 
422  auto timeout = remaining > max_timeout_ ? max_timeout_ : remaining;
423  auto result =
424  callback_group_executor_.spin_until_future_complete(*future_goal_handle_, timeout);
425  elapsed += timeout;
426 
427  if (result == rclcpp::FutureReturnCode::INTERRUPTED) {
428  future_goal_handle_.reset();
429  throw std::runtime_error("send_goal failed");
430  }
431 
432  if (result == rclcpp::FutureReturnCode::SUCCESS) {
433  goal_handle_ = future_goal_handle_->get();
434  future_goal_handle_.reset();
435  if (!goal_handle_) {
436  throw std::runtime_error("Goal was rejected by the action server");
437  }
438  return true;
439  }
440 
441  return false;
442  }
443 
448  {
449  int recovery_count = 0;
450  [[maybe_unused]] auto res = config().blackboard->get("number_recoveries", recovery_count); // NOLINT
451  recovery_count += 1;
452  config().blackboard->set("number_recoveries", recovery_count); // NOLINT
453  }
454 
455  std::string action_name_;
456  typename std::shared_ptr<rclcpp_action::Client<ActionT>> action_client_;
457 
458  // All ROS2 actions have a goal and a result
459  typename ActionT::Goal goal_;
460  bool goal_updated_{false};
461  bool goal_result_available_{false};
462  typename rclcpp_action::ClientGoalHandle<ActionT>::SharedPtr goal_handle_;
463  typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult result_;
464 
465  // To handle feedback from action server
466  std::shared_ptr<const typename ActionT::Feedback> feedback_;
467 
468  // The node that will be used for any ROS operations
469  rclcpp::Node::SharedPtr node_;
470  rclcpp::CallbackGroup::SharedPtr callback_group_;
471  rclcpp::executors::SingleThreadedExecutor callback_group_executor_;
472 
473  // The timeout value while waiting for response from a server when a
474  // new action goal is sent or canceled
475  std::chrono::milliseconds server_timeout_;
476 
477  // The timeout value when cancelling actions during halt
478  std::chrono::milliseconds cancel_timeout_;
479 
480  // The timeout value for BT loop execution
481  std::chrono::milliseconds max_timeout_;
482 
483  // The timeout value for waiting for a service to response
484  std::chrono::milliseconds wait_for_service_timeout_;
485 
486  // To track the action server acknowledgement when a new goal is sent
487  std::shared_ptr<std::shared_future<typename rclcpp_action::ClientGoalHandle<ActionT>::SharedPtr>>
488  future_goal_handle_;
489  rclcpp::Time time_goal_sent_;
490 
491  // Can be set in on_tick or on_wait_for_result to indicate if a goal should be sent.
492  bool should_send_goal_;
493 };
494 
495 } // namespace nav2_behavior_tree
496 
497 #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 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 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...
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.