EasyFleet
A simple-by-default framework for multi-robot, multi-capability fleets built on ROS 2
Loading...
Searching...
No Matches
action_server_base_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_SERVER_BASE_IMPL_HPP_
17#define EASYFLEET_CORE__DETAIL__ACTION_SERVER_BASE_IMPL_HPP_
18
19// Out-of-line member definitions for easyfleet_core::ActionServerBase<ActionT>.
20// Included from the bottom of easyfleet_core/action_server_base.hpp. Not meant
21// to be 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_server_base.hpp free of member bodies.
24
25#include <memory>
26#include <stdexcept>
27#include <string>
28#include <vector>
29
30#include "easyfleet_core/detail/namespace_utils.hpp"
31#include "easyfleet_core/detail/param_utils.hpp"
32
33namespace easyfleet_core
34{
35
36template<typename ActionT>
37ActionServerBase<ActionT>::ActionServerBase(
38 rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base,
39 rclcpp::node_interfaces::NodeClockInterface::SharedPtr node_clock,
40 rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging,
41 rclcpp::node_interfaces::NodeParametersInterface::SharedPtr node_parameters,
42 rclcpp::node_interfaces::NodeWaitablesInterface::SharedPtr node_waitables,
43 const std::string & action_name,
44 bool default_allow_preemption)
45: node_base_(std::move(node_base)),
46 node_clock_(std::move(node_clock)),
47 node_logging_(std::move(node_logging)),
48 node_parameters_(std::move(node_parameters)),
49 node_waitables_(std::move(node_waitables)),
50 action_name_(action_name),
51 param_name_(detail::sanitize_parameter_name(action_name) + ".allow_preemption")
52{
53 if (!node_base_ || !node_clock_ || !node_logging_ || !node_parameters_ || !node_waitables_) {
54 throw std::invalid_argument("ActionServerBase: node must not be null");
55 }
56 robot_name_ = detail::robot_label(node_base_->get_namespace());
57
58 if (!node_parameters_->has_parameter(param_name_)) {
59 rcl_interfaces::msg::ParameterDescriptor descriptor;
60 descriptor.description =
61 "Whether the '" + action_name_ +
62 "' action server may preempt an active goal when a new one is accepted.";
63 node_parameters_->declare_parameter(
64 param_name_, rclcpp::ParameterValue(default_allow_preemption), descriptor);
65 }
66 allow_preemption_.store(node_parameters_->get_parameter(param_name_).as_bool());
67
68 param_cb_handle_ = node_parameters_->add_post_set_parameters_callback(
69 [this](const std::vector<rclcpp::Parameter> & params) {
70 this->on_parameters_set(params);
71 });
72
73 server_ = rclcpp_action::create_server<ActionT>(
74 node_base_,
75 node_clock_,
76 node_logging_,
77 node_waitables_,
78 action_name_,
79 [this](const rclcpp_action::GoalUUID & uuid, std::shared_ptr<const Goal> goal) {
80 return this->handle_goal(uuid, goal);
81 },
82 [this](const GoalHandleSharedPtr goal_handle) {
83 return this->handle_cancel(goal_handle);
84 },
85 [this](const GoalHandleSharedPtr goal_handle) {
86 this->handle_accepted(goal_handle);
87 });
88
89 worker_ = std::thread(&ActionServerBase::worker_loop, this);
91
92template<typename ActionT>
93ActionServerBase<ActionT>::~ActionServerBase()
95 if (param_cb_handle_) {
96 node_parameters_->remove_post_set_parameters_callback(param_cb_handle_.get());
97 }
98 {
99 std::lock_guard<std::mutex> lock(mutex_);
100 shutting_down_.store(true);
101 }
102 cv_.notify_all();
103 if (worker_.joinable()) {
104 worker_.join();
105 }
106}
107
108template<typename ActionT>
109const std::string & ActionServerBase<ActionT>::get_action_name() const noexcept
110{
111 return action_name_;
112}
113
114template<typename ActionT>
116{
117 std::lock_guard<std::mutex> lock(mutex_);
118 return active_handle_ != nullptr;
119}
120
121template<typename ActionT>
123{
124 return allow_preemption_.load();
125}
126
127template<typename ActionT>
129 const GoalHandleSharedPtr /*goal_handle*/)
130{
131 return rclcpp_action::CancelResponse::ACCEPT;
132}
133
134template<typename ActionT>
136{
137}
138
139template<typename ActionT>
141{
142 return preempt_requested_.load();
143}
144
145template<typename ActionT>
147{
148 return shutting_down_.load();
149}
150
151template<typename ActionT>
153{
154 return node_logging_->get_logger();
156
157template<typename ActionT>
158rclcpp_action::GoalResponse ActionServerBase<ActionT>::handle_goal(
159 const rclcpp_action::GoalUUID & uuid,
160 std::shared_ptr<const Goal> goal)
161{
162 const auto response = this->on_goal_received(uuid, goal);
163 if (response != rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE) {
164 return response;
165 }
167 std::lock_guard<std::mutex> lock(mutex_);
168 if (active_handle_ && !allow_preemption_.load()) {
169 return rclcpp_action::GoalResponse::REJECT;
170 }
171 return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
173
174template<typename ActionT>
175rclcpp_action::CancelResponse ActionServerBase<ActionT>::handle_cancel(
176 const GoalHandleSharedPtr goal_handle)
177{
178 return this->on_cancel_requested(goal_handle);
180
181template<typename ActionT>
182void ActionServerBase<ActionT>::handle_accepted(GoalHandleSharedPtr goal_handle)
183{
184 GoalHandleSharedPtr bumped;
185 GoalHandleSharedPtr preempted_active;
186 {
187 std::lock_guard<std::mutex> lock(mutex_);
188 if (pending_handle_) {
189 // A goal was already queued to run next but never started: it is
190 // superseded by this newer one.
191 bumped = pending_handle_;
192 }
193 pending_handle_ = goal_handle;
194 if (active_handle_) {
195 preempt_requested_.store(true);
196 preempted_active = active_handle_;
197 }
198 }
199
200 if (bumped) {
201 RCLCPP_WARN(
202 node_logging_->get_logger(),
203 "Action '%s': a queued goal was superseded before it could start executing.",
204 action_name_.c_str());
205 bumped->abort(std::make_shared<Result>());
206 }
207 if (preempted_active) {
208 this->on_preempted(preempted_active);
209 }
210 cv_.notify_one();
211}
212
213template<typename ActionT>
214void ActionServerBase<ActionT>::on_parameters_set(const std::vector<rclcpp::Parameter> & params)
215{
216 for (const auto & param : params) {
217 if (param.get_name() == param_name_ &&
218 param.get_type() == rclcpp::ParameterType::PARAMETER_BOOL)
219 {
220 allow_preemption_.store(param.as_bool());
221 }
222 }
223}
224
225template<typename ActionT>
226void ActionServerBase<ActionT>::worker_loop()
227{
228 while (true) {
229 GoalHandleSharedPtr goal;
230 {
231 std::unique_lock<std::mutex> lock(mutex_);
232 cv_.wait(
233 lock, [this] {
234 return shutting_down_.load() || pending_handle_ != nullptr;
235 });
236 if (shutting_down_.load() && !pending_handle_) {
237 return;
238 }
239 goal = pending_handle_;
240 pending_handle_.reset();
241 active_handle_ = goal;
242 preempt_requested_.store(false);
243 }
244
245 if (goal->is_canceling()) {
246 // Canceled by the client before it ever got to run.
247 goal->canceled(std::make_shared<Result>());
248 } else {
249 RCLCPP_INFO(
250 node_logging_->get_logger(),
251 "Capability '%s' on robot '%s': goal execution started.",
252 action_name_.c_str(), robot_name_.c_str());
253
254 // Goals are always accepted with ACCEPT_AND_EXECUTE, so rcl_action has
255 // already transitioned this goal straight to EXECUTING; calling
256 // goal->execute() again here would be an invalid double transition.
257 try {
258 this->on_execute(goal);
259 } catch (const std::exception & e) {
260 RCLCPP_ERROR(
261 node_logging_->get_logger(),
262 "Action '%s': exception thrown from on_execute(): %s",
263 action_name_.c_str(), e.what());
264 if (goal->is_active()) {
265 goal->abort(std::make_shared<Result>());
266 }
267 }
268 if (goal->is_active()) {
269 RCLCPP_WARN(
270 node_logging_->get_logger(),
271 "Action '%s': on_execute() returned without settling the goal; aborting it.",
272 action_name_.c_str());
273 goal->abort(std::make_shared<Result>());
274 }
275
276 RCLCPP_INFO(
277 node_logging_->get_logger(),
278 "Capability '%s' on robot '%s': goal execution finished.",
279 action_name_.c_str(), robot_name_.c_str());
280 }
281
282 std::lock_guard<std::mutex> lock(mutex_);
283 active_handle_.reset();
284 }
285}
286
287} // namespace easyfleet_core
288
289#endif // EASYFLEET_CORE__DETAIL__ACTION_SERVER_BASE_IMPL_HPP_
bool is_shutdown_requested() const noexcept
Whether this object is being destroyed.
Definition action_server_base_impl.hpp:146
const std::string & get_action_name() const noexcept
Fully-qualified name of the action served by this instance.
Definition action_server_base_impl.hpp:109
rclcpp::Logger logger() const
Logger of the node hosting this action server.
Definition action_server_base_impl.hpp:152
virtual rclcpp_action::CancelResponse on_cancel_requested(const GoalHandleSharedPtr goal_handle)
Decide whether a cancel request (via the action's cancel service) should be accepted.
Definition action_server_base_impl.hpp:128
bool is_preempt_requested() const noexcept
Whether a newer goal has been accepted and is waiting to replace the one currently executing.
Definition action_server_base_impl.hpp:140
bool is_preemptable() const noexcept
Current value of the "allow_preemption" parameter for this action.
Definition action_server_base_impl.hpp:122
virtual void on_preempted(const GoalHandleSharedPtr &preempted_goal_handle)
Optional hook invoked (from the accepting thread, not the worker thread) when a new goal is about to ...
Definition action_server_base_impl.hpp:135
std::shared_ptr< GoalHandle > GoalHandleSharedPtr
Shared pointer to a GoalHandle.
Definition action_server_base.hpp:79
virtual rclcpp_action::GoalResponse on_goal_received(const rclcpp_action::GoalUUID &uuid, std::shared_ptr< const Goal > goal)=0
bool is_active() const
Whether a goal is currently executing.
Definition action_server_base_impl.hpp:115