AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
behavior_mode_executor.cpp
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// http://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#include "auto_apms_px4/behavior_mode_executor.hpp"
16
17#include <cstdint>
18#include <stdexcept>
19#include <utility>
20
21#include "auto_apms_behavior_tree/executor/options.hpp"
22#include "auto_apms_behavior_tree_core/node/node_manifest.hpp"
23#include "auto_apms_px4/behavior_mode_executor_params.hpp"
24#include "px4_msgs/msg/vehicle_command.hpp"
25
26namespace auto_apms_px4
27{
28
29// #####################################################################################################################
30// ################################## BehaviorOwnedMode ######################################
31// #####################################################################################################################
32
33BehaviorOwnedMode::BehaviorOwnedMode(rclcpp::Node & node, const px4_ros2::ModeBase::Settings & settings)
34: px4_ros2::ModeBase(node, settings)
35{
36 actuator_setpoint_ptr_ = std::make_shared<px4_ros2::DirectActuatorsSetpointType>(*this);
37}
38
39void BehaviorOwnedMode::updateSetpoint(float /*dt_s*/)
40{
41 // Intentionally a no-op. This mode is a registration placeholder: px4_ros2 requires every mode to declare at least
42 // one setpoint type (the DirectActuatorsSetpointType constructed above), but this mode is never meant to actively
43 // fly.
44}
45
46// #####################################################################################################################
47// ################################# BehaviorModeExecutor ####################################
48// #####################################################################################################################
49
51 BehaviorOwnedMode & owned_mode, const px4_ros2::ModeExecutorBase::Settings & settings,
52 std::function<Config()> config_provider, auto_apms_behavior_tree::GenericTreeExecutorNode & engine)
53: px4_ros2::ModeExecutorBase(settings, owned_mode),
54 node_(owned_mode.node()),
55 owned_mode_(owned_mode),
56 behavior_executor_(engine),
57 config_provider_(std::move(config_provider)),
58 config_(config_provider_())
59{
60}
61
62void BehaviorModeExecutor::onExecutionResult(ExecutionResult result)
63{
64 RCLCPP_INFO(node_.get_logger(), "Behavior execution result: %s.", auto_apms_behavior_tree::toStr(result).c_str());
65
66 // If we lost charge in the meantime (pilot/failsafe), don't fight the FMU: skip the completion reaction. We still
67 // prepare a fresh tree below so the next activation stays lightweight.
68 if (isInCharge()) {
69 const CompletionReaction reaction =
70 result == ExecutionResult::TREE_SUCCEEDED ? config_.on_completion : config_.on_failure;
71 performReaction(reaction, result);
72 } else {
73 RCLCPP_INFO(node_.get_logger(), "No longer in charge. Skipping completion reaction");
74 }
75
76 // Rebuild the detached tree so the next time this executor is put in charge, activation does not pay the tree
77 // construction cost. A build failure here (e.g. an invalid build request set at runtime) leaves no prepared tree;
78 // the next activation then reports an error instead of running a stale one.
79 try {
81 } catch (const std::exception & e) {
82 RCLCPP_ERROR(node_.get_logger(), "Failed to prepare behavior tree for the next activation: %s", e.what());
83 }
84}
85
87{
88 if (str == "hold") return CompletionReaction::HOLD;
89 if (str == "rtl") return CompletionReaction::RTL;
90 if (str == "land") return CompletionReaction::LAND;
91 if (str == "disarm") return CompletionReaction::DISARM;
92 if (str == "complete") return CompletionReaction::COMPLETE;
93 if (str == "none") return CompletionReaction::NONE;
94 throw std::invalid_argument(
95 "Invalid completion reaction '" + str + "' (expected one of: hold, rtl, land, disarm, complete, none)");
96}
97
98void BehaviorModeExecutor::onActivate()
99{
100 // Refresh the configuration from the current parameter values so runtime changes (e.g. via `ros2 param set`) to the
101 // completion reactions and failsafe deferral take effect on this activation. The behavior itself was already built
102 // (see prepareTree); its build request is latched at build time.
103 config_ = config_provider_();
104
105 if (!prepared_tree_ptr_) {
106 RCLCPP_ERROR(node_.get_logger(), "Cannot start behavior: no pre-built behavior tree is available");
107 onExecutionResult(ExecutionResult::ERROR);
108 return;
109 }
110
111 RCLCPP_INFO(
112 node_.get_logger(), "Behavior executor put in charge. Starting behavior '%s'", config_.spec.build_request.c_str());
113
114 if (config_.defer_failsafes) {
115 if (deferFailsafesSync(true)) {
116 RCLCPP_INFO(node_.get_logger(), "Failsafes are now being deferred while the behavior is running");
117 } else {
118 RCLCPP_WARN(node_.get_logger(), "Failed to enable failsafe deferral");
119 }
120 }
121
122 // Publish the executor's VehicleCommand source component on the global blackboard so that ownership-aware behavior
123 // tree nodes (e.g. SendCmdSetNavState) can attribute their commands to this executor. Those nodes read it at tick
124 // time, so setting it here (id() is valid once registered) is sufficient. Stored as int to match the type read by
125 // the SendCmdSetNavState node. The global key already carries the '@' prefix; on the global blackboard (which is its
126 // own root) this resolves to the same entry the tree nodes read transitively.
127 const int source_component = static_cast<int>(px4_msgs::msg::VehicleCommand::COMPONENT_MODE_EXECUTOR_START) + id();
128 behavior_executor_.getGlobalBlackboardPtr()->set(AUTO_APMS_PX4_SOURCE_COMPONENT_GLOBAL_KEY, source_component);
129
130 // Hand the pre-built tree to the executor. This only (re)creates the lightweight execution timer and starts ticking;
131 // the expensive tree construction already happened in prepareTree.
132 const auto executor_params = behavior_executor_.getExecutorParameters();
133 try {
134 behavior_executor_.startExecution(
135 std::move(prepared_tree_ptr_), executor_params.tick_rate, executor_params.groot2_port);
136 } catch (const std::exception & e) {
137 RCLCPP_ERROR(node_.get_logger(), "Failed to start behavior: %s", e.what());
138 onExecutionResult(ExecutionResult::ERROR);
139 }
140}
141
142void BehaviorModeExecutor::onDeactivate(DeactivateReason reason)
143{
144 const char * reason_str = reason == DeactivateReason::FailsafeActivated ? "failsafe activated" : "other";
145 RCLCPP_INFO(node_.get_logger(), "Behavior executor deactivating (reason: %s)", reason_str);
146
147 // The FMU has already taken over (pilot override or failsafe). Halting the behavior is cleanup, not safety.
148 if (behavior_executor_.isBusy()) {
149 RCLCPP_INFO(node_.get_logger(), "Behavior is still running. Terminating it now...");
151 }
152
153 if (config_.defer_failsafes) {
154 deferFailsafesSync(false);
155 }
156}
157
159{
160 const Config config = config_provider_();
161 if (config.spec.build_request.empty()) {
162 throw std::invalid_argument("Cannot prepare behavior tree: parameter 'behavior.build_request' must not be empty");
163 }
164
165 // Flag that a mode executor is in charge so SendVehicleCommand routes commands through the mode-executor command
166 // topic. This must be set before the tree is built below, because the command nodes latch their command topic from
167 // this flag as they construct. The behavior executor is dedicated to this mode executor, so the flag stays true for
168 // the lifetime of its global blackboard.
169 behavior_executor_.getGlobalBlackboardPtr()->set(AUTO_APMS_PX4_MODE_EXECUTOR_ACTIVE_GLOBAL_KEY, true);
170
172 if (!config.spec.node_manifest.empty()) {
173 node_manifest = auto_apms_behavior_tree::core::NodeManifest::decode(config.spec.node_manifest);
174 }
175
176 // Build the tree now: this is the expensive step because it instantiates the ROS 2 waitables of the behavior tree
177 // nodes. The tree is kept detached until the next activation hands it to the executor. Its blackboard is rooted at
178 // the executor's global blackboard so that '@'-prefixed entries resolve at runtime (mirroring what
179 // TreeExecutorBase::startExecution does for the TreeConstructor overloads).
180 const auto_apms_behavior_tree::TreeConstructor make_tree =
181 behavior_executor_.makeTreeConstructor(config.spec.build_request, config.spec.entry_point, node_manifest);
182 const auto_apms_behavior_tree::TreeBlackboardSharedPtr main_tree_bb_ptr =
183 auto_apms_behavior_tree::TreeBlackboard::create(behavior_executor_.getGlobalBlackboardPtr());
184 prepared_tree_ptr_ = std::make_unique<auto_apms_behavior_tree::Tree>(make_tree(main_tree_bb_ptr));
185
186 RCLCPP_INFO(
187 node_.get_logger(), "Behavior tree '%s' built and ready for activation", config.spec.build_request.c_str());
188}
189
190void BehaviorModeExecutor::performReaction(CompletionReaction reaction, ExecutionResult result)
191{
192 px4_ros2::Result px4_result = px4_ros2::Result::Success;
193 switch (result) {
194 case ExecutionResult::TREE_SUCCEEDED:
195 px4_result = px4_ros2::Result::Success;
196 break;
197 case ExecutionResult::TERMINATED_PREMATURELY:
198 px4_result = px4_ros2::Result::Deactivated;
199 break;
200 case ExecutionResult::TREE_FAILED:
201 case ExecutionResult::ERROR:
202 default:
203 px4_result = px4_ros2::Result::ModeFailureOther;
204 break;
205 }
206
207 const auto log_done = [this](px4_ros2::Result r) {
208 RCLCPP_INFO(node_.get_logger(), "Completion reaction finished (%s)", px4_ros2::resultToString(r));
209 };
210
211 switch (reaction) {
213 RCLCPP_INFO(node_.get_logger(), "Completion reaction: HOLD");
214 scheduleLoiter();
215 break;
217 RCLCPP_INFO(node_.get_logger(), "Completion reaction: RTL");
218 rtl(log_done);
219 break;
221 RCLCPP_INFO(node_.get_logger(), "Completion reaction: LAND");
222 land(log_done);
223 break;
225 RCLCPP_INFO(node_.get_logger(), "Completion reaction: DISARM");
226 disarm(log_done);
227 break;
229 RCLCPP_INFO(node_.get_logger(), "Completion reaction: COMPLETE (reporting owned mode completion)");
230 owned_mode_.finish(px4_result);
231 break;
233 RCLCPP_INFO(node_.get_logger(), "Completion reaction: NONE");
234 break;
235 }
236}
237
238void BehaviorModeExecutor::scheduleLoiter()
239{
240 scheduleMode(px4_ros2::ModeBase::kModeIDLoiter, [](px4_ros2::Result) {
241 // Loiter mode has no completion signal, so callback is no-op.
242 });
243}
244
245// #####################################################################################################################
246// ############################### BehaviorModeExecutorNode ###################################
247// #####################################################################################################################
248
249px4_ros2::ModeExecutorBase::Settings::Activation BehaviorModeExecutorNode::activationFromString(const std::string & str)
250{
251 using Activation = px4_ros2::ModeExecutorBase::Settings::Activation;
252 if (str == "armed") return Activation::ActivateOnlyWhenArmed;
253 if (str == "always") return Activation::ActivateAlways;
254 if (str == "immediately") return Activation::ActivateImmediately;
255 throw std::invalid_argument("Invalid activation '" + str + "' (expected one of: armed, always, immediately)");
256}
257
258BehaviorModeExecutorNode::BehaviorModeExecutorNode(const rclcpp::NodeOptions & options)
259: GenericTreeExecutorNode(
260 "behavior_mode_executor",
261 auto_apms_behavior_tree::TreeExecutorNodeOptions(options).enableStrictUnkownParameterRemoval(false)),
262 registration_handler_(getNodePtr())
263{
264 // Declares all behavior-specific parameters via generate_parameter_library. The listener is kept alive for the
265 // lifetime of the executor (captured by the config provider below) so its parameter-validation callback stays
266 // registered and get_params() keeps reflecting runtime changes. The behavior parameters are declared writable (not
267 // read_only), so they can be updated at runtime; the new values are picked up on the next activation (see
268 // BehaviorModeExecutor::onActivate). Only `activation` and `mode_name` are read only, because the owned mode is
269 // registered with the FMU once, here in the constructor.
270 const auto param_listener_ptr = std::make_shared<behavior_mode_executor_params::ParamListener>(getNodePtr());
271 const behavior_mode_executor_params::Params params = param_listener_ptr->get_params();
272
273 if (params.behavior.build_request.empty()) {
274 throw std::invalid_argument("Parameter 'behavior.build_request' must not be empty.");
275 }
276
277 // Builds a fresh Config from the current parameter values. Invoked by the executor on every activation so runtime
278 // parameter changes take effect. The reaction strings are constrained to valid values by the parameter validation
279 // (one_of<>), so reactionFromString never sees an invalid value here.
280 auto config_provider = [param_listener_ptr]() -> BehaviorModeExecutor::Config {
281 const behavior_mode_executor_params::Params p = param_listener_ptr->get_params();
282 BehaviorModeExecutor::Config config;
283 config.spec.build_request = p.behavior.build_request;
284 config.spec.entry_point = p.behavior.entry_point;
285 config.spec.node_manifest = p.behavior.node_manifest;
286 config.on_completion = BehaviorModeExecutor::reactionFromString(p.on_completion);
287 config.on_failure = BehaviorModeExecutor::reactionFromString(p.on_failure);
288 config.defer_failsafes = p.defer_failsafes;
289 return config;
290 };
291
292 const px4_ros2::ModeExecutorBase::Settings settings{activationFromString(params.activation)};
293
294 owned_mode_ptr_ = std::make_unique<BehaviorOwnedMode>(*getNodePtr(), px4_ros2::ModeBase::Settings{params.mode_name});
295
296 executor_ptr_ = std::make_unique<BehaviorModeExecutor>(*owned_mode_ptr_, settings, std::move(config_provider), *this);
297
298 // Build the behavior tree up front so the expensive tree construction (instantiating the behavior tree nodes' ROS 2
299 // waitables) happens here instead of on the activation path. If building fails, the exception propagates and the mode
300 // is never registered with the FMU.
301 executor_ptr_->prepareTree();
302
303 // Wait for the FMU and register the executor together with its owned mode
304 registration_handler_.registerMode(*executor_ptr_, params.mode_name);
305}
306
307void BehaviorModeExecutorNode::onTermination(const ExecutionResult & result)
308{
309 executor_ptr_->onExecutionResult(result);
310}
311
312} // namespace auto_apms_px4
313
314#include "rclcpp_components/register_node_macro.hpp"
315RCLCPP_COMPONENTS_REGISTER_NODE(auto_apms_px4::BehaviorModeExecutorNode)
Flexible and configurable ROS 2 behavior tree executor node.
@ TERMINATE
Halt the currently executing tree and terminate the execution routine.
Data structure for information about which behavior tree node plugin to load and how to configure the...
ROS 2 component that hosts a BehaviorModeExecutor and its owned mode on an in-process behavior tree e...
static CompletionReaction reactionFromString(const std::string &str)
Parse a string into a CompletionReaction.
CompletionReaction
Reaction performed when a behavior terminates or fails, using the in-charge executor API.
@ HOLD
Schedule the owned mode (hold position) and stay in charge.
@ COMPLETE
Report the owned mode as completed to the FMU (relinquish charge).
void onExecutionResult(ExecutionResult result)
Handle the termination of the behavior running on the in-process executor.
BehaviorModeExecutor(BehaviorOwnedMode &owned_mode, const px4_ros2::ModeExecutorBase::Settings &settings, std::function< Config()> config_provider, auto_apms_behavior_tree::GenericTreeExecutorNode &engine)
Constructor.
void prepareTree()
Build the configured behavior tree and keep it detached, ready to be executed on the next activation.
Registration placeholder PX4 mode owned by a BehaviorModeExecutor.
Implementation of PX4 mode peers offered by px4_ros2_cpp enabling integration with AutoAPMS.
std::string entry_point
Single point of entry for behavior execution.
std::string build_request
Behavior build request (e.g. a registered behavior resource identity or XML).
std::string node_manifest
Encoded node manifest specifying additional nodes to load.