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