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.
67 if (!isInCharge()) {
68 RCLCPP_INFO(node_.get_logger(), "No longer in charge. Skipping completion reaction");
69 return;
70 }
71
72 const CompletionReaction reaction =
73 result == ExecutionResult::TREE_SUCCEEDED ? config_.on_completion : config_.on_failure;
74 performReaction(reaction, result);
75}
76
78{
79 if (str == "hold") return CompletionReaction::HOLD;
80 if (str == "rtl") return CompletionReaction::RTL;
81 if (str == "land") return CompletionReaction::LAND;
82 if (str == "disarm") return CompletionReaction::DISARM;
83 if (str == "complete") return CompletionReaction::COMPLETE;
84 if (str == "none") return CompletionReaction::NONE;
85 throw std::invalid_argument(
86 "Invalid completion reaction '" + str + "' (expected one of: hold, rtl, land, disarm, complete, none)");
87}
88
89void BehaviorModeExecutor::onActivate()
90{
91 // Refresh the configuration from the current parameter values so runtime changes (e.g. via `ros2 param set`) take
92 // effect on this activation. The registration-time parameters (activation policy, mode name) are latched when the
93 // mode is registered with the FMU and are not part of this snapshot.
94 config_ = config_provider_();
95
96 if (config_.spec.build_request.empty()) {
97 RCLCPP_ERROR(node_.get_logger(), "Cannot start behavior: parameter 'behavior.build_request' must not be empty");
98 onExecutionResult(ExecutionResult::ERROR);
99 return;
100 }
101
102 RCLCPP_INFO(
103 node_.get_logger(), "Behavior executor put in charge. Starting behavior '%s'", config_.spec.build_request.c_str());
104
105 if (config_.defer_failsafes) {
106 if (deferFailsafesSync(true)) {
107 RCLCPP_INFO(node_.get_logger(), "Failsafes are now being deferred while the behavior is running");
108 } else {
109 RCLCPP_WARN(node_.get_logger(), "Failed to enable failsafe deferral");
110 }
111 }
112
113 // Publish the executor's VehicleCommand source component on the global blackboard so that ownership-aware behavior
114 // tree nodes (e.g. SendCmdSetNavState) can attribute their commands to this executor. Stored as int to match the type
115 // read by the SendCmdSetNavState node. The global key already carries the '@' prefix; on the global blackboard (which
116 // is its own root) this resolves to the same entry the tree nodes read transitively.
117 const int source_component = static_cast<int>(px4_msgs::msg::VehicleCommand::COMPONENT_MODE_EXECUTOR_START) + id();
118 behavior_executor_.getGlobalBlackboardPtr()->set(AUTO_APMS_PX4_SOURCE_COMPONENT_GLOBAL_KEY, source_component);
119
120 // Flag that a mode executor is in charge so SendVehicleCommand routes commands through the mode-executor command
121 // topic. Set before the behavior tree is built (below), so the flag is already readable when its nodes construct.
122 behavior_executor_.getGlobalBlackboardPtr()->set(AUTO_APMS_PX4_MODE_EXECUTOR_ACTIVE_GLOBAL_KEY, true);
123
124 auto_apms_behavior_tree::core::NodeManifest node_manifest;
125 if (!config_.spec.node_manifest.empty()) {
126 node_manifest = auto_apms_behavior_tree::core::NodeManifest::decode(config_.spec.node_manifest);
127 }
128
129 try {
130 behavior_executor_.startExecution(config_.spec.build_request, config_.spec.entry_point, node_manifest);
131 } catch (const std::exception & e) {
132 RCLCPP_ERROR(node_.get_logger(), "Failed to start behavior: %s", e.what());
133 onExecutionResult(ExecutionResult::ERROR);
134 }
135}
136
137void BehaviorModeExecutor::onDeactivate(DeactivateReason reason)
138{
139 const char * reason_str = reason == DeactivateReason::FailsafeActivated ? "failsafe activated" : "other";
140 RCLCPP_INFO(node_.get_logger(), "Behavior executor deactivating (reason: %s)", reason_str);
141
142 // The FMU has already taken over (pilot override or failsafe). Halting the behavior is cleanup, not safety.
143 if (behavior_executor_.isBusy()) {
144 RCLCPP_INFO(node_.get_logger(), "Behavior is still running. Terminating it now...");
146 }
147
148 if (config_.defer_failsafes) {
149 deferFailsafesSync(false);
150 }
151}
152
153void BehaviorModeExecutor::performReaction(CompletionReaction reaction, ExecutionResult result)
154{
155 px4_ros2::Result px4_result = px4_ros2::Result::Success;
156 switch (result) {
157 case ExecutionResult::TREE_SUCCEEDED:
158 px4_result = px4_ros2::Result::Success;
159 break;
160 case ExecutionResult::TERMINATED_PREMATURELY:
161 px4_result = px4_ros2::Result::Deactivated;
162 break;
163 case ExecutionResult::TREE_FAILED:
164 case ExecutionResult::ERROR:
165 default:
166 px4_result = px4_ros2::Result::ModeFailureOther;
167 break;
168 }
169
170 const auto log_done = [this](px4_ros2::Result r) {
171 RCLCPP_INFO(node_.get_logger(), "Completion reaction finished (%s)", px4_ros2::resultToString(r));
172 };
173
174 switch (reaction) {
176 RCLCPP_INFO(node_.get_logger(), "Completion reaction: HOLD");
177 scheduleLoiter();
178 break;
180 RCLCPP_INFO(node_.get_logger(), "Completion reaction: RTL");
181 rtl(log_done);
182 break;
184 RCLCPP_INFO(node_.get_logger(), "Completion reaction: LAND");
185 land(log_done);
186 break;
188 RCLCPP_INFO(node_.get_logger(), "Completion reaction: DISARM");
189 disarm(log_done);
190 break;
192 RCLCPP_INFO(node_.get_logger(), "Completion reaction: COMPLETE (reporting owned mode completion)");
193 owned_mode_.finish(px4_result);
194 break;
196 RCLCPP_INFO(node_.get_logger(), "Completion reaction: NONE");
197 break;
198 }
199}
200
201void BehaviorModeExecutor::scheduleLoiter()
202{
203 scheduleMode(px4_ros2::ModeBase::kModeIDLoiter, [](px4_ros2::Result) {
204 // Loiter mode has no completion signal, so callback is no-op.
205 });
206}
207
208// #####################################################################################################################
209// ############################### BehaviorModeExecutorNode ###################################
210// #####################################################################################################################
211
212px4_ros2::ModeExecutorBase::Settings::Activation BehaviorModeExecutorNode::activationFromString(const std::string & str)
213{
214 using Activation = px4_ros2::ModeExecutorBase::Settings::Activation;
215 if (str == "armed") return Activation::ActivateOnlyWhenArmed;
216 if (str == "always") return Activation::ActivateAlways;
217 if (str == "immediately") return Activation::ActivateImmediately;
218 throw std::invalid_argument("Invalid activation '" + str + "' (expected one of: armed, always, immediately)");
219}
220
221BehaviorModeExecutorNode::BehaviorModeExecutorNode(const rclcpp::NodeOptions & options)
222: GenericTreeExecutorNode(
223 "behavior_mode_executor",
224 auto_apms_behavior_tree::TreeExecutorNodeOptions(options).enableStrictUnkownParameterRemoval(false)),
225 registration_handler_(getNodePtr())
226{
227 // Declares all behavior-specific parameters via generate_parameter_library. The listener is kept alive for the
228 // lifetime of the executor (captured by the config provider below) so its parameter-validation callback stays
229 // registered and get_params() keeps reflecting runtime changes. The behavior parameters are declared writable (not
230 // read_only), so they can be updated at runtime; the new values are picked up on the next activation (see
231 // BehaviorModeExecutor::onActivate). Only `activation` and `mode_name` are read only, because the owned mode is
232 // registered with the FMU once, here in the constructor.
233 const auto param_listener_ptr = std::make_shared<behavior_mode_executor_params::ParamListener>(getNodePtr());
234 const behavior_mode_executor_params::Params params = param_listener_ptr->get_params();
235
236 if (params.behavior.build_request.empty()) {
237 throw std::invalid_argument("Parameter 'behavior.build_request' must not be empty.");
238 }
239
240 // Builds a fresh Config from the current parameter values. Invoked by the executor on every activation so runtime
241 // parameter changes take effect. The reaction strings are constrained to valid values by the parameter validation
242 // (one_of<>), so reactionFromString never sees an invalid value here.
243 auto config_provider = [param_listener_ptr]() -> BehaviorModeExecutor::Config {
244 const behavior_mode_executor_params::Params p = param_listener_ptr->get_params();
245 BehaviorModeExecutor::Config config;
246 config.spec.build_request = p.behavior.build_request;
247 config.spec.entry_point = p.behavior.entry_point;
248 config.spec.node_manifest = p.behavior.node_manifest;
249 config.on_completion = BehaviorModeExecutor::reactionFromString(p.on_completion);
250 config.on_failure = BehaviorModeExecutor::reactionFromString(p.on_failure);
251 config.defer_failsafes = p.defer_failsafes;
252 return config;
253 };
254
255 const px4_ros2::ModeExecutorBase::Settings settings{activationFromString(params.activation)};
256
257 owned_mode_ptr_ = std::make_unique<BehaviorOwnedMode>(*getNodePtr(), px4_ros2::ModeBase::Settings{params.mode_name});
258
259 executor_ptr_ = std::make_unique<BehaviorModeExecutor>(*owned_mode_ptr_, settings, std::move(config_provider), *this);
260
261 // Wait for the FMU and register the executor together with its owned mode
262 registration_handler_.registerMode(*executor_ptr_, params.mode_name);
263}
264
265void BehaviorModeExecutorNode::onTermination(const ExecutionResult & result)
266{
267 executor_ptr_->onExecutionResult(result);
268}
269
270} // namespace auto_apms_px4
271
272#include "rclcpp_components/register_node_macro.hpp"
273RCLCPP_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.
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.
Registration placeholder PX4 mode owned by a BehaviorModeExecutor.
Implementation of PX4 mode peers offered by px4_ros2_cpp enabling integration with AutoAPMS.
std::string build_request
Behavior build request (e.g. a registered behavior resource identity or XML).