AutoAPMS
Streamlining behaviors in ROS 2
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{
35 // The behavior tree node callback group is intended to be passed to all nodes and used when adding subscriptions,
36 // publishers, services, actions etc. It is associated with a standalone single threaded executor, which is spun in
37 // between ticks, to make sure that pending work is executed while the main tick routine is sleeping.
38 if (!tree_node_waitables_callback_group_ptr_) {
39 tree_node_waitables_callback_group_ptr_ =
40 node_ptr_->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive, false);
41 }
42
43 // Add the behavior tree node callback group to the internal executor
44 tree_node_waitables_executor_ptr_->add_callback_group(
45 tree_node_waitables_callback_group_ptr_, get_node_base_interface());
46}
47
48std::shared_future<TreeExecutorBase::ExecutionResult> TreeExecutorBase::startExecution(
49 TreeConstructor make_tree, double tick_rate_sec, int groot2_port)
50{
51 if (isBusy()) {
52 throw exceptions::TreeExecutorError(
53 "Cannot start execution with tree '" + getTreeName() + "' currently executing.");
54 }
55
56 TreeBlackboardSharedPtr main_tree_bb_ptr = TreeBlackboard::create(global_blackboard_ptr_);
57 try {
58 tree_ptr_.reset(new Tree(make_tree(main_tree_bb_ptr)));
59 } catch (const std::exception & e) {
60 throw exceptions::TreeBuildError(
61 "Cannot start execution because creating the tree failed: " + std::string(e.what()));
62 }
63
64 // Groot2 publisher
65 groot2_publisher_ptr_.reset();
66 if (groot2_port != -1) {
67 try {
68 groot2_publisher_ptr_ = std::make_unique<BT::Groot2Publisher>(*tree_ptr_, groot2_port);
69 } catch (const std::exception & e) {
70 throw exceptions::TreeExecutorError(
71 "Failed to initialize Groot2 publisher with port " + std::to_string(groot2_port) + ": " + e.what());
72 }
73 }
74
75 // Tree state observer
76 state_observer_ptr_.reset();
77 state_observer_ptr_ = std::make_unique<TreeStateObserver>(*tree_ptr_, logger_);
78 state_observer_ptr_->enableTransitionToIdle(false);
79
80 /* Start execution timer */
81
82 // Reset state variables
83 prev_execution_state_ = getExecutionState();
84 control_command_ = ControlCommand::RUN;
85 termination_reason_ = "";
86 execution_stopped_ = true;
87
88 // Create promise for asynchronous execution and configure termination callback
89 auto promise_ptr = std::make_shared<std::promise<ExecutionResult>>();
90 TerminationCallback termination_callback = [this, promise_ptr](ExecutionResult result, const std::string & msg) {
91 RCLCPP_INFO(
92 logger_, "Terminating tree '%s' from state %s.", getTreeName().c_str(), toStr(getExecutionState()).c_str());
93 if (result == ExecutionResult::ERROR) {
94 RCLCPP_ERROR(logger_, "Termination reason: %s", msg.c_str());
95 } else {
96 RCLCPP_INFO(logger_, "Termination reason: %s", msg.c_str());
97 }
98 onTermination(result); // is evaluated before the timer is cancelled, which means the execution state has not
99 // changed yet during the callback and can be evaluated to inspect the terminal state.
100 promise_ptr->set_value(result);
101 execution_timer_ptr_->cancel();
102 tree_ptr_.reset(); // Release the memory allocated by the tree
103 };
104
105 // NOTE: The main callback timer is using the node's default callback group
106 const std::chrono::nanoseconds period =
107 std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::duration<double>(tick_rate_sec));
108 execution_timer_ptr_ = node_ptr_->create_wall_timer(period, [this, period, termination_callback]() {
109 // Collect and process incoming messages before ticking
110 tree_node_waitables_executor_ptr_->spin_all(period);
111
112 // Tick the tree, evaluate control commands and handle the returned tree status
113 tick_callback_(termination_callback);
114 });
115 return promise_ptr->get_future();
117
118void TreeExecutorBase::tick_callback_(TerminationCallback termination_callback)
119{
120 const ExecutionState this_execution_state = getExecutionState();
121 if (prev_execution_state_ != this_execution_state) {
122 RCLCPP_DEBUG(
123 logger_, "Executor for tree '%s' changed state from '%s' to '%s'.", getTreeName().c_str(),
124 toStr(prev_execution_state_).c_str(), toStr(this_execution_state).c_str());
125 prev_execution_state_ = this_execution_state;
126 }
127
128 /* Evaluate control command */
129
130 execution_stopped_ = false;
131 bool do_on_tick = true;
132 switch (control_command_) {
134 execution_stopped_ = true;
135 return;
137 if (this_execution_state == ExecutionState::STARTING) {
138 // Evaluate initial tick callback before ticking for the first time since the timer has been created
139 if (!onInitialTick()) {
140 do_on_tick = false;
141 termination_reason_ = "onInitialTick() returned false.";
142 }
143 }
144 // Evaluate tick callback everytime before actually ticking.
145 // This also happens the first time except if onInitialTick() returned false
146 if (do_on_tick) {
147 if (onTick()) {
148 break;
149 } else {
150 termination_reason_ = "onTick() returned false.";
151 }
152 }
153
154 // Fall through to terminate if any of the callbacks returned false
155 [[fallthrough]];
157 if (this_execution_state == ExecutionState::HALTED) {
158 termination_callback(
160 termination_reason_.empty() ? "Control command was set to TERMINATE." : termination_reason_);
161 return;
162 }
163
164 // Fall through to halt tree before termination
165 [[fallthrough]];
167 // Check if already halted
168 if (this_execution_state != ExecutionState::HALTED) {
169#ifdef AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_THROW_ON_TICK_ERROR
170 tree_ptr_->haltTree();
171#else
172 try {
173 tree_ptr_->haltTree();
174 } catch (const std::exception & e) {
175 termination_callback(
177 "Error during haltTree() on command " + toStr(control_command_) + ": " + std::string(e.what()));
178 }
179#endif
180 }
181 return;
182 default:
183 throw std::logic_error(
184 "Handling control command " + std::to_string(static_cast<int>(control_command_)) + " '" +
185 toStr(control_command_) + "' is not implemented.");
186 }
187
188 /* Tick the tree instance */
189
190 BT::NodeStatus bt_status = BT::NodeStatus::IDLE;
191#ifdef AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_THROW_ON_TICK_ERROR
192 bt_status = tree_ptr_->tickExactlyOnce();
193#else
194 try {
195 // It is important to tick EXACTLY once to prevent loops induced by BT nodes from blocking
196 bt_status = tree_ptr_->tickExactlyOnce();
197 } catch (const std::exception & e) {
198 std::string msg = "Ran into an exception during tick: " + std::string(e.what());
199 try {
200 tree_ptr_->haltTree(); // Try to halt tree before aborting
201 } catch (const std::exception & e) {
202 msg += "\nDuring haltTree(), another exception occurred: " + std::string(e.what());
203 }
204 termination_callback(ExecutionResult::ERROR, msg);
205 return;
206 }
207#endif
208
209 if (!afterTick()) {
210 termination_callback(ExecutionResult::TERMINATED_PREMATURELY, "afterTick() returned false.");
211 return;
212 }
213
214 if (bt_status == BT::NodeStatus::RUNNING) return;
215
216 /* Determine how to handle the behavior tree execution result */
217
218 if (!(bt_status == BT::NodeStatus::SUCCESS || bt_status == BT::NodeStatus::FAILURE)) {
219 throw std::logic_error(
220 "bt_status is " + BT::toStr(bt_status) + ". Must be one of SUCCESS or FAILURE at this point.");
221 }
222 const bool success = bt_status == BT::NodeStatus::SUCCESS;
223 switch (onTreeExit(success)) {
225 termination_callback(
227 "Terminated on tree result " + BT::toStr(bt_status) + ".");
228 return;
230 control_command_ = ControlCommand::RUN;
231 return;
232 }
233
234 throw std::logic_error("Execution routine is not intended to proceed to this statement.");
235}
236
237bool TreeExecutorBase::onInitialTick() { return true; }
238
239bool TreeExecutorBase::onTick() { return true; }
240
241bool TreeExecutorBase::afterTick() { return true; }
242
247
249
251{
252 ExecutionState curr_state = getExecutionState();
253 if (curr_state == ExecutionState::IDLE) {
254 global_blackboard_ptr_ = TreeBlackboard::create();
255 return true;
256 }
257 RCLCPP_WARN(
258 logger_, "clearGlobalBlackboard() was called when executor is %s, but this is only allowed when IDLE. Ignoring...",
259 toStr(curr_state).c_str());
260 return false;
261}
262
263void TreeExecutorBase::setControlCommand(ControlCommand cmd) { control_command_ = cmd; }
264
265bool TreeExecutorBase::isBusy() { return execution_timer_ptr_ && !execution_timer_ptr_->is_canceled(); }
266
268{
269 if (isBusy()) {
270 if (!tree_ptr_) throw std::logic_error("tree_ptr_ cannot be nullptr when execution is started.");
271 if (tree_ptr_->rootNode()->status() == BT::NodeStatus::IDLE) {
272 // The root node being IDLE here means one of the following:
273 // - the tree hasn't been ticked yet since its creation
274 // - the tree was halted or just finished executing
275 return execution_stopped_ ? ExecutionState::STARTING : ExecutionState::HALTED;
276 }
277 return execution_stopped_ ? ExecutionState::PAUSED : ExecutionState::RUNNING;
278 }
280}
281
283{
284 if (tree_ptr_) return tree_ptr_->subtrees[0]->tree_ID;
285 return "NO_TREE_NAME";
286}
287
288TreeBlackboardSharedPtr TreeExecutorBase::getGlobalBlackboardPtr() { return global_blackboard_ptr_; }
289
291{
292 if (!state_observer_ptr_) {
293 throw exceptions::TreeExecutorError("Cannot get state observer because executor is not busy.");
294 }
295 return *state_observer_ptr_;
296}
297
298rclcpp::Node::SharedPtr TreeExecutorBase::getNodePtr() { return node_ptr_; }
299
300rclcpp::node_interfaces::NodeBaseInterface::SharedPtr TreeExecutorBase::get_node_base_interface()
301{
302 return node_ptr_->get_node_base_interface();
303}
304
306{
307 return tree_node_waitables_callback_group_ptr_;
308}
309
310rclcpp::executors::SingleThreadedExecutor::SharedPtr TreeExecutorBase::getTreeNodeWaitablesExecutorPtr()
311{
312 return tree_node_waitables_executor_ptr_;
313}
314
315std::string toStr(TreeExecutorBase::ExecutionState state)
316{
317 switch (state) {
319 return "IDLE";
321 return "STARTING";
323 return "RUNNING";
325 return "PAUSED";
327 return "HALTED";
328 }
329 return "undefined";
330}
331
332std::string toStr(TreeExecutorBase::ControlCommand cmd)
333{
334 switch (cmd) {
336 return "RUN";
338 return "PAUSE";
340 return "HALT";
342 return "TERMINATE";
343 }
344 return "undefined";
345}
346
347std::string toStr(TreeExecutorBase::TreeExitBehavior behavior)
348{
349 switch (behavior) {
351 return "TERMINATE";
353 return "RESTART";
354 }
355 return "undefined";
356}
357
358std::string toStr(TreeExecutorBase::ExecutionResult result)
359{
360 switch (result) {
362 return "TREE_SUCCEEDED";
364 return "TREE_FAILED";
366 return "TERMINATED_PREMATURELY";
368 return "ERROR";
369 }
370 return "undefined";
371}
372
373} // 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.
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.
virtual bool clearGlobalBlackboard()
Reset the global blackboard and clear all entries.
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...
const char * toStr(const ActionNodeErrorCode &err)
Convert the action error code to string.
Powerful tooling for incorporating behavior trees for task development.
Definition behavior.hpp:32