AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
ros_action_node.hpp
1// Copyright 2024 Robin Müller
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// https://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#pragma once
16
17#include <chrono>
18#include <memory>
19#include <string>
20
21#include "action_msgs/srv/cancel_goal.hpp"
22#include "auto_apms_behavior_tree_core/exceptions.hpp"
23#include "auto_apms_behavior_tree_core/node/ros_node_context.hpp"
24#include "auto_apms_util/string.hpp"
25#include "behaviortree_cpp/action_node.h"
26#include "rclcpp/executors.hpp"
27#include "rclcpp_action/rclcpp_action.hpp"
28
30{
31
32enum ActionNodeErrorCode
33{
34 SERVER_UNREACHABLE,
35 SEND_GOAL_TIMEOUT,
36 GOAL_REJECTED_BY_SERVER,
37 INVALID_GOAL
38};
39
45inline const char * toStr(const ActionNodeErrorCode & err)
46{
47 switch (err) {
48 case SERVER_UNREACHABLE:
49 return "SERVER_UNREACHABLE";
50 case SEND_GOAL_TIMEOUT:
51 return "SEND_GOAL_TIMEOUT";
52 case GOAL_REJECTED_BY_SERVER:
53 return "GOAL_REJECTED_BY_SERVER";
54 case INVALID_GOAL:
55 return "INVALID_GOAL";
56 }
57 return nullptr;
58}
59
97template <class ActionT>
98class RosActionNode : public BT::ActionNodeBase
99{
100 using ActionClient = typename rclcpp_action::Client<ActionT>;
101 using ActionClientPtr = std::shared_ptr<ActionClient>;
102 using GoalHandle = typename rclcpp_action::ClientGoalHandle<ActionT>;
103
104 struct ActionClientInstance
105 {
106 ActionClientInstance(
107 rclcpp::Node::SharedPtr node, rclcpp::CallbackGroup::SharedPtr group, const std::string & action_name);
108
109 ActionClientPtr action_client;
110 std::string name;
111 };
112
113 using ClientsRegistry = std::unordered_map<std::string, std::weak_ptr<ActionClientInstance>>;
114
115public:
116 using ActionType = ActionT;
117 using Goal = typename ActionT::Goal;
118 using Feedback = typename ActionT::Feedback;
119 using WrappedResult = typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult;
120 using Config = BT::NodeConfig;
121 using Context = RosNodeContext;
122
132 explicit RosActionNode(const std::string & instance_name, const Config & config, Context context);
133
134 virtual ~RosActionNode() = default;
135
143 static BT::PortsList providedBasicPorts(BT::PortsList addition)
144 {
145 BT::PortsList basic = {BT::InputPort<std::string>("topic", "Name of the ROS 2 action.")};
146 basic.insert(addition.begin(), addition.end());
147 return basic;
148 }
149
154 static BT::PortsList providedPorts() { return providedBasicPorts({}); }
155
163 virtual void onHalt();
164
174 virtual bool setGoal(Goal & goal);
175
186 virtual BT::NodeStatus onResultReceived(const WrappedResult & result);
187
198 virtual BT::NodeStatus onFeedback(const Feedback & feedback);
199
209 virtual BT::NodeStatus onFailure(ActionNodeErrorCode error);
210
217
223 bool createClient(const std::string & action_name);
224
229 std::string getActionName() const;
230
231protected:
232 const Context context_;
233 const rclcpp::Logger logger_;
234
235 void halt() override final;
236
237 BT::NodeStatus tick() override final;
238
239private:
240 static std::mutex & getMutex();
241
242 // contains the fully-qualified name of the node and the name of the client
243 static ClientsRegistry & getRegistry();
244
245 bool dynamic_client_instance_ = false;
246 std::shared_ptr<ActionClientInstance> client_instance_;
247 std::string action_client_key_;
248 std::shared_future<typename GoalHandle::SharedPtr> future_goal_handle_;
249 typename GoalHandle::SharedPtr goal_handle_;
250 rclcpp::Time time_goal_sent_;
251 BT::NodeStatus on_feedback_state_change_;
252 bool goal_response_received_; // We must use this additional flag because goal_handle_ may be nullptr if rejected
253 bool goal_rejected_;
254 bool result_received_;
255 bool cancel_requested_;
256 WrappedResult result_;
257};
258
259// #####################################################################################################################
260// ################################ DEFINITIONS ##############################################
261// #####################################################################################################################
262
263template <class ActionT>
264RosActionNode<ActionT>::ActionClientInstance::ActionClientInstance(
265 rclcpp::Node::SharedPtr node, rclcpp::CallbackGroup::SharedPtr group, const std::string & action_name)
266{
267 action_client = rclcpp_action::create_client<ActionT>(node, action_name, group);
268 name = action_name;
269}
270
271template <class ActionT>
272inline RosActionNode<ActionT>::RosActionNode(const std::string & instance_name, const Config & config, Context context)
273: BT::ActionNodeBase(instance_name, config),
274 context_(context),
275 logger_(context.getChildLogger(auto_apms_util::toSnakeCase(instance_name)))
276{
277 // Consider aliasing in ports and copy the values from aliased to original ports
278 this->modifyPortsRemapping(context_.copyAliasedPortValuesToOriginalPorts(this));
279
280 if (const BT::Expected<std::string> expected_name = context_.getTopicName(this)) {
281 createClient(expected_name.value());
282 } else {
283 // We assume that determining the action name requires a blackboard pointer, which cannot be evaluated at
284 // construction time. The expression will be evaluated each time before the node is ticked the first time after
285 // successful execution.
286 dynamic_client_instance_ = true;
287 }
288}
289
290template <class ActionT>
292{
293}
294
295template <class ActionT>
296inline bool RosActionNode<ActionT>::setGoal(Goal & /*goal*/)
297{
298 return true;
299}
300
301template <class ActionT>
302inline BT::NodeStatus RosActionNode<ActionT>::onResultReceived(const WrappedResult & result)
303{
304 std::string result_str;
305 switch (result.code) {
306 case rclcpp_action::ResultCode::ABORTED:
307 result_str = "ABORTED";
308 break;
309 case rclcpp_action::ResultCode::CANCELED:
310 result_str = "CANCELED";
311 break;
312 case rclcpp_action::ResultCode::SUCCEEDED:
313 result_str = "SUCCEEDED";
314 break;
315 case rclcpp_action::ResultCode::UNKNOWN:
316 result_str = "UNKNOWN";
317 break;
318 }
319 RCLCPP_DEBUG(
320 logger_, "%s - Goal completed. Received result %s.", context_.getFullyQualifiedTreeNodeName(this).c_str(),
321 result_str.c_str());
322 if (result.code == rclcpp_action::ResultCode::SUCCEEDED) return BT::NodeStatus::SUCCESS;
323 if (cancel_requested_ && result.code == rclcpp_action::ResultCode::CANCELED) return BT::NodeStatus::SUCCESS;
324 return BT::NodeStatus::FAILURE;
325}
326
327template <class ActionT>
328inline BT::NodeStatus RosActionNode<ActionT>::onFeedback(const Feedback & /*feedback*/)
329{
330 return BT::NodeStatus::RUNNING;
331}
332
333template <class ActionT>
334inline BT::NodeStatus RosActionNode<ActionT>::onFailure(ActionNodeErrorCode error)
335{
336 const std::string msg = context_.getFullyQualifiedTreeNodeName(this) + " - Unexpected error " +
337 std::to_string(error) + ": " + toStr(error) + ".";
338 RCLCPP_ERROR_STREAM(logger_, msg);
339 throw exceptions::RosNodeError(msg);
340}
341
342template <class T>
344{
345 rclcpp::executors::SingleThreadedExecutor::SharedPtr executor_ptr = context_.executor_.lock();
346 if (!executor_ptr) {
347 throw exceptions::RosNodeError(
348 context_.getFullyQualifiedTreeNodeName(this) + " - Cannot cancel goal for action '" + client_instance_->name +
349 "' since the pointer to the associated ROS 2 executor expired.");
350 }
351
352 if (future_goal_handle_.valid()) {
353 RCLCPP_DEBUG(
354 logger_, "%s - Awaiting goal response before trying to cancel goal...",
355 context_.getFullyQualifiedTreeNodeName(this).c_str());
356 // Here the discussion is if we should block or put a timer for the waiting
357 const rclcpp::FutureReturnCode ret =
358 executor_ptr->spin_until_future_complete(future_goal_handle_, context_.registration_options_.request_timeout);
359 if (ret != rclcpp::FutureReturnCode::SUCCESS) {
360 // Do nothing in case of INTERRUPT or TIMEOUT
361 return;
362 }
363 goal_handle_ = future_goal_handle_.get();
364 future_goal_handle_ = {};
365 goal_rejected_ = goal_handle_ == nullptr;
366 }
367
368 // If goal was rejected or handle has been invalidated, we do not need to cancel
369 if (goal_rejected_) {
370 RCLCPP_DEBUG(
371 logger_, "%s - Goal was rejected. Nothing to cancel.", context_.getFullyQualifiedTreeNodeName(this).c_str());
372 return;
373 };
374
375 // If goal was accepted, but goal handle is nullptr, result callback was already called which means that the goal has
376 // already reached a terminal state
377 if (!goal_handle_) {
378 RCLCPP_DEBUG(
379 logger_, "%s - Goal has already reached a terminal state. Nothing to cancel.",
380 context_.getFullyQualifiedTreeNodeName(this).c_str());
381 return;
382 };
383
384 const std::string uuid_str = rclcpp_action::to_string(goal_handle_->get_goal_id());
385 RCLCPP_DEBUG(
386 logger_, "%s - Canceling goal %s for action '%s'.", context_.getFullyQualifiedTreeNodeName(this).c_str(),
387 uuid_str.c_str(), client_instance_->name.c_str());
388
389 // Send the cancellation request
390 std::shared_future<std::shared_ptr<typename ActionClient::CancelResponse>> future_cancel_response =
391 client_instance_->action_client->async_cancel_goal(goal_handle_);
392 if (const auto code = executor_ptr->spin_until_future_complete(
393 future_cancel_response, context_.registration_options_.request_timeout);
394 code != rclcpp::FutureReturnCode::SUCCESS) {
395 RCLCPP_WARN(
396 logger_, "%s - Failed to wait for response for cancellation request (Code: %s).",
397 context_.getFullyQualifiedTreeNodeName(this).c_str(), rclcpp::to_string(code).c_str());
398
399 // Make sure goal handle is invalidated
400 goal_handle_ = nullptr;
401 return;
402 }
403
404 // Check the response for the cancellation request
405 if (!future_cancel_response.get()) {
406 throw std::logic_error("Shared pointer to cancel response is nullptr.");
407 }
408 typename ActionClient::CancelResponse cancel_response = *future_cancel_response.get();
409 std::string cancel_response_str;
410 switch (cancel_response.return_code) {
411 case action_msgs::srv::CancelGoal::Response::ERROR_REJECTED:
412 cancel_response_str = "ERROR_REJECTED";
413 break;
414 case action_msgs::srv::CancelGoal::Response::ERROR_UNKNOWN_GOAL_ID:
415 cancel_response_str = "ERROR_UNKNOWN_GOAL_ID";
416 break;
417 case action_msgs::srv::CancelGoal::Response::ERROR_GOAL_TERMINATED:
418 cancel_response_str = "ERROR_GOAL_TERMINATED";
419 break;
420 default:
421 cancel_response_str = "ERROR_NONE";
422 break;
423 }
424 if (cancel_response.return_code == action_msgs::srv::CancelGoal::Response::ERROR_NONE) {
425 RCLCPP_DEBUG(
426 logger_,
427 "%s - Cancellation request of goal %s for action '%s' was accepted (Response: %s). Awaiting completion...",
428 context_.getFullyQualifiedTreeNodeName(this).c_str(),
429 rclcpp_action::to_string(goal_handle_->get_goal_id()).c_str(), client_instance_->name.c_str(),
430 cancel_response_str.c_str());
431
432 // Wait for the cancellation to be complete (goal result received)
433 std::shared_future<WrappedResult> future_goal_result =
434 client_instance_->action_client->async_get_result(goal_handle_);
435 if (const auto code =
436 executor_ptr->spin_until_future_complete(future_goal_result, context_.registration_options_.request_timeout);
437 code == rclcpp::FutureReturnCode::SUCCESS) {
438 RCLCPP_DEBUG(
439 logger_, "%s - Goal %s for action '%s' was cancelled successfully.",
440 context_.getFullyQualifiedTreeNodeName(this).c_str(), uuid_str.c_str(), client_instance_->name.c_str());
441 } else {
442 RCLCPP_WARN(
443 logger_, "%s - Failed to wait until cancellation completed (Code: %s).",
444 context_.getFullyQualifiedTreeNodeName(this).c_str(), rclcpp::to_string(code).c_str());
445 }
446 } else {
447 // The cancellation request was rejected. If this was due to the goal having terminated normally before the request
448 // was processed by the server, we consider the cancellation as a success. Otherwise we warn.
449 if (cancel_response.return_code == action_msgs::srv::CancelGoal::Response::ERROR_GOAL_TERMINATED) {
450 RCLCPP_DEBUG(
451 logger_, "%s - Goal %s for action '%s' has already terminated (Response: %s). Nothing to cancel.",
452 context_.getFullyQualifiedTreeNodeName(this).c_str(), uuid_str.c_str(), client_instance_->name.c_str(),
453 cancel_response_str.c_str());
454 } else {
455 RCLCPP_WARN(
456 logger_, "%s - Cancellation request was rejected (Response: %s).",
457 context_.getFullyQualifiedTreeNodeName(this).c_str(), cancel_response_str.c_str());
458 }
459 }
460
461 // Make sure goal handle is invalidated
462 goal_handle_ = nullptr;
463}
464
465template <class T>
466inline void RosActionNode<T>::halt()
467{
468 if (status() == BT::NodeStatus::RUNNING) {
469 cancel_requested_ = true;
470 onHalt();
471 cancelGoal();
472 resetStatus();
473 }
474}
475
476template <class T>
477inline BT::NodeStatus RosActionNode<T>::tick()
478{
479 if (!rclcpp::ok()) {
480 halt();
481 throw exceptions::RosNodeError(
482 context_.getFullyQualifiedTreeNodeName(this) + " - ROS 2 context has been shut down.");
483 }
484
485 // If client has been set up in derived constructor, event though this constructor couldn't, we discard the intention
486 // of dynamically creating the client
487 if (dynamic_client_instance_ && client_instance_) {
488 dynamic_client_instance_ = false;
489 }
490
491 // Try again to create the client on first tick if this was not possible during construction or if client should be
492 // created from a blackboard entry on the start of every iteration
493 if (status() == BT::NodeStatus::IDLE && dynamic_client_instance_) {
494 const BT::Expected<std::string> expected_name = context_.getTopicName(this);
495 if (expected_name) {
496 createClient(expected_name.value());
497 } else {
498 throw exceptions::RosNodeError(
499 context_.getFullyQualifiedTreeNodeName(this) +
500 " - Cannot create the action client because the action name couldn't be resolved using "
501 "the expression specified by the node's registration parameters (" +
502 NodeRegistrationOptions::PARAM_NAME_ROS2TOPIC + ": " + context_.registration_options_.topic +
503 "). Error message: " + expected_name.error());
504 }
505 }
506
507 if (!client_instance_) {
508 throw exceptions::RosNodeError(context_.getFullyQualifiedTreeNodeName(this) + " - client_instance_ is nullptr.");
509 }
510
511 auto & action_client = client_instance_->action_client;
512
513 //------------------------------------------
514 auto check_status = [this](BT::NodeStatus status) {
515 if (!isStatusCompleted(status)) {
516 throw exceptions::RosNodeError(
517 context_.getFullyQualifiedTreeNodeName(this) + " - The callback must return either SUCCESS or FAILURE.");
518 }
519 return status;
520 };
521
522 // first step to be done only at the beginning of the Action
523 if (status() == BT::NodeStatus::IDLE) {
524 setStatus(BT::NodeStatus::RUNNING);
525
526 goal_response_received_ = false;
527 goal_rejected_ = false;
528 result_received_ = false;
529 cancel_requested_ = false;
530 on_feedback_state_change_ = BT::NodeStatus::RUNNING;
531 result_ = {};
532
533 // Check if server is ready
534 if (!action_client->action_server_is_ready()) {
535 return onFailure(SERVER_UNREACHABLE);
536 }
537
538 Goal goal;
539 if (!setGoal(goal)) {
540 return check_status(onFailure(INVALID_GOAL));
541 }
542
543 typename ActionClient::SendGoalOptions goal_options;
544 goal_options.goal_response_callback = [this](typename GoalHandle::SharedPtr goal_handle) {
545 // Indicate that a goal response has been received and let tick() do the rest
546 this->goal_response_received_ = true;
547 this->goal_rejected_ = goal_handle == nullptr;
548 this->goal_handle_ = goal_handle;
549 };
550 goal_options.feedback_callback =
551 [this](typename GoalHandle::SharedPtr /*goal_handle*/, const std::shared_ptr<const Feedback> feedback) {
552 this->on_feedback_state_change_ = onFeedback(*feedback);
553 if (this->on_feedback_state_change_ == BT::NodeStatus::IDLE) {
554 throw std::logic_error(
555 this->context_.getFullyQualifiedTreeNodeName(this) + " - onFeedback() must not return IDLE.");
556 }
557 this->emitWakeUpSignal();
558 };
559 goal_options.result_callback = [this](const WrappedResult & result) {
560 // The result callback is also invoked when goal is rejected (code: CANCELED), but we only want to call
561 // onResultReceived if the goal was accepted and we want to return prematurely. Therefore, we use the
562 // cancel_requested_ flag.
563 if (this->cancel_requested_) {
564 // If the node is to return prematurely, we must invoke onResultReceived here. The returned status has no effect
565 // in this case.
566 this->onResultReceived(result);
567 }
568 this->result_received_ = true;
569 this->goal_handle_ = nullptr; // Reset internal goal handle when result was received
570 this->result_ = result;
571 this->emitWakeUpSignal();
572 };
573
574 future_goal_handle_ = action_client->async_send_goal(goal, goal_options);
575 time_goal_sent_ = context_.getCurrentTime();
576 return BT::NodeStatus::RUNNING;
577 }
578
579 if (status() == BT::NodeStatus::RUNNING) {
580 std::unique_lock<std::mutex> lock(getMutex());
581
582 // FIRST case: check if the goal request has a timeout as long as goal_response_received_ is false (Is set to true
583 // as soon as a goal response is received)
584 if (!goal_response_received_) {
585 // See if we must time out
586 if ((context_.getCurrentTime() - time_goal_sent_) > context_.registration_options_.request_timeout) {
587 return check_status(onFailure(SEND_GOAL_TIMEOUT));
588 }
589 return BT::NodeStatus::RUNNING;
590 } else if (future_goal_handle_.valid()) {
591 // We noticed, that a goal response has just been received and have to prepare the next steps now
592 future_goal_handle_ = {}; // Invalidate future since it's obsolete now and it indicates that we've done this step
593
594 if (goal_rejected_) return check_status(onFailure(GOAL_REJECTED_BY_SERVER));
595 RCLCPP_DEBUG(
596 logger_, "%s - Goal %s accepted by server, waiting for result.",
597 context_.getFullyQualifiedTreeNodeName(this).c_str(),
598 rclcpp_action::to_string(goal_handle_->get_goal_id()).c_str());
599 }
600
601 // SECOND case: onFeedback requested a stop
602 if (on_feedback_state_change_ != BT::NodeStatus::RUNNING) {
603 cancel_requested_ = true;
604 cancelGoal();
605 return on_feedback_state_change_;
606 }
607
608 // THIRD case: result received
609 if (result_received_) {
610 return check_status(onResultReceived(result_));
611 }
612 }
613 return BT::NodeStatus::RUNNING;
614}
615
616template <class ActionT>
617inline bool RosActionNode<ActionT>::createClient(const std::string & action_name)
618{
619 if (action_name.empty()) {
620 throw exceptions::RosNodeError(
621 context_.getFullyQualifiedTreeNodeName(this) +
622 " - Argument action_name is empty when trying to create the client.");
623 }
624
625 // Check if the action with given name is already set up
626 if (
627 client_instance_ && action_name == client_instance_->name &&
628 client_instance_->action_client->action_server_is_ready()) {
629 return true;
630 }
631
632 std::unique_lock lk(getMutex());
633
634 rclcpp::Node::SharedPtr node = context_.nh_.lock();
635 if (!node) {
636 throw exceptions::RosNodeError(
637 context_.getFullyQualifiedTreeNodeName(this) +
638 " - The weak pointer to the ROS 2 node expired. The tree node doesn't "
639 "take ownership of it.");
640 }
641 rclcpp::CallbackGroup::SharedPtr group = context_.cb_group_.lock();
642 if (!group) {
643 throw exceptions::RosNodeError(
644 context_.getFullyQualifiedTreeNodeName(this) +
645 " - The weak pointer to the ROS 2 callback group expired. The tree node doesn't "
646 "take ownership of it.");
647 }
648 action_client_key_ = std::string(node->get_fully_qualified_name()) + "/" + action_name;
649
650 auto & registry = getRegistry();
651 auto it = registry.find(action_client_key_);
652 if (it == registry.end() || it->second.expired()) {
653 client_instance_ = std::make_shared<ActionClientInstance>(node, group, action_name);
654 registry.insert({action_client_key_, client_instance_});
655 RCLCPP_DEBUG(
656 logger_, "%s - Created client for action '%s'.", context_.getFullyQualifiedTreeNodeName(this).c_str(),
657 action_name.c_str());
658 } else {
659 client_instance_ = it->second.lock();
660 }
661
662 bool found = client_instance_->action_client->wait_for_action_server(context_.registration_options_.wait_timeout);
663 if (!found) {
664 std::string msg = context_.getFullyQualifiedTreeNodeName(this) + " - Action server with name '" +
665 client_instance_->name + "' is not reachable.";
666 if (context_.registration_options_.allow_unreachable) {
667 RCLCPP_WARN_STREAM(logger_, msg);
668 } else {
669 RCLCPP_ERROR_STREAM(logger_, msg);
670 throw exceptions::RosNodeError(msg);
671 }
672 }
673 return found;
674}
675
676template <class ActionT>
678{
679 if (client_instance_) return client_instance_->name;
680 return "unkown";
681}
682
683template <class ActionT>
684inline std::mutex & RosActionNode<ActionT>::getMutex()
685{
686 static std::mutex action_client_mutex;
687 return action_client_mutex;
688}
689
690template <class ActionT>
691inline typename RosActionNode<ActionT>::ClientsRegistry & RosActionNode<ActionT>::getRegistry()
692{
693 static ClientsRegistry action_clients_registry;
694 return action_clients_registry;
695}
696
697} // namespace auto_apms_behavior_tree::core
virtual BT::NodeStatus onFeedback(const Feedback &feedback)
Callback invoked after action feedback was received.
virtual void onHalt()
Callback invoked when the node is halted by the behavior tree.
virtual bool setGoal(Goal &goal)
Set the goal message to be sent to the ROS 2 action.
RosActionNode(const std::string &instance_name, const Config &config, Context context)
Constructor.
virtual BT::NodeStatus onFailure(ActionNodeErrorCode error)
Callback invoked when one of the errors in ActionNodeErrorCode occur.
void cancelGoal()
Synchronous method that sends a request to the server to cancel the current action.
static BT::PortsList providedBasicPorts(BT::PortsList addition)
Derived nodes implementing the static method RosActionNode::providedPorts may call this method to als...
bool createClient(const std::string &action_name)
Create the client of the ROS 2 action.
virtual BT::NodeStatus onResultReceived(const WrappedResult &result)
Callback invoked after the result that is sent by the action server when the goal terminated was rece...
static BT::PortsList providedPorts()
If a behavior tree requires input/output data ports, the developer must define this method accordingl...
std::string getActionName() const
Get the name of the action this node connects with.
Additional parameters specific to ROS 2 determined at runtime by TreeBuilder.
rclcpp::Time getCurrentTime() const
Get the current time using the associated ROS 2 node.
std::string getFullyQualifiedTreeNodeName(const BT::TreeNode *node, bool with_class_name=true) const
Create a string representing the detailed name of a behavior tree node.
Core API for AutoAPMS's behavior tree implementation.
Definition behavior.hpp:32
const char * toStr(const ActionNodeErrorCode &err)
Convert the action error code to string.
Fundamental helper classes and utility functions.
std::string topic
Name of the ROS 2 communication interface to connect with.
std::chrono::duration< double > request_timeout
Period [s] (measured from sending a goal request) after the node aborts waiting for a server response...