AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
mission_builder_base.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// https://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_mission/mission_builder_base.hpp"
16
17#include "auto_apms_util/exceptions.hpp"
18
19namespace auto_apms_mission
20{
21
22const std::string MissionBuildHandlerBase::ORCHESTRATOR_EXECUTOR_NAME = _AUTO_APMS_MISSION__ORCHESTRATOR_EXECUTOR_NAME;
23const std::string MissionBuildHandlerBase::MISSION_EXECUTOR_NAME = _AUTO_APMS_MISSION__MISSION_EXECUTOR_NAME;
24const std::string MissionBuildHandlerBase::EVENT_MONITOR_EXECUTOR_NAME =
25 _AUTO_APMS_MISSION__EVENT_MONITOR_EXECUTOR_NAME;
26const std::string MissionBuildHandlerBase::EVENT_HANDLER_EXECUTOR_NAME =
27 _AUTO_APMS_MISSION__EVENT_HANDLER_EXECUTOR_NAME;
28
29bool MissionBuildHandlerBase::setBuildRequest(
30 const std::string & build_request, const std::string & entrypoint, const NodeManifest & node_manifest)
31{
32 if (!entrypoint.empty()) {
33 RCLCPP_WARN(logger_, "Argument entrypoint is not empty. Custom entrypoints are not supported and will be ignored.");
34 }
35 if (!node_manifest.empty()) {
36 RCLCPP_WARN(
37 logger_, "Argument node_manifest is not empty. Custom node manifests are not supported and will be ignored.");
38 }
39 try {
40 mission_config_ = createMissionConfig(build_request);
41 } catch (const auto_apms_util::exceptions::ResourceIdentityFormatError & e) {
42 RCLCPP_WARN(logger_, "%s", e.what());
43 return false;
44 } catch (const auto_apms_util::exceptions::ResourceError & e) {
45 RCLCPP_WARN(logger_, "%s", e.what());
46 return false;
47 } catch (const auto_apms_util::exceptions::YAMLFormatError & e) {
48 RCLCPP_WARN(logger_, "Invalid YAML format of mission configuration '%s': %s", build_request.c_str(), e.what());
49 return false;
50 }
51
52 // Validate all tree resource identities and replace all empty tree names with the corresponding root tree name
53 auto add_tree_name_to_identity = [this](TreeResource::Identity & identity) {
54 if (identity.tree_name.empty()) {
55 TreeResource r(identity);
56 identity.tree_name = r.getRootTreeName();
57 }
58 };
59 try {
60 for (TreeResource::Identity & identity : mission_config_.bringup) add_tree_name_to_identity(identity);
61 for (TreeResource::Identity & identity : mission_config_.mission) add_tree_name_to_identity(identity);
62 for (auto & [monitor_id, handler_id] : mission_config_.contingency) {
63 add_tree_name_to_identity(monitor_id);
64 add_tree_name_to_identity(handler_id);
65 }
66 for (auto & [monitor_id, handler_id] : mission_config_.emergency) {
67 add_tree_name_to_identity(monitor_id);
68 add_tree_name_to_identity(handler_id);
69 }
70 for (TreeResource::Identity & identity : mission_config_.shutdown) add_tree_name_to_identity(identity);
71 } catch (const auto_apms_util::exceptions::ResourceError & e) {
72 RCLCPP_WARN(logger_, "%s", e.what());
73 return false;
74 }
75
76 return true;
77}
78
79MissionBuildHandlerBase::TreeDocument::TreeElement MissionBuildHandlerBase::buildTree(
80 TreeDocument & doc, TreeBlackboard & bb)
81{
82 using namespace auto_apms_behavior_tree::model;
83
84 // Load orchestrator tree
85 RCLCPP_DEBUG(logger_, "Loading orchestrator tree.");
86 TreeDocument::TreeElement root_tree =
87 doc.newTreeFromResource("tree__internal/auto_apms_mission::orchestrator_base::MissionOrchestrator").makeRoot();
88
89 RCLCPP_DEBUG(logger_, "Configuring orchestrator root blackboard.");
90 configureOrchestratorRootBlackboard(bb);
91
92 TreeDocument::TreeElement is_contingency_tree = doc.getTree("__IsContingency__").removeChildren();
93 TreeDocument::TreeElement is_emergency_tree = doc.getTree("__IsEmergency__").removeChildren();
94
95 if (mission_config_.contingency.empty()) {
96 is_contingency_tree.insertNode<model::AlwaysFailure>();
97 } else {
98 model::Fallback fallback = is_contingency_tree.insertNode<model::Fallback>();
99 for (const auto & [monitor_id, _] : mission_config_.contingency) {
100 fallback.insertNode<model::ScriptCondition>().set_code("event_id == '" + monitor_id.str() + "'");
101 }
102 }
103
104 if (mission_config_.emergency.empty()) {
105 is_emergency_tree.insertNode<model::AlwaysFailure>();
106 } else {
107 model::Fallback fallback = is_emergency_tree.insertNode<model::Fallback>();
108 for (const auto & [monitor_id, _] : mission_config_.emergency) {
109 fallback.insertNode<model::ScriptCondition>().set_code("event_id == '" + monitor_id.str() + "'");
110 }
111 }
112
113 // Bring up
114 if (!mission_config_.bringup.empty()) {
115 RCLCPP_DEBUG(logger_, "Creating bringup subtree.");
116 TreeDocument::TreeElement bringup_tree = doc.getTree("__BringUp__");
117 buildBringUp(bringup_tree, mission_config_.bringup);
118 if (const BT::Result res = bringup_tree.verify(); !res) {
119 throw auto_apms_behavior_tree::exceptions::TreeBuildHandlerError("Bringup tree is not valid: " + res.error());
120 }
121 }
122
123 // Mission
124 RCLCPP_DEBUG(logger_, "Creating mission subtree.");
125 TreeDocument::TreeElement mission_tree = doc.getTree("__RunMission__");
126 buildMission(mission_tree, mission_config_.mission);
127 if (const BT::Result res = mission_tree.verify(); !res) {
128 throw auto_apms_behavior_tree::exceptions::TreeBuildHandlerError("Mission tree is not valid: " + res.error());
129 }
130
131 // Event monitor
132 if (!mission_config_.contingency.empty() || !mission_config_.emergency.empty()) {
133 RCLCPP_DEBUG(logger_, "Creating event monitor subtree.");
134 TreeDocument::TreeElement event_monitor_tree = doc.getTree("__MonitorEvents__");
135 buildEventMonitor(event_monitor_tree, mission_config_.contingency, mission_config_.emergency);
136 if (const BT::Result res = event_monitor_tree.verify(); !res) {
137 throw auto_apms_behavior_tree::exceptions::TreeBuildHandlerError(
138 "Event monitor tree is not valid: " + res.error());
139 }
140 }
141
142 // Contingency Handling
143 if (!mission_config_.contingency.empty()) {
144 RCLCPP_DEBUG(logger_, "Creating contingency handler subtree.");
145 TreeDocument::TreeElement contingency_tree = doc.getTree("__HandleContingency__");
146 buildContingencyHandling(contingency_tree, mission_config_.contingency);
147 if (const BT::Result res = contingency_tree.verify(); !res) {
148 throw auto_apms_behavior_tree::exceptions::TreeBuildHandlerError(
149 "Contingency handling tree is not valid: " + res.error());
150 }
151 }
152
153 // Emergency Handling
154 if (!mission_config_.emergency.empty()) {
155 RCLCPP_DEBUG(logger_, "Creating emergency handler subtree.");
156 TreeDocument::TreeElement emergency_tree = doc.getTree("__HandleEmergency__");
157 buildEmergencyHandling(emergency_tree, mission_config_.emergency);
158 if (const BT::Result res = emergency_tree.verify(); !res) {
159 throw auto_apms_behavior_tree::exceptions::TreeBuildHandlerError(
160 "Emergency handling tree is not valid: " + res.error());
161 }
162 }
163
164 // Shut down
165 if (!mission_config_.shutdown.empty()) {
166 RCLCPP_DEBUG(logger_, "Creating shutdown subtree.");
167 TreeDocument::TreeElement shutdown_tree = doc.getTree("__ShutDown__");
168 buildShutDown(shutdown_tree, mission_config_.shutdown);
169 if (const BT::Result res = shutdown_tree.verify(); !res) {
170 throw auto_apms_behavior_tree::exceptions::TreeBuildHandlerError("Shutdown tree is not valid: " + res.error());
171 }
172 }
173
174 // Write tree for debugging purposes
175 // doc.writeToFile("/home/robin/Desktop/px4-ros2-env/src/dep/auto-apms/test.xml");
176
177 return root_tree;
178}
179
180void MissionBuildHandlerBase::buildBringUp(
181 TreeDocument::TreeElement & sub_tree, const std::vector<TreeResource::Identity> & trees)
182{
183 sub_tree.removeChildren();
184 for (const TreeResource::Identity & r : trees) {
185 RCLCPP_DEBUG(logger_, "Adding bringup tree '%s'.", r.str().c_str());
186 sub_tree.insertTreeFromResource(r);
187 }
188}
189
190void MissionBuildHandlerBase::buildMission(
191 TreeDocument::TreeElement & sub_tree, const std::vector<TreeResource::Identity> & trees)
192{
193 sub_tree.removeChildren();
194 for (const TreeResource::Identity & r : trees) {
195 RCLCPP_DEBUG(logger_, "Adding mission tree '%s'.", r.str().c_str());
196 sub_tree.insertTreeFromResource(r);
197 }
198}
199
200void MissionBuildHandlerBase::buildEventMonitor(
201 TreeDocument::TreeElement & sub_tree,
202 const std::vector<std::pair<TreeResource::Identity, TreeResource::Identity>> & contingencies,
203 const std::vector<std::pair<TreeResource::Identity, TreeResource::Identity>> & emergencies)
204{
205 std::vector<TreeResource::Identity> sorted_monitor_ids;
206
207 // Emergencies have higher priority than contingencies (they are inserted to the vector first)
208 for (const auto & [monitor_id, _] : emergencies) {
209 sorted_monitor_ids.push_back(monitor_id);
210 }
211 for (const auto & [monitor_id, _] : contingencies) {
212 if (!auto_apms_util::contains(sorted_monitor_ids, monitor_id)) sorted_monitor_ids.push_back(monitor_id);
213 }
214
215 // Assemble an extra document that holds all monitor subtrees
216 TreeDocument monitor_doc;
217 for (const TreeResource::Identity & monitor_id : sorted_monitor_ids) {
218 if (!monitor_doc.hasTreeName(monitor_id.str())) {
219 RCLCPP_DEBUG(logger_, "Adding event monitor tree '%s'.", monitor_id.str().c_str());
220 RCLCPP_DEBUG(
221 logger_, "Identity has file stem '%s' and tree name '%s'.", monitor_id.file_stem.c_str(),
222 monitor_id.tree_name.c_str());
223 monitor_doc.newTreeFromResource(monitor_id).setName(monitor_id.str());
224 }
225 }
226
227 model::Fallback fallback = sub_tree.removeChildren().insertNode<model::Fallback>();
228 for (const TreeResource::Identity & monitor_id : sorted_monitor_ids) {
229 // We want the monitor to have its own blackboard, so we create a subtree node
230 const std::string monitor_tree_name = monitor_id.str();
231 model::SubTree subtree_node = fallback.getParentDocument().hasTreeName(monitor_tree_name)
232 ? fallback.insertNode<model::SubTree>(monitor_tree_name)
233 : fallback.insertNode<model::SubTree>(monitor_doc.getTree(monitor_tree_name));
234 subtree_node.setConditionalScript(BT::PostCond::ON_SUCCESS, "event_id := '" + monitor_tree_name + "'");
235 }
236}
237
238void MissionBuildHandlerBase::buildContingencyHandling(
239 TreeDocument::TreeElement & sub_tree,
240 const std::vector<std::pair<TreeResource::Identity, TreeResource::Identity>> & contingencies)
241{
242 // Assemble an extra document that holds all handler subtrees
243 TreeDocument handler_doc;
244 for (const auto & [_, handler_id] : contingencies) {
245 if (!handler_doc.hasTreeName(handler_id.str())) {
246 RCLCPP_DEBUG(logger_, "Adding contingency handler tree '%s'.", handler_id.str().c_str());
247 handler_doc.newTreeFromResource(handler_id).setName(handler_id.str());
248 }
249 }
250
251 model::ReactiveFallback fallback = sub_tree.removeChildren().insertNode<model::ReactiveFallback>();
252
253 // With the highest priority (first child) we acknowledge when the event is reset (e.g. we abort the current action
254 // and resume)
255 fallback.insertNode<model::AlwaysFailure>()
256 .setName("ResetEventHandler")
257 .setConditionalScript(BT::PreCond::SUCCESS_IF, "event_id == ''");
258
259 for (const auto & [monitor_id, handler_id] : contingencies) {
260 model::AsyncSequence seq =
261 fallback.insertNode<model::AsyncSequence>().setName("EventHandler (" + handler_id.str() + ")");
262 seq.insertNode<model::ScriptCondition>().set_code("event_id == '" + monitor_id.str() + "'");
263
264 // We want the handler to have its own blackboard, so we create a subtree node
265 const std::string handler_tree_name = handler_id.str();
266 if (seq.getParentDocument().hasTreeName(handler_tree_name)) {
267 seq.insertNode<model::SubTree>(handler_tree_name);
268 } else {
269 // Automatically add the handler tree if it doesn't exist yet
270 seq.insertNode<model::SubTree>(handler_doc.getTree(handler_tree_name));
271 }
272
273 // Once the contingency handler subtree is done, the tree idles as long as the respective event is still active
274 seq.insertNode<model::KeepRunningUntilFailure>()
275 .insertNode<model::ScriptCondition>()
276 .setName("IsEventStillActive")
277 .set_code("event_id == '" + monitor_id.str() + "'");
278 }
279}
280
281void MissionBuildHandlerBase::buildEmergencyHandling(
282 TreeDocument::TreeElement & sub_tree,
283 const std::vector<std::pair<TreeResource::Identity, TreeResource::Identity>> & emergencies)
284{
285 // Assemble an extra document that holds all handler subtrees
286 TreeDocument handler_doc;
287 for (const auto & [_, handler_id] : emergencies) {
288 if (!handler_doc.hasTreeName(handler_id.str())) {
289 RCLCPP_DEBUG(logger_, "Adding emergency handler tree '%s'.", handler_id.str().c_str());
290 handler_doc.newTreeFromResource(handler_id).setName(handler_id.str());
291 }
292 }
293
294 model::ReactiveFallback fallback = sub_tree.removeChildren().insertNode<model::ReactiveFallback>();
295 for (const auto & [monitor_id, handler_id] : emergencies) {
296 model::AsyncSequence seq =
297 fallback.insertNode<model::AsyncSequence>().setName("EventHandler (" + handler_id.str() + ")");
298 seq.insertNode<model::ScriptCondition>().set_code("event_id == '" + monitor_id.str() + "'");
299
300 // We want the handler to have its own blackboard, so we create a subtree node
301 const std::string handler_tree_name = handler_id.str();
302 if (seq.getParentDocument().hasTreeName(handler_tree_name)) {
303 seq.insertNode<model::SubTree>(handler_tree_name);
304 } else {
305 // Automatically add the handler tree if it doesn't exist yet
306 seq.insertNode<model::SubTree>(handler_doc.getTree(handler_tree_name));
307 }
308 }
309}
310
311void MissionBuildHandlerBase::buildShutDown(
312 TreeDocument::TreeElement & sub_tree, const std::vector<TreeResource::Identity> & trees)
313{
314 sub_tree.removeChildren();
315 for (const TreeResource::Identity & r : trees) {
316 RCLCPP_DEBUG(logger_, "Adding shutdown tree '%s'.", r.str().c_str());
317 sub_tree.insertTreeFromResource(r);
318 }
319}
320
321void MissionBuildHandlerBase::configureOrchestratorRootBlackboard(TreeBlackboard & /*bb*/) {}
322
323} // namespace auto_apms_mission
const rclcpp::Logger logger_
ROS 2 logger initialized with the name of the build handler.
NodeElement & setConditionalScript(BT::PreCond type, const Script &script)
Specify a script that is evaluated before this node is ticked.
bool contains(const ContainerT< ValueT, AllocatorT > &c, const ValueT &val)
Check whether a particular container structure contains a value.
Definition container.hpp:36
Mission design utilities incorporating behavior trees to model the complexity of arbitrary operations...