AutoAPMS
Resilient Robot Mission Management
Loading...
Searching...
No Matches
executor_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_behavior_tree/executor/executor_base.hpp"
16
17#include <chrono>
18
19#include "auto_apms_util/container.hpp"
20#include "auto_apms_util/logging.hpp"
21
23{
24
26 rclcpp::Node::SharedPtr node_ptr, rclcpp::CallbackGroup::SharedPtr tree_node_callback_group_ptr)
27: node_ptr_(node_ptr),
28 logger_(node_ptr_->get_logger()),
29 tree_node_waitables_callback_group_ptr_(tree_node_callback_group_ptr),
30 tree_node_waitables_executor_ptr_(rclcpp::executors::SingleThreadedExecutor::make_shared()),
31 global_blackboard_ptr_(TreeBlackboard::create()),
32 control_command_(ControlCommand::RUN),
33 execution_stopped_(true)
34{
36
37 // The behavior tree node callback group is intended to be passed to all nodes and used when adding subscriptions,
38 // publishers, services, actions etc. It is associated with a standalone single threaded executor, which is spun in
39 // between ticks, to make sure that pending work is executed while the main tick routine is sleeping.
40 if (!tree_node_waitables_callback_group_ptr_) {
41 tree_node_waitables_callback_group_ptr_ =
42 node_ptr_->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive, false);
43 }
44
45 // Add the behavior tree node callback group to the internal executor
46 tree_node_waitables_executor_ptr_->add_callback_group(
47 tree_node_waitables_callback_group_ptr_, get_node_base_interface());
48}
49
50std::shared_future<TreeExecutorBase::ExecutionResult> TreeExecutorBase::startExecution(
51 TreeConstructor make_tree, double tick_rate_sec, int groot2_port)
52{
53 if (isBusy()) {
54 throw exceptions::TreeExecutorError(
55 "Cannot start execution with tree '" + getTreeName() + "' currently executing.");
56 }
57
58 TreeBlackboardSharedPtr main_tree_bb_ptr = TreeBlackboard::create(global_blackboard_ptr_);
59 try {
60 tree_ptr_.reset(new Tree(make_tree(main_tree_bb_ptr)));
61 } catch (const std::exception & e) {
62 throw exceptions::TreeBuildError(
63 "Cannot start execution because creating the tree failed: " + std::string(e.what()));
64 }
65
66 // Groot2 publisher
67 groot2_publisher_ptr_.reset();
68 if (groot2_port != -1) {
69 try {
70 groot2_publisher_ptr_ = std::make_unique<BT::Groot2Publisher>(*tree_ptr_, groot2_port);
71 } catch (const std::exception & e) {
72 throw exceptions::TreeExecutorError(
73 "Failed to initialize Groot2 publisher with port " + std::to_string(groot2_port) + ": " + e.what());
74 }
75 }
76
77 // Tree state observer
78 state_observer_ptr_.reset();
79 state_observer_ptr_ = std::make_unique<TreeStateObserver>(*tree_ptr_, logger_);
80 state_observer_ptr_->enableTransitionToIdle(false);
81
82 /* Start execution timer */
83
84 // Reset state variables
85 prev_execution_state_ = getExecutionState();
86 control_command_ = ControlCommand::RUN;
87 termination_reason_ = "";
88 execution_stopped_ = true;
89
90 // Create promise for asynchronous execution and configure termination callback
91 auto promise_ptr = std::make_shared<std::promise<ExecutionResult>>();
92 TerminationCallback termination_callback = [this, promise_ptr](ExecutionResult result, const std::string & msg) {
93 RCLCPP_INFO(
94 logger_, "Terminating tree '%s' from state %s.", getTreeName().c_str(), toStr(getExecutionState()).c_str());
95 if (result == ExecutionResult::ERROR) {
96 RCLCPP_ERROR(logger_, "Termination reason: %s", msg.c_str());
97 } else {
98 RCLCPP_INFO(logger_, "Termination reason: %s", msg.c_str());
99 }
100 onTermination(result); // is evaluated before the timer is cancelled, which means the execution state has not
101 // changed yet during the callback and can be evaluated to inspect the terminal state.
102 promise_ptr->set_value(result);
103 execution_timer_ptr_->cancel();
104 tree_ptr_.reset(); // Release the memory allocated by the tree
105 };
106
107 // NOTE: The main callback timer is using the node's default callback group
108 const std::chrono::nanoseconds period =
109 std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::duration<double>(tick_rate_sec));
110 execution_timer_ptr_ = node_ptr_->create_wall_timer(period, [this, period, termination_callback]() {
111 // Collect and process incoming messages before ticking
112 tree_node_waitables_executor_ptr_->spin_all(period);
113
114 // Tick the tree, evaluate control commands and handle the returned tree status
115 tick_callback_(termination_callback);
116 });
117 return promise_ptr->get_future();
118}
119
120void TreeExecutorBase::tick_callback_(TerminationCallback termination_callback)
121{
122 const ExecutionState this_execution_state = getExecutionState();
123 if (prev_execution_state_ != this_execution_state) {
124 RCLCPP_DEBUG(
125 logger_, "Executor for tree '%s' changed state from '%s' to '%s'.", getTreeName().c_str(),
126 toStr(prev_execution_state_).c_str(), toStr(this_execution_state).c_str());
127 prev_execution_state_ = this_execution_state;
128 }
129
130 /* Evaluate control command */
131
132 execution_stopped_ = false;
133 bool do_on_tick = true;
134 switch (control_command_) {
136 execution_stopped_ = true;
137 return;
139 if (this_execution_state == ExecutionState::STARTING) {
140 // Evaluate initial tick callback before ticking for the first time since the timer has been created
141 if (!onInitialTick()) {
142 do_on_tick = false;
143 termination_reason_ = "onInitialTick() returned false.";
144 }
145 }
146 // Evaluate tick callback everytime before actually ticking.
147 // This also happens the first time except if onInitialTick() returned false
148 if (do_on_tick) {
149 if (onTick()) {
150 break;
151 } else {
152 termination_reason_ = "onTick() returned false.";
153 }
154 }
155
156 // Fall through to terminate if any of the callbacks returned false
157 [[fallthrough]];
159 if (this_execution_state == ExecutionState::HALTED) {
160 termination_callback(
162 termination_reason_.empty() ? "Control command was set to TERMINATE." : termination_reason_);
163 return;
164 }
165
166 // Fall through to halt tree before termination
167 [[fallthrough]];
169 // Check if already halted
170 if (this_execution_state != ExecutionState::HALTED) {
171#ifdef AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_THROW_ON_TICK_ERROR
172 tree_ptr_->haltTree();
173#else
174 try {
175 tree_ptr_->haltTree();
176 } catch (const std::exception & e) {
177 termination_callback(
179 "Error during haltTree() on command " + toStr(control_command_) + ": " + std::string(e.what()));
180 }
181#endif
182 }
183 return;
184 default:
185 throw std::logic_error(
186 "Handling control command " + std::to_string(static_cast<int>(control_command_)) + " '" +
187 toStr(control_command_) + "' is not implemented.");
188 }
189
190 /* Tick the tree instance */
191
192 BT::NodeStatus bt_status = BT::NodeStatus::IDLE;
193#ifdef AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_THROW_ON_TICK_ERROR
194 bt_status = tree_ptr_->tickExactlyOnce();
195#else
196 try {
197 // It is important to tick EXACTLY once to prevent loops induced by BT nodes from blocking
198 bt_status = tree_ptr_->tickExactlyOnce();
199 } catch (const std::exception & e) {
200 std::string msg = "Ran into an exception during tick: " + std::string(e.what());
201 try {
202 tree_ptr_->haltTree(); // Try to halt tree before aborting
203 } catch (const std::exception & e) {
204 msg += "\nDuring haltTree(), another exception occurred: " + std::string(e.what());
205 }
206 termination_callback(ExecutionResult::ERROR, msg);
207 return;
208 }
209#endif
210
211 if (!afterTick()) {
212 termination_callback(ExecutionResult::TERMINATED_PREMATURELY, "afterTick() returned false.");
213 return;
214 }
215
216 if (bt_status == BT::NodeStatus::RUNNING) return;
217
218 /* Determine how to handle the behavior tree execution result */
219
220 if (!(bt_status == BT::NodeStatus::SUCCESS || bt_status == BT::NodeStatus::FAILURE)) {
221 throw std::logic_error(
222 "bt_status is " + BT::toStr(bt_status) + ". Must be one of SUCCESS or FAILURE at this point.");
223 }
224 const bool success = bt_status == BT::NodeStatus::SUCCESS;
225 switch (onTreeExit(success)) {
227 termination_callback(
229 "Terminated on tree result " + BT::toStr(bt_status) + ".");
230 return;
232 control_command_ = ControlCommand::RUN;
233 return;
234 }
235
236 throw std::logic_error("Execution routine is not intended to proceed to this statement.");
237}
238
239bool TreeExecutorBase::onInitialTick() { return true; }
240
241bool TreeExecutorBase::onTick() { return true; }
242
243bool TreeExecutorBase::afterTick() { return true; }
244
249
251
252void TreeExecutorBase::setControlCommand(ControlCommand cmd) { control_command_ = cmd; }
253
254bool TreeExecutorBase::isBusy() { return execution_timer_ptr_ && !execution_timer_ptr_->is_canceled(); }
255
257{
258 if (isBusy()) {
259 if (!tree_ptr_) throw std::logic_error("tree_ptr_ cannot be nullptr when execution is started.");
260 if (tree_ptr_->rootNode()->status() == BT::NodeStatus::IDLE) {
261 // The root node being IDLE here means that the tree hasn't been ticked yet since its creation or was halted
262 return execution_stopped_ ? ExecutionState::STARTING : ExecutionState::HALTED;
263 }
264 return execution_stopped_ ? ExecutionState::PAUSED : ExecutionState::RUNNING;
265 }
267}
268
270{
271 if (tree_ptr_) return tree_ptr_->subtrees[0]->tree_ID;
272 return "NO_TREE_NAME";
273}
274
275TreeBlackboardSharedPtr TreeExecutorBase::getGlobalBlackboardPtr() { return global_blackboard_ptr_; }
276
277void TreeExecutorBase::clearGlobalBlackboard() { global_blackboard_ptr_ = TreeBlackboard::create(); }
278
280{
281 if (!state_observer_ptr_) {
282 throw exceptions::TreeExecutorError("Cannot get state observer because executor is not busy.");
283 }
284 return *state_observer_ptr_;
285}
286
287rclcpp::Node::SharedPtr TreeExecutorBase::getNodePtr() { return node_ptr_; }
288
289rclcpp::node_interfaces::NodeBaseInterface::SharedPtr TreeExecutorBase::get_node_base_interface()
290{
291 return node_ptr_->get_node_base_interface();
292}
293
295{
296 return tree_node_waitables_callback_group_ptr_;
297}
298
299rclcpp::executors::SingleThreadedExecutor::SharedPtr TreeExecutorBase::getTreeNodeWaitablesExecutorPtr()
300{
301 return tree_node_waitables_executor_ptr_;
302}
303
304std::string toStr(TreeExecutorBase::ExecutionState state)
305{
306 switch (state) {
308 return "IDLE";
310 return "STARTING";
312 return "RUNNING";
314 return "PAUSED";
316 return "HALTED";
317 }
318 return "undefined";
319}
320
321std::string toStr(TreeExecutorBase::ControlCommand cmd)
322{
323 switch (cmd) {
325 return "RUN";
327 return "PAUSE";
329 return "HALT";
331 return "TERMINATE";
332 }
333 return "undefined";
334}
335
336std::string toStr(TreeExecutorBase::TreeExitBehavior behavior)
337{
338 switch (behavior) {
340 return "TERMINATE";
342 return "RESTART";
343 }
344 return "undefined";
345}
346
347std::string toStr(TreeExecutorBase::ExecutionResult result)
348{
349 switch (result) {
351 return "TREE_SUCCEEDED";
353 return "TREE_FAILED";
355 return "TERMINATED_PREMATURELY";
357 return "ERROR";
358 }
359 return "undefined";
360}
361
362} // namespace auto_apms_behavior_tree
ExecutionState
Enum representing possible behavior tree execution states.
@ RUNNING
Executor is busy and tree has been ticked at least once.
@ PAUSED
Execution routine is active, but tree is not being ticked.
@ HALTED
Execution routine is active, but tree is not being ticked and has been halted before.
virtual void onTermination(const ExecutionResult &result)
Callback invoked when the execution routine terminates.
std::shared_future< ExecutionResult > startExecution(TreeConstructor make_tree, double tick_rate_sec=0.1, int groot2_port=-1)
Start a behavior tree that is built using a callback.
ExecutionState getExecutionState()
Get a status code indicating the current state of execution.
bool isBusy()
Determine whether this executor is currently executing a behavior tree.
rclcpp::Node::SharedPtr node_ptr_
Shared pointer to the parent ROS 2 node.
virtual TreeExitBehavior onTreeExit(bool success)
Callback invoked last thing when the execution routine completes because the behavior tree is finishe...
virtual bool onInitialTick()
Callback invoked once before the behavior tree is ticked for the very first time.
rclcpp::executors::SingleThreadedExecutor::SharedPtr getTreeNodeWaitablesExecutorPtr()
Get the ROS 2 executor instance used for spinning waitables registered by behavior tree nodes.
rclcpp::CallbackGroup::SharedPtr getTreeNodeWaitablesCallbackGroupPtr()
Get the callback group used for all waitables registered by behavior tree nodes.
void setControlCommand(ControlCommand cmd)
Set the command that handles the control flow of the execution routine.
void clearGlobalBlackboard()
Reset the global blackboard and clear all entries.
virtual bool onTick()
Callback invoked every time before the behavior tree is ticked.
virtual bool afterTick()
Callback invoked every time after the behavior tree is ticked.
TreeExitBehavior
Enum representing possible options for what to do when a behavior tree is completed.
ControlCommand
Enum representing possible commands for controlling the behavior tree execution routine.
@ TERMINATE
Halt the currently executing tree and terminate the execution routine.
@ HALT
Halt the currently executing tree and pause the execution routine.
TreeStateObserver & getStateObserver()
Get a reference to the current behavior tree state observer.
TreeExecutorBase(rclcpp::Node::SharedPtr node_ptr, rclcpp::CallbackGroup::SharedPtr tree_node_callback_group_ptr=nullptr)
Constructor.
rclcpp::Node::SharedPtr getNodePtr()
Get a shared pointer to the parent ROS 2 node.
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr get_node_base_interface()
Get the node's base interface. Is required to be able to register derived classes as ROS2 components.
TreeBlackboardSharedPtr getGlobalBlackboardPtr()
Get a shared pointer to the global blackboard instance.
std::string getTreeName()
Get the name of the tree that is currently executing.
ExecutionResult
Enum representing possible behavior tree execution results.
@ TREE_SUCCEEDED
Tree completed with BT::NodeStatus::SUCCESS.
@ TERMINATED_PREMATURELY
Execution terminated before the tree was able to propagate the tick to all its nodes.
@ TREE_FAILED
Tree completed with BT::NodeStatus::FAILURE.
const rclcpp::Logger logger_
Logger associated with the parent ROS 2 node.
State observer for a particular behavior tree object that writes introspection and debugging informat...
void exposeToGlobalDebugLogging(const rclcpp::Logger &logger)
Enable ROS 2 debug logging, if the C preprocessor flag _AUTO_APMS_DEBUG_LOGGING is set.
Definition logging.cpp:25
const char * toStr(const ActionNodeErrorCode &err)
Convert the action error code to string.
Useful tooling for incorporating behavior trees for task development.
Definition builder.hpp:30