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// 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_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 std::unique_ptr<Tree> tree_ptr;
57 try {
58 // Lives inside BT::Tree once it is created
59 TreeBlackboardSharedPtr main_tree_bb_ptr = TreeBlackboard::create(global_blackboard_ptr_);
60 // Create the tree from the provided callback
61 tree_ptr = std::make_unique<Tree>(make_tree(main_tree_bb_ptr));
62 } catch (const std::exception & e) {
63 throw exceptions::TreeBuildError(
64 "Cannot start execution because creating the tree failed: " + std::string(e.what()));
65 }
66
67 // Hand the freshly created tree over to the overload that starts the execution routine.
68 return startExecution(std::move(tree_ptr), tick_rate_sec, groot2_port);
69}
70
71std::shared_future<TreeExecutorBase::ExecutionResult> TreeExecutorBase::startExecution(
72 std::unique_ptr<Tree> tree, double tick_rate_sec, int groot2_port)
73{
74 if (isBusy()) {
75 throw exceptions::TreeExecutorError(
76 "Cannot start execution with tree '" + getTreeName() + "' currently executing.");
77 }
78 if (!tree) {
79 throw exceptions::TreeExecutorError("Cannot start execution because the provided tree is nullptr.");
80 }
81
82 // Take ownership of the already created tree
83 tree_ptr_ = std::move(tree);
84
85 // Groot2 publisher
86 groot2_publisher_ptr_.reset();
87 if (groot2_port != -1) {
88 try {
89 groot2_publisher_ptr_ = std::make_unique<BT::Groot2Publisher>(*tree_ptr_, groot2_port);
90 } catch (const std::exception & e) {
91 throw exceptions::TreeExecutorError(
92 "Failed to initialize Groot2 publisher with port " + std::to_string(groot2_port) + ": " + e.what());
93 }
94 }
95
96 // Tree state observer
97 state_observer_ptr_.reset();
98 state_observer_ptr_ = std::make_unique<TreeStateObserver>(*tree_ptr_, logger_);
99 state_observer_ptr_->enableTransitionToIdle(false);
100
101 /* Start execution timer */
102
103 // Reset state variables
104 prev_execution_state_ = getExecutionState();
105 control_command_ = ControlCommand::RUN;
106 termination_reason_ = "";
107 execution_stopped_ = true;
108
109 // Create promise for asynchronous execution and configure termination callback
110 auto promise_ptr = std::make_shared<std::promise<ExecutionResult>>();
111 TerminationCallback termination_callback = [this, promise_ptr](ExecutionResult result, const std::string & msg) {
112 RCLCPP_INFO(
113 logger_, "Terminating tree '%s' from state %s.", getTreeName().c_str(), toStr(getExecutionState()).c_str());
114 if (result == ExecutionResult::ERROR) {
115 RCLCPP_ERROR(logger_, "Termination reason: %s", msg.c_str());
116 } else {
117 RCLCPP_INFO(logger_, "Termination reason: %s", msg.c_str());
118 }
119 onTermination(result); // is evaluated before the timer is cancelled, which means the execution state has not
120 // changed yet during the callback and can be evaluated to inspect the terminal state.
121 promise_ptr->set_value(result);
122 execution_timer_ptr_->cancel();
123 tree_ptr_.reset(); // Release the memory allocated by the tree
124 };
125
126 // NOTE: The main callback timer is using the node's default callback group
127 const std::chrono::nanoseconds period =
128 std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::duration<double>(tick_rate_sec));
129 execution_timer_ptr_ = node_ptr_->create_wall_timer(period, [this, period, termination_callback]() {
130 // Collect and process incoming messages before ticking
131 tree_node_waitables_executor_ptr_->spin_all(period);
132
133 // Tick the tree, evaluate control commands and handle the returned tree status
134 tick_callback_(termination_callback);
135 });
136 return promise_ptr->get_future();
137}
138
139void TreeExecutorBase::tick_callback_(TerminationCallback termination_callback)
140{
141 const ExecutionState this_execution_state = getExecutionState();
142 if (prev_execution_state_ != this_execution_state) {
143 RCLCPP_DEBUG(
144 logger_, "Executor for tree '%s' changed state from '%s' to '%s'.", getTreeName().c_str(),
145 toStr(prev_execution_state_).c_str(), toStr(this_execution_state).c_str());
146 prev_execution_state_ = this_execution_state;
147 }
148
149 /* Evaluate control command */
150
151 execution_stopped_ = false;
152 bool do_on_tick = true;
153 switch (control_command_) {
155 execution_stopped_ = true;
156 return;
158 if (this_execution_state == ExecutionState::STARTING) {
159 // Evaluate initial tick callback before ticking for the first time since the timer has been created
160 if (!onInitialTick()) {
161 do_on_tick = false;
162 termination_reason_ = "onInitialTick() returned false.";
163 }
164 }
165 // Evaluate tick callback everytime before actually ticking.
166 // This also happens the first time except if onInitialTick() returned false
167 if (do_on_tick) {
168 if (onTick()) {
169 break;
170 } else {
171 termination_reason_ = "onTick() returned false.";
172 }
173 }
174
175 // Fall through to terminate if any of the callbacks returned false
176 [[fallthrough]];
178 if (this_execution_state == ExecutionState::HALTED) {
179 termination_callback(
181 termination_reason_.empty() ? "Control command was set to TERMINATE." : termination_reason_);
182 return;
183 }
184
185 // Fall through to halt tree before termination
186 [[fallthrough]];
188 // Check if already halted
189 if (this_execution_state != ExecutionState::HALTED) {
190#ifdef AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_THROW_ON_TICK_ERROR
191 tree_ptr_->haltTree();
192#else
193 try {
194 tree_ptr_->haltTree();
195 } catch (const std::exception & e) {
196 termination_callback(
198 "Error during haltTree() on command " + toStr(control_command_) + ": " + std::string(e.what()));
199 }
200#endif
201 }
202 return;
203 default:
204 throw std::logic_error(
205 "Handling control command " + std::to_string(static_cast<int>(control_command_)) + " '" +
206 toStr(control_command_) + "' is not implemented.");
207 }
208
209 /* Tick the tree instance */
210
211 BT::NodeStatus bt_status = BT::NodeStatus::IDLE;
212#ifdef AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_THROW_ON_TICK_ERROR
213 bt_status = tree_ptr_->tickExactlyOnce();
214#else
215 try {
216 // It is important to tick EXACTLY once to prevent loops induced by BT nodes from blocking
217 bt_status = tree_ptr_->tickExactlyOnce();
218 } catch (const std::exception & e) {
219 std::string msg = "Ran into an exception during tick: " + std::string(e.what());
220 try {
221 tree_ptr_->haltTree(); // Try to halt tree before aborting
222 } catch (const std::exception & e) {
223 msg += "\nDuring haltTree(), another exception occurred: " + std::string(e.what());
224 }
225 termination_callback(ExecutionResult::ERROR, msg);
226 return;
227 }
228#endif
229
230 if (!afterTick()) {
231 termination_callback(ExecutionResult::TERMINATED_PREMATURELY, "afterTick() returned false.");
232 return;
233 }
234
235 if (bt_status == BT::NodeStatus::RUNNING) return;
236
237 /* Determine how to handle the behavior tree execution result */
238
239 if (!(bt_status == BT::NodeStatus::SUCCESS || bt_status == BT::NodeStatus::FAILURE)) {
240 throw std::logic_error(
241 "bt_status is " + BT::toStr(bt_status) + ". Must be one of SUCCESS or FAILURE at this point.");
242 }
243 const bool success = bt_status == BT::NodeStatus::SUCCESS;
244 switch (onTreeExit(success)) {
246 termination_callback(
248 "Terminated on tree result " + BT::toStr(bt_status) + ".");
249 return;
251 control_command_ = ControlCommand::RUN;
252 return;
253 }
254
255 throw std::logic_error("Execution routine is not intended to proceed to this statement.");
256}
257
258bool TreeExecutorBase::onInitialTick() { return true; }
259
260bool TreeExecutorBase::onTick() { return true; }
261
262bool TreeExecutorBase::afterTick() { return true; }
263
268
270
272{
273 ExecutionState curr_state = getExecutionState();
274 if (curr_state == ExecutionState::IDLE) {
275 global_blackboard_ptr_ = TreeBlackboard::create();
276 return true;
277 }
278 RCLCPP_WARN(
279 logger_, "clearGlobalBlackboard() was called when executor is %s, but this is only allowed when IDLE. Ignoring...",
280 toStr(curr_state).c_str());
281 return false;
282}
283
284void TreeExecutorBase::setControlCommand(ControlCommand cmd) { control_command_ = cmd; }
285
286bool TreeExecutorBase::isBusy() { return execution_timer_ptr_ && !execution_timer_ptr_->is_canceled(); }
287
289{
290 if (isBusy()) {
291 if (!tree_ptr_) throw std::logic_error("tree_ptr_ cannot be nullptr when execution is started.");
292 if (tree_ptr_->rootNode()->status() == BT::NodeStatus::IDLE) {
293 // The root node being IDLE here means one of the following:
294 // - the tree hasn't been ticked yet since its creation
295 // - the tree was halted or just finished executing
296 return execution_stopped_ ? ExecutionState::STARTING : ExecutionState::HALTED;
297 }
298 return execution_stopped_ ? ExecutionState::PAUSED : ExecutionState::RUNNING;
299 }
301}
302
304{
305 if (tree_ptr_) return tree_ptr_->subtrees[0]->tree_ID;
306 return "NO_TREE_NAME";
307}
308
309TreeBlackboardSharedPtr TreeExecutorBase::getGlobalBlackboardPtr() { return global_blackboard_ptr_; }
310
312{
313 if (!state_observer_ptr_) {
314 throw exceptions::TreeExecutorError("Cannot get state observer because executor is not busy.");
315 }
316 return *state_observer_ptr_;
317}
318
319rclcpp::Node::SharedPtr TreeExecutorBase::getNodePtr() { return node_ptr_; }
320
321rclcpp::node_interfaces::NodeBaseInterface::SharedPtr TreeExecutorBase::get_node_base_interface()
322{
323 return node_ptr_->get_node_base_interface();
324}
325
327{
328 return tree_node_waitables_callback_group_ptr_;
329}
330
331rclcpp::executors::SingleThreadedExecutor::SharedPtr TreeExecutorBase::getTreeNodeWaitablesExecutorPtr()
332{
333 return tree_node_waitables_executor_ptr_;
334}
335
336std::string toStr(TreeExecutorBase::ExecutionState state)
337{
338 switch (state) {
340 return "IDLE";
342 return "STARTING";
344 return "RUNNING";
346 return "PAUSED";
348 return "HALTED";
349 }
350 return "undefined";
351}
352
353std::string toStr(TreeExecutorBase::ControlCommand cmd)
354{
355 switch (cmd) {
357 return "RUN";
359 return "PAUSE";
361 return "HALT";
363 return "TERMINATE";
364 }
365 return "undefined";
366}
367
368std::string toStr(TreeExecutorBase::TreeExitBehavior behavior)
369{
370 switch (behavior) {
372 return "TERMINATE";
374 return "RESTART";
375 }
376 return "undefined";
377}
378
379std::string toStr(TreeExecutorBase::ExecutionResult result)
380{
381 switch (result) {
383 return "TREE_SUCCEEDED";
385 return "TREE_FAILED";
387 return "TERMINATED_PREMATURELY";
389 return "ERROR";
390 }
391 return "undefined";
392}
393
394} // 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