EasyFleet
A simple-by-default framework for multi-robot, multi-capability fleets built on ROS 2
Loading...
Searching...
No Matches
action_client_impl.hpp
1// Copyright 2026 Intelligent Robotics Lab
2//
3// This file is part of the project EasyFleet
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16#ifndef EASYFLEET_CORE__DETAIL__ACTION_CLIENT_IMPL_HPP_
17#define EASYFLEET_CORE__DETAIL__ACTION_CLIENT_IMPL_HPP_
18
19// Out-of-line member definitions for easyfleet_core::ActionClient<ActionT>.
20// Included from the bottom of easyfleet_core/action_client.hpp. Not meant to be
21// included directly: templates cannot be compiled into easyfleet_core's
22// .cpp/.so, so the implementation lives here to keep the class declaration
23// in action_client.hpp free of member bodies.
24
25#include <atomic>
26#include <future>
27#include <string>
28#include <utility>
29
30#include "easyfleet_core/detail/param_utils.hpp"
31
32namespace easyfleet_core
33{
34
35template<typename ActionT>
37 rclcpp::Node & parent_node,
38 const std::string & action_name,
39 std::chrono::milliseconds default_server_timeout)
40{
41 return SharedPtr(new ActionClient(parent_node, action_name, default_server_timeout));
42}
43
44template<typename ActionT>
45ActionClient<ActionT>::ActionClient(
46 rclcpp::Node & parent_node,
47 const std::string & action_name,
48 std::chrono::milliseconds default_server_timeout)
49: action_name_(action_name),
50 default_server_timeout_(default_server_timeout)
51{
52 static std::atomic<uint64_t> instance_counter{0};
53 const std::string node_name =
54 detail::sanitize_identifier(
55 std::string(parent_node.get_name()) + "_ac_" +
56 detail::sanitize_parameter_name(action_name)) +
57 "_" + std::to_string(instance_counter.fetch_add(1));
58
59 rclcpp::NodeOptions options;
60 options.start_parameter_services(false);
61 options.start_parameter_event_publisher(false);
62 options.use_global_arguments(false);
63
64 internal_node_ = std::make_shared<rclcpp::Node>(
65 node_name, parent_node.get_namespace(), options);
66
67 client_ = rclcpp_action::create_client<ActionT>(internal_node_, action_name_);
68
69 executor_ = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
70 executor_->add_node(internal_node_);
71 spin_thread_ = std::thread([this] {executor_->spin();});
72 // executor_->cancel() only reliably interrupts a spin() that has already
73 // started; calling it any earlier (e.g. if this object is destroyed right
74 // after construction) can race with the thread above and block forever.
75 while (!executor_->is_spinning()) {
76 std::this_thread::yield();
77 }
78}
79
80template<typename ActionT>
81ActionClient<ActionT>::~ActionClient()
82{
83 if (executor_) {
84 executor_->cancel();
85 }
86 if (spin_thread_.joinable()) {
87 spin_thread_.join();
88 }
89 if (executor_ && internal_node_) {
90 executor_->remove_node(internal_node_);
91 }
92}
93
94template<typename ActionT>
95const std::string & ActionClient<ActionT>::get_action_name() const noexcept
96{
97 return action_name_;
98}
99
100template<typename ActionT>
102{
103 return client_->action_server_is_ready();
104}
105
106template<typename ActionT>
108{
109 return client_->wait_for_action_server(default_server_timeout_);
110}
111
112template<typename ActionT>
113bool ActionClient<ActionT>::wait_for_server(std::chrono::milliseconds timeout)
114{
115 return client_->wait_for_action_server(timeout);
116}
117
118template<typename ActionT>
120 const Goal & goal,
121 ResultCallback result_callback,
122 FeedbackCallback feedback_callback,
123 GoalResponseCallback goal_response_callback)
124{
125 if (!client_->action_server_is_ready()) {
126 if (result_callback) {
127 result_callback(GoalResult{GoalOutcome::SERVER_UNAVAILABLE, {}, nullptr});
128 }
129 return false;
130 }
131
132 typename rclcpp_action::Client<ActionT>::SendGoalOptions send_options;
133
134 if (feedback_callback) {
135 send_options.feedback_callback =
136 [feedback_callback](
137 typename ClientGoalHandle::SharedPtr /*handle*/,
138 const std::shared_ptr<const Feedback> feedback)
139 {
140 feedback_callback(feedback);
141 };
142 }
143
144 send_options.goal_response_callback =
145 [this, result_callback, goal_response_callback](typename ClientGoalHandle::SharedPtr handle)
146 {
147 if (!handle) {
148 if (goal_response_callback) {
149 goal_response_callback(false, rclcpp_action::GoalUUID{});
150 }
151 if (result_callback) {
152 result_callback(GoalResult{GoalOutcome::REJECTED, {}, nullptr});
153 }
154 return;
155 }
156 {
157 std::lock_guard<std::mutex> lock(goals_mutex_);
158 active_goals_[rclcpp_action::to_string(handle->get_goal_id())] = handle;
159 }
160 if (goal_response_callback) {
161 goal_response_callback(true, handle->get_goal_id());
162 }
163 };
164
165 send_options.result_callback =
166 [this, result_callback](const typename ClientGoalHandle::WrappedResult & wrapped)
167 {
168 {
169 std::lock_guard<std::mutex> lock(goals_mutex_);
170 active_goals_.erase(rclcpp_action::to_string(wrapped.goal_id));
171 }
172 if (result_callback) {
173 result_callback(to_goal_result(wrapped));
174 }
175 };
176
177 client_->async_send_goal(goal, send_options);
178 return true;
179}
180
181template<typename ActionT>
183 const Goal & goal,
184 FeedbackCallback feedback_callback,
185 std::chrono::milliseconds timeout)
186{
187 auto promise = std::make_shared<std::promise<GoalResult>>();
188 auto future = promise->get_future();
189
190 send_goal(
191 goal,
192 [promise](const GoalResult & result) {
193 promise->set_value(result);
194 },
195 feedback_callback);
196
197 if (timeout.count() <= 0) {
198 return future.get();
199 }
200 if (future.wait_for(timeout) == std::future_status::timeout) {
201 return GoalResult{GoalOutcome::TIMEOUT, {}, nullptr};
202 }
203 return future.get();
204}
205
206template<typename ActionT>
207bool ActionClient<ActionT>::cancel_goal(const rclcpp_action::GoalUUID & goal_id)
208{
209 typename ClientGoalHandle::SharedPtr handle;
210 {
211 std::lock_guard<std::mutex> lock(goals_mutex_);
212 auto it = active_goals_.find(rclcpp_action::to_string(goal_id));
213 if (it == active_goals_.end()) {
214 return false;
215 }
216 handle = it->second;
217 }
218 client_->async_cancel_goal(handle);
219 return true;
220}
221
222template<typename ActionT>
224{
225 client_->async_cancel_all_goals();
226}
227
228template<typename ActionT>
230{
231 std::lock_guard<std::mutex> lock(goals_mutex_);
232 return active_goals_.size();
233}
234
235template<typename ActionT>
236typename ActionClient<ActionT>::GoalResult ActionClient<ActionT>::to_goal_result(
237 const typename ClientGoalHandle::WrappedResult & wrapped)
238{
239 GoalOutcome outcome;
240 switch (wrapped.code) {
241 case rclcpp_action::ResultCode::SUCCEEDED:
242 outcome = GoalOutcome::SUCCEEDED;
243 break;
244 case rclcpp_action::ResultCode::CANCELED:
245 outcome = GoalOutcome::CANCELED;
246 break;
247 case rclcpp_action::ResultCode::ABORTED:
248 default:
249 outcome = GoalOutcome::ABORTED;
250 break;
251 }
252 return GoalResult{outcome, wrapped.goal_id, wrapped.result};
253}
254
255} // namespace easyfleet_core
256
257#endif // EASYFLEET_CORE__DETAIL__ACTION_CLIENT_IMPL_HPP_
bool wait_for_server()
Block until the server is reachable, using the constructor's default_server_timeout.
Definition action_client_impl.hpp:107
bool is_server_ready() const
Whether the action server is currently reachable.
Definition action_client_impl.hpp:101
std::function< void(const GoalResult &)> ResultCallback
Callback invoked once a goal reaches a terminal state.
Definition action_client.hpp:89
const std::string & get_action_name() const noexcept
The action name this client was created for.
Definition action_client_impl.hpp:95
std::function< void(bool accepted, const rclcpp_action::GoalUUID &goal_id)> GoalResponseCallback
Callback invoked once the server has accepted or rejected a goal.
Definition action_client.hpp:91
std::shared_ptr< ActionClient< ActionT > > SharedPtr
Shared pointer to an ActionClient<ActionT>.
Definition action_client.hpp:51
bool send_goal(const Goal &goal, ResultCallback result_callback=nullptr, FeedbackCallback feedback_callback=nullptr, GoalResponseCallback goal_response_callback=nullptr)
Send a goal without blocking.
Definition action_client_impl.hpp:119
std::size_t active_goal_count() const
Number of goals accepted by the server and not yet in a terminal state.
Definition action_client_impl.hpp:229
GoalResult send_goal_and_wait(const Goal &goal, FeedbackCallback feedback_callback=nullptr, std::chrono::milliseconds timeout=std::chrono::milliseconds(0))
Send a goal and block the calling thread until it reaches a terminal state (or timeout elapses,...
Definition action_client_impl.hpp:182
bool cancel_goal(const rclcpp_action::GoalUUID &goal_id)
Request cancellation of a specific in-flight goal.
Definition action_client_impl.hpp:207
std::function< void(std::shared_ptr< const Feedback >)> FeedbackCallback
Callback invoked with feedback as it arrives for an in-flight goal.
Definition action_client.hpp:62
typename ActionT::Goal Goal
Goal type of the wrapped action.
Definition action_client.hpp:53
void cancel_all_goals()
Request cancellation of every goal currently tracked by this client.
Definition action_client_impl.hpp:223
static SharedPtr create(rclcpp::Node &parent_node, const std::string &action_name, std::chrono::milliseconds default_server_timeout=std::chrono::seconds(5))
Constructs a new client for action_name, spinning its own internal node in the background.
Definition action_client_impl.hpp:36
Terminal outcome of a goal, as delivered to a ResultCallback or returned by send_goal_and_wait().
Definition action_client.hpp:79