AutoAPMS
Resilient Robot Mission Management
Loading...
Searching...
No Matches
ros_action_node.hpp
Go to the documentation of this file.
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 <memory>
18#include <string>
19
20#include "rclcpp/executors.hpp"
21#include "rclcpp_action/rclcpp_action.hpp"
22#include "behaviortree_cpp/action_node.h"
23
26
28{
29
37
38inline const char* toStr(const ActionNodeErrorCode& err)
39{
40 switch (err)
41 {
43 return "SERVER_UNREACHABLE";
45 return "SEND_GOAL_TIMEOUT";
47 return "GOAL_REJECTED_BY_SERVER";
48 case INVALID_GOAL:
49 return "INVALID_GOAL";
50 }
51 return nullptr;
52}
53
72template <class ActionT>
73class RosActionNode : public BT::ActionNodeBase
74{
75 using ActionClient = typename rclcpp_action::Client<ActionT>;
76 using ActionClientPtr = std::shared_ptr<ActionClient>;
77 using GoalHandle = typename rclcpp_action::ClientGoalHandle<ActionT>;
78
79 struct ActionClientInstance
80 {
81 ActionClientInstance(std::shared_ptr<rclcpp::Node> node, const std::string& action_name);
82
83 ActionClientPtr action_client;
84 rclcpp::CallbackGroup::SharedPtr callback_group;
85 rclcpp::executors::SingleThreadedExecutor callback_executor;
86 typename ActionClient::SendGoalOptions goal_options;
87 };
88
89 using ClientsRegistry = std::unordered_map<std::string, std::weak_ptr<ActionClientInstance>>;
90
91public:
92 using ActionType = ActionT;
93 using Goal = typename ActionT::Goal;
94 using Feedback = typename ActionT::Feedback;
95 using WrappedResult = typename rclcpp_action::ClientGoalHandle<ActionT>::WrappedResult;
96 using Config = BT::NodeConfig;
98
99 explicit RosActionNode(const std::string& instance_name, const Config& config, const Context& context);
100
101 virtual ~RosActionNode() = default;
102
110 static BT::PortsList providedBasicPorts(BT::PortsList addition)
111 {
112 BT::PortsList basic = { BT::InputPort<std::string>("action_name", "", "Action server name") };
113 basic.insert(addition.begin(), addition.end());
114 return basic;
115 }
116
121 static BT::PortsList providedPorts()
122 {
123 return providedBasicPorts({});
124 }
125
128 virtual void onHalt();
129
138 virtual bool setGoal(Goal& goal);
139
143 virtual BT::NodeStatus onResultReceived(const WrappedResult& result);
144
149 virtual BT::NodeStatus onFeedback(const std::shared_ptr<const Feedback> feedback_ptr);
150
154 virtual BT::NodeStatus onFailure(ActionNodeErrorCode error);
155
158
160 void halt() override;
161
162 BT::NodeStatus tick() override;
163
165 void setActionName(const std::string& action_name);
166
167 std::string getFullName() const;
168
169protected:
171
172 static std::mutex& getMutex();
173
174 // contains the fully-qualified name of the node and the name of the client
175 static ClientsRegistry& getRegistry();
176
177 const rclcpp::Logger logger_;
178
179private:
180 const Context context_;
181 std::string action_name_;
182 std::shared_ptr<ActionClientInstance> client_instance_;
183 bool action_name_should_be_checked_ = false;
184 std::string action_client_key_;
185 std::shared_future<typename GoalHandle::SharedPtr> future_goal_handle_;
186 typename GoalHandle::SharedPtr goal_handle_;
187 rclcpp::Time time_goal_sent_;
188 BT::NodeStatus on_feedback_state_change_;
189 bool goal_response_;
190 WrappedResult result_;
191
192 bool createClient(const std::string& action_name);
193};
194
195//----------------------------------------------------------------
196//---------------------- DEFINITIONS -----------------------------
197//----------------------------------------------------------------
198
199template <class ActionT>
201 const std::string& action_name)
202{
203 callback_group = node->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
204 callback_executor.add_callback_group(callback_group, node->get_node_base_interface());
205 action_client = rclcpp_action::create_client<ActionT>(node, action_name, callback_group);
206}
207
208template <class ActionT>
209inline RosActionNode<ActionT>::RosActionNode(const std::string& instance_name, const Config& config,
210 const Context& context)
211 : BT::ActionNodeBase(instance_name, config), logger_(context.getLogger()), context_(context)
212{
213 // Three cases:
214 // - we use the default action_name in RosNodeContext when port is empty
215 // - we use the action_name in the port and it is a static string.
216 // - we use the action_name in the port and it is blackboard entry.
217
218 // check port remapping
219 auto portIt = config.input_ports.find("action_name");
220 if (portIt != config.input_ports.end())
221 {
222 const std::string& bb_service_name = portIt->second;
223
224 if (isBlackboardPointer(bb_service_name))
225 {
226 // unknown value at construction time. Postpone to tick
227 action_name_should_be_checked_ = true;
228 }
229 else if (!bb_service_name.empty())
230 {
231 // "hard-coded" name in the bb_service_name. Use it.
232 createClient(bb_service_name);
233 }
234 }
235 // no port value or it is empty. Use the default value
236 if (!client_instance_ && !context_.default_port_name.empty())
237 {
238 createClient(context_.default_port_name);
239 }
240}
241
242template <class ActionT>
243inline bool RosActionNode<ActionT>::createClient(const std::string& action_name)
244{
245 if (action_name.empty())
246 {
247 throw exceptions::RosNodeError(getFullName() + " - Argument action_name is empty when trying to create a client.");
248 }
249
250 std::unique_lock lk(getMutex());
251 auto node = getRosContext().nh.lock();
252 if (!node)
253 {
254 throw exceptions::RosNodeError(getFullName() +
255 " - The shared pointer to the ROS node went out of scope. The tree node doesn't "
256 "take the ownership of the node.");
257 }
258 action_client_key_ = std::string(node->get_fully_qualified_name()) + "/" + action_name;
259
260 auto& registry = getRegistry();
261 auto it = registry.find(action_client_key_);
262 if (it == registry.end() || it->second.expired())
263 {
264 client_instance_ = std::make_shared<ActionClientInstance>(node, action_name);
265 registry.insert({ action_client_key_, client_instance_ });
266 RCLCPP_DEBUG(logger_, "%s - Created client for action '%s'.", getFullName().c_str(), action_name.c_str());
267 }
268 else
269 {
270 client_instance_ = it->second.lock();
271 }
272
273 action_name_ = action_name;
274
275 bool found = client_instance_->action_client->wait_for_action_server(getRosContext().wait_for_server_timeout);
276 if (!found)
277 {
278 RCLCPP_ERROR(logger_, "%s - Action server with name '%s' is not reachable.", getFullName().c_str(),
279 action_name_.c_str());
280 }
281 return found;
282}
283
284template <class ActionT>
286{
287}
288
289template <class ActionT>
291{
292 return true;
293}
294
295template <class ActionT>
296inline BT::NodeStatus RosActionNode<ActionT>::onResultReceived(const WrappedResult& result)
297{
298 if (result.code == rclcpp_action::ResultCode::SUCCEEDED)
299 return BT::NodeStatus::SUCCESS;
300 return BT::NodeStatus::FAILURE;
301}
302
303template <class ActionT>
304inline BT::NodeStatus RosActionNode<ActionT>::onFeedback(const std::shared_ptr<const Feedback> /*feedback_ptr*/)
305{
306 return BT::NodeStatus::RUNNING;
307}
308
309template <class ActionT>
311{
312 RCLCPP_ERROR(logger_, "%s - Error %d: %s", getFullName().c_str(), error, toStr(error));
313 return BT::NodeStatus::FAILURE;
314}
315
316template <class T>
318{
319 auto& executor = client_instance_->callback_executor;
320 if (!goal_response_ && future_goal_handle_.valid())
321 {
322 // Here the discussion is if we should block or put a timer for the waiting
323 auto ret = executor.spin_until_future_complete(future_goal_handle_, getRosContext().request_timeout);
324 if (ret != rclcpp::FutureReturnCode::SUCCESS)
325 {
326 // Do nothing in case of INTERRUPT or TIMEOUT since we must return rather quickly
327 return;
328 }
329 goal_handle_ = future_goal_handle_.get();
330 future_goal_handle_ = {};
331 }
332
333 // If goal was rejected or handle has been invalidated, we do not need to cancel
334 if (!goal_handle_)
335 return;
336
341 auto future_cancel = client_instance_->action_client->async_cancel_goal(goal_handle_);
342 if (const auto code = executor.spin_until_future_complete(future_cancel, getRosContext().request_timeout);
343 code != rclcpp::FutureReturnCode::SUCCESS)
344 {
345 RCLCPP_WARN(logger_, "%s - Failed to wait until cancellation of action '%s' is complete (Received: %s). Ignoring.",
346 getFullName().c_str(), action_name_.c_str(), rclcpp::to_string(code).c_str());
347 }
348}
349
350template <class T>
352{
353 if (status() == BT::NodeStatus::RUNNING)
354 {
355 cancelGoal();
356 onHalt();
357 }
358}
359
360template <class T>
361inline BT::NodeStatus RosActionNode<T>::tick()
362{
363 if (!rclcpp::ok())
364 {
365 halt();
366 return BT::NodeStatus::FAILURE;
367 }
368
369 // First, check if the action_client_ is valid and that the name of the
370 // action_name in the port didn't change.
371 // otherwise, create a new client
372 if (!client_instance_ || (status() == BT::NodeStatus::IDLE && action_name_should_be_checked_))
373 {
374 std::string action_name;
375 getInput("action_name", action_name);
376 if (action_name_ != action_name)
377 {
378 createClient(action_name);
379 }
380 }
381
382 if (!client_instance_)
383 {
384 throw exceptions::RosNodeError(getFullName() +
385 " - You must specify an action name either by using a default value or by "
386 "passing a value to the corresponding dynamic input port.");
387 }
388
389 auto& action_client = client_instance_->action_client;
390
391 //------------------------------------------
392 auto check_status = [this](BT::NodeStatus status) {
393 if (!isStatusCompleted(status))
394 {
395 throw exceptions::RosNodeError(getFullName() + " - The callback must return either SUCCESS or FAILURE.");
396 }
397 return status;
398 };
399
400 // first step to be done only at the beginning of the Action
401 if (status() == BT::NodeStatus::IDLE)
402 {
403 setStatus(BT::NodeStatus::RUNNING);
404
405 goal_response_ = false;
406 future_goal_handle_ = {};
407 on_feedback_state_change_ = BT::NodeStatus::RUNNING;
408 result_ = {};
409
410 Goal goal;
411 if (!setGoal(goal))
412 {
413 return check_status(onFailure(INVALID_GOAL));
414 }
415
416 typename ActionClient::SendGoalOptions goal_options;
417
418 //--------------------
419 goal_options.feedback_callback = [this](typename GoalHandle::SharedPtr,
420 const std::shared_ptr<const Feedback> feedback) {
421 on_feedback_state_change_ = onFeedback(feedback);
422 if (on_feedback_state_change_ == BT::NodeStatus::IDLE)
423 {
424 throw std::logic_error(getFullName() + " - onFeedback() must not return IDLE.");
425 }
426 emitWakeUpSignal();
427 };
428 //--------------------
429 goal_options.result_callback = [this](const WrappedResult& result) {
430 if (!goal_handle_)
431 throw std::logic_error(getFullName() + " - goal_handle_ is nullptr in result callback.");
432 if (goal_handle_->get_goal_id() == result.goal_id)
433 {
434 result_ = result;
435 goal_handle_ = nullptr; // Reset internal goal handle
436 emitWakeUpSignal();
437 }
438 };
439 //--------------------
440 // Check if server is ready
441 if (!action_client->action_server_is_ready())
442 {
443 return onFailure(SERVER_UNREACHABLE);
444 }
445
446 future_goal_handle_ = action_client->async_send_goal(goal, goal_options);
447 time_goal_sent_ = getRosContext().getCurrentTime();
448
449 return BT::NodeStatus::RUNNING;
450 }
451
452 if (status() == BT::NodeStatus::RUNNING)
453 {
454 std::unique_lock<std::mutex> lock(getMutex());
455 client_instance_->callback_executor.spin_some();
456
457 // FIRST case: check if the goal request has a timeout
458 if (!goal_response_)
459 {
460 auto ret =
461 client_instance_->callback_executor.spin_until_future_complete(future_goal_handle_, std::chrono::seconds(0));
462 if (ret != rclcpp::FutureReturnCode::SUCCESS)
463 {
464 if ((getRosContext().getCurrentTime() - time_goal_sent_) > getRosContext().request_timeout)
465 {
466 return check_status(onFailure(SEND_GOAL_TIMEOUT));
467 }
468 return BT::NodeStatus::RUNNING;
469 }
470 else
471 {
472 goal_response_ = true;
473 goal_handle_ = future_goal_handle_.get();
474 future_goal_handle_ = {};
475
476 if (!goal_handle_)
477 {
478 RCLCPP_ERROR(logger_, "%s - Goal was rejected by server.", getFullName().c_str());
479 return check_status(onFailure(GOAL_REJECTED_BY_SERVER));
480 }
481 RCLCPP_DEBUG(logger_, "%s - Goal accepted by server, waiting for result.", getFullName().c_str());
482 }
483 }
484
485 // SECOND case: onFeedback requested a stop
486 if (on_feedback_state_change_ != BT::NodeStatus::RUNNING)
487 {
488 cancelGoal();
489 return on_feedback_state_change_;
490 }
491
492 // THIRD case: result received
493 if (result_.code != rclcpp_action::ResultCode::UNKNOWN)
494 return check_status(onResultReceived(result_));
495 }
496 return BT::NodeStatus::RUNNING;
497}
498
499template <class T>
500inline void RosActionNode<T>::setActionName(const std::string& action_name)
501{
502 action_name_ = action_name;
503 createClient(action_name);
504}
505
506template <class ActionT>
507inline std::string RosActionNode<ActionT>::getFullName() const
508{
509 // NOTE: registrationName() is empty during construction as this member is frist set after the factory constructed the
510 // object
511 if (registrationName().empty())
512 return name();
513 if (this->name() == this->registrationName())
514 return this->name();
515 return this->name() + " (Type: " + this->registrationName() + ")";
516}
517
518template <class ActionT>
520{
521 return context_;
522}
523
524template <class ActionT>
526{
527 static std::mutex action_client_mutex;
528 return action_client_mutex;
529}
530
531template <class ActionT>
532inline typename RosActionNode<ActionT>::ClientsRegistry& RosActionNode<ActionT>::getRegistry()
533{
534 static ClientsRegistry action_clients_registry;
535 return action_clients_registry;
536}
537
538} // namespace auto_apms_behavior_tree
Abstract class to wrap rclcpp_action::Client<>
void setActionName(const std::string &action_name)
Can be used to change the name of the action programmatically.
virtual void onHalt()
Callback executed when the node is halted. Note that cancelGoal() is done automatically.
virtual BT::NodeStatus onFailure(ActionNodeErrorCode error)
void halt() override
The default halt() implementation will call cancelGoal if necessary.
RosActionNode(const std::string &instance_name, const Config &config, const Context &context)
void cancelGoal()
Method used to send a request to the Action server to cancel the current goal.
static BT::PortsList providedBasicPorts(BT::PortsList addition)
Any subclass of RosActionNode that has ports must implement a providedPorts method and call providedB...
typename rclcpp_action::ClientGoalHandle< ActionT >::WrappedResult WrappedResult
virtual BT::NodeStatus onFeedback(const std::shared_ptr< const Feedback > feedback_ptr)
virtual BT::NodeStatus onResultReceived(const WrappedResult &result)
static BT::PortsList providedPorts()
Creates list of BT ports.
std::string toStr(TreeExecutor::ExecutionState state)
Definition executor.cpp:290