EasyFleet
A simple-by-default framework for multi-robot, multi-capability fleets built on ROS 2
Loading...
Searching...
No Matches
running_capability.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_MISSION_MANAGER__DETAIL__RUNNING_CAPABILITY_HPP_
17#define EASYFLEET_MISSION_MANAGER__DETAIL__RUNNING_CAPABILITY_HPP_
18
19#include <atomic>
20#include <chrono>
21#include <sstream>
22#include <string>
23
24#include "rclcpp/rclcpp.hpp"
25
26#include "easyfleet_core/capability_client.hpp"
27
28#include "easyfleet_mission_manager/ansi.hpp"
29#include "easyfleet_mission_manager/capability_state.hpp"
30#include "easyfleet_mission_manager/output.hpp"
31#include "easyfleet_mission_manager/status_markers.hpp"
32
33// RobotHandle::run_capability<ActionT>() already knows ActionT at the call
34// site (it's the template parameter), so RobotHandle only needs a
35// *non-template* handle to hold in its single per-capability-type map for
36// the ActionT-independent parts (current state, stop(), timeout
37// enforcement) -- RunningCapabilityBase. run() itself, which needs
38// ActionT::Goal, is not part of that virtual interface: RobotHandle
39// dynamic_casts down to the concrete RunningCapability<ActionT> (verifying
40// this capability_type hasn't previously been run() with a *different*
41// ActionT -- see robot_handle_impl.hpp) and calls it directly.
42namespace easyfleet_mission_manager::detail
43{
44
49inline easyfleet::CapabilityState outcome_to_state(uint8_t outcome_value)
50{
51 // Mirrors easyfleet_core::ActionClient<ActionT>::GoalOutcome's values
52 // (CapabilityClient::Outcome is a direct alias of it) member for member.
53 switch (outcome_value) {
54 case 0: return easyfleet::CapabilityState::SUCCEEDED;
55 case 1: return easyfleet::CapabilityState::ABORTED;
56 case 2: return easyfleet::CapabilityState::CANCELED;
57 case 3: return easyfleet::CapabilityState::REJECTED;
58 case 4: return easyfleet::CapabilityState::TIMEOUT;
59 default: return easyfleet::CapabilityState::UNREACHABLE; // SERVER_UNAVAILABLE
60 }
61}
62
67{
68public:
69 virtual ~RunningCapabilityBase() = default;
70
72 virtual void stop() = 0;
73
75 virtual easyfleet::CapabilityState state() const = 0;
76
79 virtual void check_timeout() = 0;
80};
81
84template<typename ActionT>
86{
87public:
90
97 rclcpp::Node & node,
98 const std::string & action_name,
99 std::string robot_name,
100 std::string capability_type,
102 : client_(Client::create(node, action_name)),
103 robot_name_(std::move(robot_name)),
104 capability_type_(std::move(capability_type)),
105 status_(status)
106 {
107 }
108
114 void run(const typename ActionT::Goal & goal, std::chrono::seconds timeout)
115 {
116 // A new run() call means a new goal supersedes whatever was in flight
117 // (server-side preemption -- see RobotHandle::run_capability()'s own
118 // doc comment). The *client*-side outcome of that superseded goal can
119 // still arrive after this point, though: its own request() callback
120 // below is still registered and will fire (typically with an
121 // ABORTED-flavored outcome) once the server settles it. Without this
122 // generation guard, that late callback would stomp state_ with the
123 // *old* goal's outcome after the new goal has already started (or
124 // even after it has already finished) -- exactly the race that
125 // produced spurious FAILED results for a robot whose goal was
126 // preempted mid-mission. Only the request() call that is still the
127 // *current* one when its callback fires is allowed to update state_.
128 const auto generation = generation_.fetch_add(1) + 1;
129 state_.store(easyfleet::CapabilityState::RUNNING);
130 deadline_ = std::chrono::steady_clock::now() + timeout;
131 status_.set_status(robot_name_, "Running " + capability_type_);
132
133 client_->request(
134 goal,
135 [this, generation](const typename Client::Response & response) {
136 if (generation_.load() != generation) {
137 return;
138 }
139 const auto new_state = outcome_to_state(static_cast<uint8_t>(response.outcome));
140 state_.store(new_state);
141 status_.set_status(
142 robot_name_,
143 capability_type_ + " -> " + easyfleet::to_string(new_state));
144 std::ostringstream out;
145 out << " " << easyfleet_mission_manager::ansi::dim << "[" << robot_name_ << "/" <<
146 capability_type_ << "] " << easyfleet_mission_manager::ansi::reset <<
147 "finished with outcome " << easyfleet_mission_manager::ansi::magenta <<
148 easyfleet::to_string(new_state) << easyfleet_mission_manager::ansi::reset;
149 easyfleet_mission_manager::safe_print(out.str());
150 });
151 }
152
153 void stop() override
154 {
155 client_->cancel();
156 }
157
158 easyfleet::CapabilityState state() const override
159 {
160 return state_.load();
161 }
162
163 void check_timeout() override
164 {
165 if (state_.load() == easyfleet::CapabilityState::RUNNING &&
166 std::chrono::steady_clock::now() >= deadline_)
167 {
168 client_->cancel();
169 }
170 }
171
172private:
173 typename Client::SharedPtr client_;
174 std::string robot_name_;
175 std::string capability_type_;
177 std::atomic<easyfleet::CapabilityState> state_{easyfleet::CapabilityState::IDLE};
178 std::chrono::steady_clock::time_point deadline_;
180 std::atomic<uint64_t> generation_{0};
181};
182
183} // namespace easyfleet_mission_manager::detail
184
185#endif // EASYFLEET_MISSION_MANAGER__DETAIL__RUNNING_CAPABILITY_HPP_
The simplest possible way to ask a capability to do something.
Definition capability_client.hpp:56
std::shared_ptr< CapabilityClient< ActionT > > SharedPtr
Shared pointer to a CapabilityClient<ActionT>.
Definition capability_client.hpp:59
Publishes a floating TEXT_VIEW_FACING marker above each robot's own base_link, showing a short,...
Definition status_markers.hpp:47
Non-template interface RobotHandle holds one of per capability type it has ever run,...
Definition running_capability.hpp:67
virtual void stop()=0
Asks the in-flight goal, if any, to stop.
virtual easyfleet::CapabilityState state() const =0
virtual void check_timeout()=0
Called periodically (from FleetSession::spin_some()) so a timeout passed to run() actually gets enfor...
RunningCapability(rclcpp::Node &node, const std::string &action_name, std::string robot_name, std::string capability_type, easyfleet_mission_manager::StatusMarkerPublisher &status)
Definition running_capability.hpp:96
easyfleet::CapabilityState state() const override
Definition running_capability.hpp:158
void run(const typename ActionT::Goal &goal, std::chrono::seconds timeout)
Sends goal and starts tracking it.
Definition running_capability.hpp:114
easyfleet_core::CapabilityClient< ActionT > Client
The CapabilityClient<ActionT> specialization this instance wraps.
Definition running_capability.hpp:89
void stop() override
Asks the in-flight goal, if any, to stop.
Definition running_capability.hpp:153
void check_timeout() override
Called periodically (from FleetSession::spin_some()) so a timeout passed to run() actually gets enfor...
Definition running_capability.hpp:163
Final outcome of a request, as delivered to a ResponseCallback or returned by request_and_wait().
Definition capability_client.hpp:75
Outcome outcome
How the request ended.
Definition capability_client.hpp:77