EasyFleet
A simple-by-default framework for multi-robot, multi-capability fleets built on ROS 2
Loading...
Searching...
No Matches
capability_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__CAPABILITY_IMPL_HPP_
17#define EASYFLEET_CORE__DETAIL__CAPABILITY_IMPL_HPP_
18
19// Out-of-line member definitions for easyfleet_core::Capability<ActionServerT>.
20// Included from the bottom of easyfleet_core/capability.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 capability.hpp free of member bodies.
24
25#include <algorithm>
26#include <fstream>
27#include <memory>
28#include <sstream>
29#include <string>
30
31#include "easyfleet_core/detail/namespace_utils.hpp"
32
33namespace easyfleet_core
34{
35
36namespace detail
37{
38constexpr char kCapabilitiesFileParam[] = "capabilities_file";
39} // namespace detail
40
41template<typename ActionServerT>
43 const std::string & capability_name,
44 const rclcpp::NodeOptions & options)
45: rclcpp_lifecycle::LifecycleNode(capability_name, options),
46 capability_name_(capability_name)
47{
48 this->declare_parameter(detail::kCapabilitiesFileParam, std::string());
49 action_server_ = std::make_shared<ActionServerT>(*this, capability_name_);
50
51 robot_name_ = detail::strip_leading_slash(this->get_namespace());
52 resolved_action_name_ = this->get_node_base_interface()->resolve_topic_or_service_name(
53 action_server_->get_action_name(), /*is_service=*/ false);
54
55 RCLCPP_INFO(
56 this->get_logger(),
57 "Starting capability '%s' on robot '%s' (action: %s).",
58 capability_name_.c_str(),
59 detail::robot_label(this->get_namespace()).c_str(),
60 resolved_action_name_.c_str());
61}
62
63template<typename ActionServerT>
64Capability<ActionServerT>::~Capability()
65{
66 stop_heartbeat();
67}
68
69template<typename ActionServerT>
72{
73 return action_server_;
74}
75
76template<typename ActionServerT>
77const std::string & Capability<ActionServerT>::get_capability_name() const noexcept
78{
79 return capability_name_;
80}
81
82template<typename ActionServerT>
83const std::string & Capability<ActionServerT>::get_robot_name() const noexcept
84{
85 return robot_name_;
86}
87
88template<typename ActionServerT>
89const std::string & Capability<ActionServerT>::get_resolved_action_name() const noexcept
91 return resolved_action_name_;
92}
93
94template<typename ActionServerT>
96Capability<ActionServerT>::on_configure(const rclcpp_lifecycle::State &)
97{
98 capabilities_pub_ = this->create_publisher<easyfleet_interfaces::msg::CapabilityDescription>(
99 "/capabilities", rclcpp::QoS(1).reliable().transient_local());
100 status_pub_ = this->create_publisher<easyfleet_interfaces::msg::CapabilityStatus>(
101 "/capabilities_status", rclcpp::QoS(1).reliable());
102 return CallbackReturn::SUCCESS;
103}
104
105template<typename ActionServerT>
107Capability<ActionServerT>::on_activate(const rclcpp_lifecycle::State & previous_state)
108{
109 // Activates capabilities_pub_/status_pub_ so publish() below actually
110 // sends; a subclass overriding on_activate must call this or the
111 // lifecycle-managed publishers stay muted.
112 rclcpp_lifecycle::LifecycleNode::on_activate(previous_state);
113
114 const auto file_path = this->get_parameter(detail::kCapabilitiesFileParam).as_string();
115 if (file_path.empty()) {
116 RCLCPP_ERROR(
117 this->get_logger(),
118 "Capability '%s': the '%s' parameter must be set to a capabilities JSON file path.",
119 capability_name_.c_str(), detail::kCapabilitiesFileParam);
120 return CallbackReturn::FAILURE;
123 std::ifstream file(file_path);
124 if (!file.is_open()) {
125 RCLCPP_ERROR(
126 this->get_logger(),
127 "Capability '%s': could not open capabilities file '%s'.",
128 capability_name_.c_str(), file_path.c_str());
129 return CallbackReturn::FAILURE;
130 }
131 std::ostringstream contents;
132 contents << file.rdbuf();
133
134 easyfleet_interfaces::msg::CapabilityDescription description_msg;
135 description_msg.robot = robot_name_;
136 description_msg.capability = capability_name_;
137 description_msg.action_name = resolved_action_name_;
138 description_msg.description_json = contents.str();
139 capabilities_pub_->publish(description_msg);
140
141 heartbeat_timer_ = this->create_wall_timer(
142 std::chrono::seconds(1), std::bind(&Capability::publish_heartbeat, this));
143
144 return CallbackReturn::SUCCESS;
145}
147template<typename ActionServerT>
149Capability<ActionServerT>::on_deactivate(const rclcpp_lifecycle::State & previous_state)
150{
151 stop_heartbeat();
152 rclcpp_lifecycle::LifecycleNode::on_deactivate(previous_state);
153 return CallbackReturn::SUCCESS;
154}
155
156template<typename ActionServerT>
158Capability<ActionServerT>::on_cleanup(const rclcpp_lifecycle::State &)
159{
160 stop_heartbeat();
161 capabilities_pub_.reset();
162 status_pub_.reset();
163 return CallbackReturn::SUCCESS;
164}
165
166template<typename ActionServerT>
168Capability<ActionServerT>::on_shutdown(const rclcpp_lifecycle::State &)
169{
170 stop_heartbeat();
171 capabilities_pub_.reset();
172 status_pub_.reset();
173 return CallbackReturn::SUCCESS;
174}
175
176template<typename ActionServerT>
177void Capability<ActionServerT>::publish_heartbeat()
178{
179 easyfleet_interfaces::msg::CapabilityStatus msg;
180 msg.robot = robot_name_;
181 msg.capability = capability_name_;
182 msg.action_name = resolved_action_name_;
183 msg.busy = action_server_->is_active();
184 status_pub_->publish(msg);
185}
186
187template<typename ActionServerT>
188void Capability<ActionServerT>::stop_heartbeat()
189{
190 if (heartbeat_timer_) {
191 heartbeat_timer_->cancel();
192 heartbeat_timer_.reset();
193 }
194}
195
196// -- CapabilityNodeBase -- each of these calls the corresponding
197// LifecycleNode transition (unambiguously -- see capability.hpp for why
198// these are named *_node() instead of colliding with LifecycleNode's own
199// configure()/activate()/etc.) and surfaces just the CallbackReturn, which
200// is all a Robot/Deployment driving this instance through CapabilityNodeBase
201// alone needs to know.
202
203template<typename ActionServerT>
206{
207 CallbackReturn cb_return_code;
208 this->configure(cb_return_code);
209 return cb_return_code;
210}
211
212template<typename ActionServerT>
215{
216 CallbackReturn cb_return_code;
217 this->activate(cb_return_code);
218 return cb_return_code;
219}
220
221template<typename ActionServerT>
224{
225 CallbackReturn cb_return_code;
226 this->deactivate(cb_return_code);
227 return cb_return_code;
228}
229
230template<typename ActionServerT>
233{
234 CallbackReturn cb_return_code;
235 this->cleanup(cb_return_code);
236 return cb_return_code;
237}
238
239template<typename ActionServerT>
242{
243 CallbackReturn cb_return_code;
244 this->shutdown(cb_return_code);
245 return cb_return_code;
246}
247
248template<typename ActionServerT>
250{
251 return rclcpp_lifecycle::LifecycleNode::get_current_state().id();
252}
253
254template<typename ActionServerT>
255rclcpp::node_interfaces::NodeBaseInterface::SharedPtr
257{
258 return rclcpp_lifecycle::LifecycleNode::get_node_base_interface();
259}
260
261} // namespace easyfleet_core
262
263#endif // EASYFLEET_CORE__DETAIL__CAPABILITY_IMPL_HPP_
std::shared_ptr< ActionServerBase< typename ActionServerT::ActionType > > SharedPtr
Definition action_server_base.hpp:81
CallbackReturn activate_node() override
Drive the underlying lifecycle node from INACTIVE to ACTIVE.
Definition capability_impl.hpp:214
CallbackReturn on_configure(const rclcpp_lifecycle::State &previous_state) override
Reads capabilities_file and configures the contained ActionServerT.
Definition capability_impl.hpp:96
CallbackReturn on_shutdown(const rclcpp_lifecycle::State &previous_state) override
Stops the status heartbeat if still running.
Definition capability_impl.hpp:168
const std::string & get_resolved_action_name() const noexcept
Fully-qualified name of the action this capability exposes, e.g.
Definition capability_impl.hpp:89
CallbackReturn on_deactivate(const rclcpp_lifecycle::State &previous_state) override
Stops the status heartbeat.
Definition capability_impl.hpp:149
Capability(const std::string &capability_name, const rclcpp::NodeOptions &options=rclcpp::NodeOptions())
Constructs the capability node.
Definition capability_impl.hpp:42
CallbackReturn on_activate(const rclcpp_lifecycle::State &previous_state) override
Publishes the capability description and starts the status heartbeat.
Definition capability_impl.hpp:107
CallbackReturn on_cleanup(const rclcpp_lifecycle::State &previous_state) override
Releases the resources acquired in on_configure().
Definition capability_impl.hpp:158
ActionServerBaseT::SharedPtr get_action_server() const noexcept
The action server backing this capability.
Definition capability_impl.hpp:71
CallbackReturn shutdown_node() override
Drive the underlying lifecycle node to FINALIZED, from whatever state it is currently in.
Definition capability_impl.hpp:241
CallbackReturn cleanup_node() override
Drive the underlying lifecycle node from INACTIVE to UNCONFIGURED.
Definition capability_impl.hpp:232
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn CallbackReturn
Lifecycle transition result type shared with rclcpp_lifecycle.
Definition capability.hpp:68
uint8_t get_current_state_id() const override
The underlying lifecycle node's current state id.
Definition capability_impl.hpp:249
CallbackReturn deactivate_node() override
Drive the underlying lifecycle node from ACTIVE to INACTIVE.
Definition capability_impl.hpp:223
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr get_node_base_interface() override
So a Deployment can add the underlying node to its shared executor without knowing its concrete type.
Definition capability_impl.hpp:256
const std::string & get_capability_name() const noexcept override
The capability identity field published on /capabilities and /capabilities_status.
Definition capability_impl.hpp:77
CallbackReturn configure_node() override
Drive the underlying lifecycle node from UNCONFIGURED to INACTIVE.
Definition capability_impl.hpp:205
const std::string & get_robot_name() const noexcept override
This node's namespace, without the leading '/' (empty if none), e.g.
Definition capability_impl.hpp:83