AutoAPMS
Resilient Robot Mission Management
All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator Modules Pages
executor_node.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_node.hpp"
16
17#include <algorithm>
18#include <functional>
19#include <regex>
20
21#include "auto_apms_behavior_tree/exceptions.hpp"
22#include "auto_apms_behavior_tree/util/parameter.hpp"
23#include "auto_apms_behavior_tree_core/definitions.hpp"
24#include "auto_apms_util/container.hpp"
25#include "auto_apms_util/string.hpp"
26#include "pluginlib/exceptions.hpp"
27#include "rclcpp/utilities.hpp"
28
30{
31
32const std::vector<std::string> EXPLICITLY_ALLOWED_PARAMETERS{
33 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_ALLOW_OTHER_BUILD_HANDLERS,
34 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_ALLOW_DYNAMIC_BLACKBOARD,
35 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_ALLOW_DYNAMIC_SCRIPTING_ENUMS,
36 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_EXCLUDE_PACKAGES_NODE,
37 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_EXCLUDE_PACKAGES_BUILD_HANDLER,
38 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_BUILD_HANDLER,
39 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_TICK_RATE,
40 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_GROOT2_PORT,
41 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_STATE_CHANGE_LOGGER};
42
43const std::vector<std::string> EXPLICITLY_ALLOWED_PARAMETERS_WHILE_BUSY{
44 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_ALLOW_DYNAMIC_BLACKBOARD,
45 _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_STATE_CHANGE_LOGGER};
46
47TreeExecutorNodeOptions::TreeExecutorNodeOptions(const rclcpp::NodeOptions & ros_node_options)
48: ros_node_options_(ros_node_options)
49{
50}
51
53{
54 scripting_enum_parameters_from_overrides_ = from_overrides;
55 scripting_enum_parameters_dynamic_ = dynamic;
56 return *this;
57}
58
60{
61 blackboard_parameters_from_overrides_ = from_overrides;
62 blackboard_parameters_dynamic_ = dynamic;
63 return *this;
64}
65
67{
68 custom_default_parameters_[_AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_BUILD_HANDLER] = rclcpp::ParameterValue(name);
69 return *this;
70}
71
73{
74 rclcpp::NodeOptions opt(ros_node_options_);
75 opt.automatically_declare_parameters_from_overrides(
76 scripting_enum_parameters_from_overrides_ || blackboard_parameters_from_overrides_);
77 opt.allow_undeclared_parameters(scripting_enum_parameters_dynamic_ || blackboard_parameters_dynamic_);
78 return opt;
79}
80
81TreeExecutorNode::TreeExecutorNode(const std::string & name, TreeExecutorNodeOptions executor_options)
82: TreeExecutorBase(std::make_shared<rclcpp::Node>(name, executor_options.getROSNodeOptions())),
83 executor_options_(executor_options),
84 executor_param_listener_(node_ptr_),
85 start_action_context_(logger_)
86{
87 // Set custom parameter default values.
88 // NOTE: We cannot do this before the node is created, because we also need the global parameter overrides here, not
89 // just rclcpp::NodeOptions::parameter_overrides (This only contains parameter overrides explicitly provided to the
90 // options object. So generally speaking, this variable doesn't represent all parameter overrides).
91 std::vector<rclcpp::Parameter> new_default_parameters;
92 std::map<std::string, rclcpp::ParameterValue> effective_param_overrides =
93 node_ptr_->get_node_parameters_interface()->get_parameter_overrides();
94 for (const auto & [name, value] : executor_options_.custom_default_parameters_) {
95 // Only set custom default parameters if not present in overrides
96 if (effective_param_overrides.find(name) == effective_param_overrides.end()) {
97 new_default_parameters.push_back(rclcpp::Parameter(name, value));
98 }
99 }
100 if (!new_default_parameters.empty()) node_ptr_->set_parameters_atomically(new_default_parameters);
101
102 const ExecutorParameters initial_params = executor_param_listener_.get_params();
103
104 // Remove all parameters from overrides that are not supported
105 rcl_interfaces::msg::ListParametersResult res = node_ptr_->list_parameters({}, 0);
106 std::vector<std::string> unkown_param_names;
107 for (const std::string & param_name : res.names) {
108 if (!stripPrefixFromParameterName(SCRIPTING_ENUM_PARAM_PREFIX, param_name).empty()) continue;
109 if (!stripPrefixFromParameterName(BLACKBOARD_PARAM_PREFIX, param_name).empty()) continue;
110 if (auto_apms_util::contains(EXPLICITLY_ALLOWED_PARAMETERS, param_name)) continue;
111 try {
112 node_ptr_->undeclare_parameter(param_name);
113 } catch (const rclcpp::exceptions::ParameterImmutableException & e) {
114 // Allow all builtin read only parameters
115 continue;
116 } catch (const rclcpp::exceptions::InvalidParameterTypeException & e) {
117 // Allow all builtin statically typed parameters
118 continue;
119 }
120 unkown_param_names.push_back(param_name);
121 }
122 if (!unkown_param_names.empty()) {
123 RCLCPP_WARN(
124 logger_, "The following initial parameters are not supported and have been removed: [ %s ].",
125 auto_apms_util::join(unkown_param_names, ", ").c_str());
126 }
127
128 // Create behavior tree node loader
129 tree_node_loader_ptr_ = core::NodeRegistrationLoader::make_shared(
130 std::set<std::string>(initial_params.node_exclude_packages.begin(), initial_params.node_exclude_packages.end()));
131
132 // Create behavior tree build handler loader
133 build_handler_loader_ptr_ = TreeBuildHandlerLoader::make_unique(
134 std::set<std::string>(
135 initial_params.build_handler_exclude_packages.begin(), initial_params.build_handler_exclude_packages.end()));
136
137 // Instantiate behavior tree build handler
138 if (
139 initial_params.build_handler != PARAM_VALUE_NO_BUILD_HANDLER &&
140 !build_handler_loader_ptr_->isClassAvailable(initial_params.build_handler)) {
141 throw exceptions::TreeExecutorError(
142 "Cannot load build handler '" + initial_params.build_handler +
143 "' because no corresponding ament_index resource was found. Make sure that you spelled the build handler's "
144 "name correctly "
145 "and registered it by calling auto_apms_behavior_tree_declare_build_handlers() in the CMakeLists.txt of the "
146 "corresponding package.");
147 }
148 loadBuildHandler(initial_params.build_handler);
149
150 // Collect scripting enum and blackboard parameters from initial parameters
151 const auto initial_scripting_enums = getParameterValuesWithPrefix(SCRIPTING_ENUM_PARAM_PREFIX);
152 if (!initial_scripting_enums.empty()) {
153 if (executor_options_.scripting_enum_parameters_from_overrides_) {
154 updateScriptingEnumsWithParameterValues(initial_scripting_enums);
155 } else {
156 RCLCPP_WARN(
157 logger_,
158 "Initial scripting enums have been provided, but the 'Scripting enums from overrides' option is disabled. "
159 "Ignoring.");
160 }
161 }
162 const auto initial_blackboard = getParameterValuesWithPrefix(BLACKBOARD_PARAM_PREFIX);
163 if (!initial_blackboard.empty()) {
164 if (executor_options_.blackboard_parameters_from_overrides_) {
165 updateGlobalBlackboardWithParameterValues(initial_blackboard);
166 } else {
167 RCLCPP_WARN(
168 logger_,
169 "Initial blackboard entries have been provided, but the 'Blackboard from overrides' option is disabled. "
170 "Ignoring.");
171 }
172 }
173
174 using namespace std::placeholders;
175 start_action_ptr_ = rclcpp_action::create_server<StartActionContext::Type>(
176 node_ptr_, std::string(node_ptr_->get_name()) + _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_START_ACTION_NAME_SUFFIX,
177 std::bind(&TreeExecutorNode::handle_start_goal_, this, _1, _2),
178 std::bind(&TreeExecutorNode::handle_start_cancel_, this, _1),
179 std::bind(&TreeExecutorNode::handle_start_accept_, this, _1));
180
181 command_action_ptr_ = rclcpp_action::create_server<CommandActionContext::Type>(
182 node_ptr_, std::string(node_ptr_->get_name()) + _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_COMMAND_ACTION_NAME_SUFFIX,
183 std::bind(&TreeExecutorNode::handle_command_goal_, this, _1, _2),
184 std::bind(&TreeExecutorNode::handle_command_cancel_, this, _1),
185 std::bind(&TreeExecutorNode::handle_command_accept_, this, _1));
186
187 // Adding the local on_set_parameters_callback after the parameter listeners from generate_parameters_library
188 // are created makes sure that this callback will be evaluated before the listener callbacks.
189 // This is desired to keep the internal parameter struct in sync, because the callbacks of the listeners implicitly
190 // set them if the change is accepted. Otherwise, they would be set even if the local callback rejects the change.
191 // We DO NOT set any variables in this callback, but only check if the request to change certain parameters is valid.
192 // The actual change is performed in the callback registered with rclcpp::ParameterEventListener
193 on_set_parameters_callback_handle_ptr_ =
194 node_ptr_->add_on_set_parameters_callback([this](const std::vector<rclcpp::Parameter> & parameters) {
195 return this->on_set_parameters_callback_(parameters);
196 });
197
198 // Add an event handler that applies the actual parameter change
199 parameter_event_handler_ptr_ = std::make_shared<rclcpp::ParameterEventHandler>(node_ptr_);
200 parameter_event_callback_handle_ptr_ = parameter_event_handler_ptr_->add_parameter_event_callback(
201 [this](const rcl_interfaces::msg::ParameterEvent & event) { this->parameter_event_callback_(event); });
202
203 // Make sure ROS arguments are removed. When using rclcpp_components, this is typically not the case.
204 std::vector<std::string> args_with_ros_arguments = node_ptr_->get_node_options().arguments();
205 int argc = args_with_ros_arguments.size();
206 char ** argv = new char *[argc + 1]; // +1 for the null terminator
207 for (int i = 0; i < argc; ++i) {
208 argv[i] = const_cast<char *>(args_with_ros_arguments[i].c_str());
209 }
210 argv[argc] = nullptr; // Null-terminate the array as required for argv[]
211
212 // Evaluate possible cli argument dictating to start executing with a specific build request immediately.
213 // Note: First argument is always path of executable.
214 if (const std::vector<std::string> args = rclcpp::remove_ros_arguments(argc, argv); args.size() > 1) {
215 // Log relevant arguments. First argument is executable name (argv[0]) and won't be considered.
216 std::vector<std::string> relevant_args{args.begin() + 1, args.end()};
217 RCLCPP_DEBUG(
218 logger_, "Additional cli arguments in rclcpp::NodeOptions: [ %s ]",
219 auto_apms_util::join(relevant_args, ", ").c_str());
220
221 // Start tree execution with the build handler request being the first relevant argument
222 startExecution(makeTreeConstructor(relevant_args[0], ""), initial_params.tick_rate, initial_params.groot2_port);
223 }
224}
225
226TreeExecutorNode::TreeExecutorNode(rclcpp::NodeOptions options)
227: TreeExecutorNode(_AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_DEFAULT_NAME, TreeExecutorNodeOptions(options))
228{
229}
230
231void TreeExecutorNode::setUpBuilder(core::TreeBuilder & /*builder*/, const core::NodeManifest & /*node_manifest*/) {}
232
233std::shared_future<TreeExecutorNode::ExecutionResult> TreeExecutorNode::startExecution(
234 const std::string & tree_build_request)
235{
236 const ExecutorParameters params = executor_param_listener_.get_params();
237 return startExecution(makeTreeConstructor(tree_build_request, ""), params.tick_rate, params.groot2_port);
238}
239
240std::map<std::string, rclcpp::ParameterValue> TreeExecutorNode::getParameterValuesWithPrefix(const std::string & prefix)
241{
242 const auto res = node_ptr_->list_parameters({prefix}, 2);
243 std::map<std::string, rclcpp::ParameterValue> value_map;
244 for (const std::string & name_with_prefix : res.names) {
245 if (const std::string suffix = stripPrefixFromParameterName(prefix, name_with_prefix); !suffix.empty()) {
246 value_map[suffix] = node_ptr_->get_parameter(name_with_prefix).get_parameter_value();
247 }
248 }
249 return value_map;
250}
251
252std::string TreeExecutorNode::stripPrefixFromParameterName(const std::string & prefix, const std::string & param_name)
253{
254 const std::regex reg("^" + prefix + "\\.(\\S+)");
255 if (std::smatch match; std::regex_match(param_name, match, reg)) return match[1].str();
256 return "";
257}
258
260 const std::map<std::string, rclcpp::ParameterValue> & value_map, bool simulate)
261{
262 std::map<std::string, std::string> set_successfully_map;
263 for (const auto & [enum_key, pval] : value_map) {
264 try {
265 switch (pval.get_type()) {
266 case rclcpp::ParameterType::PARAMETER_BOOL:
267 if (simulate) continue;
268 scripting_enums_[enum_key] = static_cast<int>(pval.get<bool>());
269 break;
270 case rclcpp::ParameterType::PARAMETER_INTEGER:
271 if (simulate) continue;
272 scripting_enums_[enum_key] = static_cast<int>(pval.get<int64_t>());
273 break;
274 default:
275 if (simulate) return false;
276 throw exceptions::ParameterConversionError("Parameter to scripting enum conversion is not allowed.");
277 }
278 set_successfully_map[enum_key] = rclcpp::to_string(pval);
279 } catch (const std::exception & e) {
280 RCLCPP_ERROR(
281 logger_, "Error setting scripting enum from parameter %s=%s (Type: %s): %s", enum_key.c_str(),
282 rclcpp::to_string(pval).c_str(), rclcpp::to_string(pval.get_type()).c_str(), e.what());
283 return false;
284 }
285 }
286 if (!set_successfully_map.empty()) {
287 RCLCPP_DEBUG(
288 logger_, "Updated scripting enums from parameters: { %s }",
289 auto_apms_util::printMap(set_successfully_map).c_str());
290 }
291 return true;
292}
293
295 const std::map<std::string, rclcpp::ParameterValue> & value_map, bool simulate)
296{
297 TreeBlackboard & bb = *getGlobalBlackboardPtr();
298 std::map<std::string, std::string> set_successfully_map;
299 for (const auto & [entry_key, pval] : value_map) {
300 try {
301 if (const BT::Expected<BT::Any> expected = createAnyFromParameterValue(pval)) {
302 BT::Any any(expected.value());
303 if (simulate) {
304 // Verify that any has the same type as the entry (if it exists)
305 if (const BT::TypeInfo * entry_info = bb.entryInfo(entry_key)) {
306 if (entry_info->isStronglyTyped() && entry_info->type() != any.type()) return false;
307 }
308 continue;
309 } else {
310 bb.set(entry_key, any);
311 }
312 } else {
313 throw exceptions::ParameterConversionError(expected.error());
314 }
315 translated_global_blackboard_entries_[entry_key] = pval;
316 set_successfully_map[entry_key] = rclcpp::to_string(pval);
317 } catch (const std::exception & e) {
318 RCLCPP_ERROR(
319 logger_, "Error updating blackboard from parameter %s=%s (Type: %s): %s", entry_key.c_str(),
320 rclcpp::to_string(pval).c_str(), rclcpp::to_string(pval.get_type()).c_str(), e.what());
321 return false;
322 }
323 }
324 if (!set_successfully_map.empty()) {
325 RCLCPP_DEBUG(
326 logger_, "Updated blackboard from parameters: { %s }", auto_apms_util::printMap(set_successfully_map).c_str());
327 }
328 return true;
329}
330
331void TreeExecutorNode::loadBuildHandler(const std::string & name)
332{
333 if (build_handler_ptr_ && !executor_param_listener_.get_params().allow_other_build_handlers) {
334 throw std::logic_error(
335 "Executor option 'Allow other build handlers' is disabled, but loadBuildHandler() was called again after "
336 "instantiating '" +
337 current_build_handler_name_ + "'.");
338 }
339 if (current_build_handler_name_ == name) return;
340 if (name == PARAM_VALUE_NO_BUILD_HANDLER) {
341 build_handler_ptr_.reset();
342 } else {
343 try {
344 build_handler_ptr_ =
345 build_handler_loader_ptr_->createUniqueInstance(name)->makeUnique(node_ptr_, tree_node_loader_ptr_);
346 } catch (const pluginlib::CreateClassException & e) {
347 throw exceptions::TreeExecutorError(
348 "An error occurred when trying to create an instance of tree build handler class '" + name +
349 "'. This might be because you forgot to call the AUTO_APMS_BEHAVIOR_TREE_DECLARE_BUILD_HANDLER macro "
350 "in the source file: " +
351 e.what());
352 } catch (const std::exception & e) {
353 throw exceptions::TreeExecutorError(
354 "An error occurred when trying to create an instance of tree build handler class '" + name + "': " + e.what());
355 }
356 }
357 current_build_handler_name_ = name;
358}
359
361 const std::string & build_handler_request, const std::string & root_tree_name,
362 const core::NodeManifest & node_manifest, const core::NodeManifest & node_overrides)
363{
364 // Request the tree identity
365 if (
366 build_handler_ptr_ && !build_handler_ptr_->setBuildRequest(build_handler_request, node_manifest, root_tree_name)) {
367 throw exceptions::TreeBuildError(
368 "Build request '" + build_handler_request + "' was denied by '" +
369 executor_param_listener_.get_params().build_handler + "' (setBuildRequest() returned false).");
370 }
371
372 // By passing the the local variables to the callback's captures by value they live on and can be used for creating
373 // the tree later. Otherwise a segmentation fault might occur since memory allocated for the arguments might be
374 // released at the time the method returns.
375 return [this, root_tree_name, node_manifest, node_overrides](TreeBlackboardSharedPtr bb_ptr) {
376 // Currently, BehaviorTree.CPP requires the memory allocated by the factory to persist even after the tree has been
377 // created, so we make the builder a unique pointer that is only reset when a new tree is to be created.
378 // See https://github.com/BehaviorTree/BehaviorTree.CPP/issues/890
379 builder_ptr_.reset(new TreeBuilder(
381
382 // Allow executor to set up the builder independently from the build handler
383 setUpBuilder(*builder_ptr_, node_manifest);
384
385 // Make scripting enums available to tree instance
386 for (const auto & [enum_key, val] : scripting_enums_) builder_ptr_->setScriptingEnum(enum_key, val);
387
388 // If a build handler is specified, let it configure the builder and determine which tree is to be instantiated
389 std::string instantiate_name = root_tree_name;
390 if (build_handler_ptr_) {
391 instantiate_name = build_handler_ptr_->buildTree(*builder_ptr_, *bb_ptr).getName();
392 }
393
394 // Allow for overriding selected node instances
395 builder_ptr_->registerNodes(node_overrides, true);
396
397 // Finally, instantiate the tree
398 return builder_ptr_->instantiate(instantiate_name, bb_ptr);
399 };
400}
401
402rcl_interfaces::msg::SetParametersResult TreeExecutorNode::on_set_parameters_callback_(
403 const std::vector<rclcpp::Parameter> & parameters)
404{
405 const ExecutorParameters params = executor_param_listener_.get_params();
406
407 // Iterate through parameters and individually decide wether to reject the change
408 for (const rclcpp::Parameter & p : parameters) {
409 auto create_rejected = [&p](const std::string msg) {
410 rcl_interfaces::msg::SetParametersResult result;
411 result.successful = false;
412 result.reason = "Rejected to set " + p.get_name() + " = " + p.value_to_string() + " (Type: " + p.get_type_name() +
413 "): " + msg + ".";
414 return result;
415 };
416 const std::string param_name = p.get_name();
417
418 // Check if parameter is a scripting enum
419 if (const std::string enum_key = stripPrefixFromParameterName(SCRIPTING_ENUM_PARAM_PREFIX, param_name);
420 !enum_key.empty()) {
421 if (isBusy()) {
422 return create_rejected("Scripting enums cannot change while tree executor is running");
423 }
424 if (!executor_options_.scripting_enum_parameters_dynamic_ || !params.allow_dynamic_scripting_enums) {
425 return create_rejected(
426 "Cannot set scripting enum '" + enum_key + "', because the 'Dynamic scripting enums' option is disabled");
427 }
428 // Validate type of scripting enum parameters
429 if (!updateScriptingEnumsWithParameterValues({{enum_key, p.get_parameter_value()}}, true)) {
430 return create_rejected(
431 "Type of scripting enum must be bool or int. Tried to set enum '" + enum_key + "' with value '" +
432 p.value_to_string() + "' (Type: " + p.get_type_name() + ")");
433 };
434 // If scripting enum is allowed to change, continue with next parameter.
435 continue;
436 }
437
438 // Check if parameter is a blackboard parameter
439 if (const std::string entry_key = stripPrefixFromParameterName(BLACKBOARD_PARAM_PREFIX, param_name);
440 !entry_key.empty()) {
441 if (!executor_options_.blackboard_parameters_dynamic_ || !params.allow_dynamic_blackboard) {
442 return create_rejected(
443 "Cannot set blackboard entry '" + entry_key + "', because the 'Dynamic blackboard' option is disabled");
444 }
445 // Validate type of blackboard parameters won't change
446 if (!updateGlobalBlackboardWithParameterValues({{entry_key, p.get_parameter_value()}}, true)) {
447 return create_rejected(
448 "Type of blackboard entries must not change. Tried to set entry '" + entry_key +
449 "' (Type: " + getGlobalBlackboardPtr()->getEntry(entry_key)->info.typeName() + ") with value '" +
450 p.value_to_string() + "' (Type: " + p.get_type_name() + ")");
451 };
452 // If blackboard entry is allowed to change, continue with next parameter.
453 continue;
454 }
455
456 // Check if parameter is known
457 if (!auto_apms_util::contains(EXPLICITLY_ALLOWED_PARAMETERS, param_name)) {
458 return create_rejected("Parameter is unkown");
459 }
460
461 // Check if the parameter is allowed to change during execution
462 if (isBusy() && !auto_apms_util::contains(EXPLICITLY_ALLOWED_PARAMETERS_WHILE_BUSY, param_name)) {
463 return create_rejected("Parameter is not allowed to change while tree executor is running");
464 }
465
466 // Check if build handler is allowed to change and valid
467 if (param_name == _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_BUILD_HANDLER) {
468 if (!params.allow_other_build_handlers) {
469 return create_rejected(
470 "This executor operates with tree build handler '" + executor_param_listener_.get_params().build_handler +
471 "' and doesn't allow other build handlers to be loaded since the 'Allow other build handlers' option is "
472 "disabled");
473 }
474 const std::string class_name = p.as_string();
475 if (class_name != PARAM_VALUE_NO_BUILD_HANDLER && !build_handler_loader_ptr_->isClassAvailable(class_name)) {
476 return create_rejected(
477 "Cannot load build handler '" + class_name +
478 "' because no corresponding ament_index resource was found. Make sure that you spelled the build handler's "
479 "name correctly "
480 "and registered it by calling auto_apms_behavior_tree_declare_build_handlers() in the CMakeLists.txt of the "
481 "corresponding package");
482 }
483 }
484
485 // At this point, if the parameter hasn't been declared, we do not support it.
486 if (!node_ptr_->has_parameter(param_name)) {
487 return create_rejected("Parameter '" + param_name + "' is not supported");
488 }
489 }
490
491 // If not returned yet, accept to set the parameter
492 rcl_interfaces::msg::SetParametersResult result;
493 result.successful = true;
494 return result;
495}
496
497void TreeExecutorNode::parameter_event_callback_(const rcl_interfaces::msg::ParameterEvent & event)
498{
499 // Look for any updates to parameters of this node
500 std::regex re(node_ptr_->get_fully_qualified_name());
501 if (std::regex_match(event.node, re)) {
502 // Enumerate all changes that came in on this event
503 for (const rclcpp::Parameter & p : rclcpp::ParameterEventHandler::get_parameters_from_event(event)) {
504 const std::string param_name = p.get_name();
505
506 // Change scripting enums
507 if (const std::string enum_key = stripPrefixFromParameterName(SCRIPTING_ENUM_PARAM_PREFIX, param_name);
508 !enum_key.empty()) {
509 updateScriptingEnumsWithParameterValues({{enum_key, p.get_parameter_value()}});
510 }
511
512 // Change blackboard parameters
513 if (const std::string entry_key = stripPrefixFromParameterName(BLACKBOARD_PARAM_PREFIX, param_name);
514 !entry_key.empty()) {
515 updateGlobalBlackboardWithParameterValues({{entry_key, p.get_parameter_value()}}, false);
516 }
517
518 // Change tree build handler instance
519 if (param_name == _AUTO_APMS_BEHAVIOR_TREE__EXECUTOR_PARAM_BUILD_HANDLER) {
520 loadBuildHandler(p.as_string());
521 }
522 }
523 }
524}
525
526rclcpp_action::GoalResponse TreeExecutorNode::handle_start_goal_(
527 const rclcpp_action::GoalUUID & uuid, std::shared_ptr<const StartActionContext::Goal> goal_ptr)
528{
529 // Reject if a tree is already executing
530 if (isBusy()) {
531 RCLCPP_WARN(
532 logger_, "Goal %s was REJECTED: Tree '%s' is currently executing.", rclcpp_action::to_string(uuid).c_str(),
533 getTreeName().c_str());
534 return rclcpp_action::GoalResponse::REJECT;
535 }
536
537 if (!goal_ptr->build_handler.empty()) {
538 if (executor_param_listener_.get_params().allow_other_build_handlers) {
539 try {
540 loadBuildHandler(goal_ptr->build_handler);
541 } catch (const std::exception & e) {
542 RCLCPP_WARN(
543 logger_, "Goal %s was REJECTED: Loading tree build handler '%s' failed: %s",
544 rclcpp_action::to_string(uuid).c_str(), goal_ptr->build_handler.c_str(), e.what());
545 return rclcpp_action::GoalResponse::REJECT;
546 }
547 } else if (goal_ptr->build_handler != current_build_handler_name_) {
548 RCLCPP_WARN(
549 logger_,
550 "Goal %s was REJECTED: Current tree build handler '%s' must not change since the 'Allow other build handlers' "
551 "option is disabled.",
552 rclcpp_action::to_string(uuid).c_str(), current_build_handler_name_.c_str());
553 return rclcpp_action::GoalResponse::REJECT;
554 }
555 }
556
557 core::NodeManifest node_manifest;
558 try {
559 node_manifest = core::NodeManifest::decode(goal_ptr->node_manifest);
560 } catch (const std::exception & e) {
561 RCLCPP_WARN(
562 logger_, "Goal %s was REJECTED: Parsing the initial node manifest failed: %s",
563 rclcpp_action::to_string(uuid).c_str(), e.what());
564 return rclcpp_action::GoalResponse::REJECT;
565 }
566
567 core::NodeManifest node_overrides;
568 try {
569 node_overrides = core::NodeManifest::decode(goal_ptr->node_overrides);
570 } catch (const std::exception & e) {
571 RCLCPP_WARN(
572 logger_, "Goal %s was REJECTED: Parsing the node override manifest failed: %s",
573 rclcpp_action::to_string(uuid).c_str(), e.what());
574 return rclcpp_action::GoalResponse::REJECT;
575 }
576
577 try {
578 tree_constructor_ =
579 makeTreeConstructor(goal_ptr->build_request, goal_ptr->root_tree, node_manifest, node_overrides);
580 } catch (const std::exception & e) {
581 RCLCPP_WARN(logger_, "Goal %s was REJECTED: %s", rclcpp_action::to_string(uuid).c_str(), e.what());
582 return rclcpp_action::GoalResponse::REJECT;
583 }
584 return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
585}
586
587rclcpp_action::CancelResponse TreeExecutorNode::handle_start_cancel_(
588 std::shared_ptr<StartActionContext::GoalHandle> /*goal_handle_ptr*/)
589{
591 return rclcpp_action::CancelResponse::ACCEPT;
592}
593
594void TreeExecutorNode::handle_start_accept_(std::shared_ptr<StartActionContext::GoalHandle> goal_handle_ptr)
595{
596 // Clear blackboard parameters if desired
597 if (goal_handle_ptr->get_goal()->clear_blackboard) {
598 const auto res = node_ptr_->list_parameters({BLACKBOARD_PARAM_PREFIX}, 2);
599 for (const std::string & name : res.names) {
600 node_ptr_->undeclare_parameter(name);
601 };
603 }
604
605 const ExecutorParameters params = executor_param_listener_.get_params();
606 try {
607 startExecution(tree_constructor_, params.tick_rate, params.groot2_port);
608 } catch (const std::exception & e) {
609 auto result_ptr = std::make_shared<StartActionContext::Result>();
610 result_ptr->message = "An error occurred trying to start execution: " + std::string(e.what());
611 result_ptr->tree_result = StartActionContext::Result::TREE_RESULT_NOT_SET;
612 goal_handle_ptr->abort(result_ptr);
613 RCLCPP_ERROR_STREAM(logger_, result_ptr->message);
614 return;
615 }
616 const std::string started_tree_name = getTreeName();
617
618 // If attach is true, the goal's life time is synchronized with the execution. Otherwise we succeed immediately and
619 // leave the executor running (Detached mode).
620 if (goal_handle_ptr->get_goal()->attach) {
621 start_action_context_.setUp(goal_handle_ptr);
622 RCLCPP_INFO(logger_, "Successfully started execution of tree '%s' (Mode: Attached).", started_tree_name.c_str());
623 } else {
624 auto result_ptr = std::make_shared<StartActionContext::Result>();
625 result_ptr->message = "Successfully started execution of tree '" + started_tree_name + "' (Mode: Detached).";
626 result_ptr->tree_result = StartActionContext::Result::TREE_RESULT_NOT_SET;
627 result_ptr->terminated_tree_identity = started_tree_name;
628 goal_handle_ptr->succeed(result_ptr);
629 RCLCPP_INFO_STREAM(logger_, result_ptr->message);
630 }
631}
632
633rclcpp_action::GoalResponse TreeExecutorNode::handle_command_goal_(
634 const rclcpp_action::GoalUUID & /*uuid*/, std::shared_ptr<const CommandActionContext::Goal> goal_ptr)
635{
636 if (command_timer_ptr_ && !command_timer_ptr_->is_canceled()) {
637 RCLCPP_WARN(logger_, "Request for setting tree executor command rejected, because previous one is still busy.");
638 return rclcpp_action::GoalResponse::REJECT;
639 }
640
641 if (isBusy() && start_action_context_.isValid() && start_action_context_.getGoalHandlePtr()->is_canceling()) {
642 RCLCPP_WARN(logger_, "Request for setting tree executor command rejected, because tree executor is canceling.");
643 return rclcpp_action::GoalResponse::REJECT;
644 }
645
646 const auto execution_state = getExecutionState();
647 switch (goal_ptr->command) {
648 case CommandActionContext::Goal::COMMAND_RESUME:
649 if (execution_state == ExecutionState::PAUSED || execution_state == ExecutionState::HALTED) {
650 RCLCPP_INFO(logger_, "Tree with ID '%s' will RESUME.", getTreeName().c_str());
651 } else {
652 RCLCPP_WARN(
653 logger_, "Requested to RESUME with executor being in state %s. Rejecting request.",
654 toStr(execution_state).c_str());
655 return rclcpp_action::GoalResponse::REJECT;
656 }
657 break;
658 case CommandActionContext::Goal::COMMAND_PAUSE:
659 if (execution_state == ExecutionState::STARTING || execution_state == ExecutionState::RUNNING) {
660 RCLCPP_INFO(logger_, "Tree with ID '%s' will PAUSE", getTreeName().c_str());
661 } else {
662 RCLCPP_INFO(
663 logger_, "Requested to PAUSE with executor already being inactive (State: %s).",
664 toStr(execution_state).c_str());
665 }
666 break;
667 case CommandActionContext::Goal::COMMAND_HALT:
668 if (
669 execution_state == ExecutionState::STARTING || execution_state == ExecutionState::RUNNING ||
670 execution_state == ExecutionState::PAUSED) {
671 RCLCPP_INFO(logger_, "Tree with ID '%s' will HALT.", getTreeName().c_str());
672 } else {
673 RCLCPP_INFO(
674 logger_, "Requested to HALT with executor already being inactive (State: %s).",
675 toStr(execution_state).c_str());
676 }
677 break;
678 case CommandActionContext::Goal::COMMAND_TERMINATE:
679 if (isBusy()) {
680 RCLCPP_INFO(logger_, "Executor will TERMINATE tree '%s'.", getTreeName().c_str());
681 } else {
682 RCLCPP_INFO(
683 logger_, "Requested to TERMINATE with executor already being inactive (State: %s).",
684 toStr(execution_state).c_str());
685 }
686 break;
687 default:
688 RCLCPP_WARN(logger_, "Executor command %i is undefined. Rejecting request.", goal_ptr->command);
689 return rclcpp_action::GoalResponse::REJECT;
690 }
691 return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
692}
693
694rclcpp_action::CancelResponse TreeExecutorNode::handle_command_cancel_(
695 std::shared_ptr<CommandActionContext::GoalHandle> /*goal_handle_ptr*/)
696{
697 return rclcpp_action::CancelResponse::ACCEPT;
698}
699
700void TreeExecutorNode::handle_command_accept_(std::shared_ptr<CommandActionContext::GoalHandle> goal_handle_ptr)
701{
702 const auto command_request = goal_handle_ptr->get_goal()->command;
703 ExecutionState requested_state;
704 switch (command_request) {
705 case CommandActionContext::Goal::COMMAND_RESUME:
707 requested_state = ExecutionState::RUNNING;
708 break;
709 case CommandActionContext::Goal::COMMAND_PAUSE:
711 requested_state = ExecutionState::PAUSED;
712 break;
713 case CommandActionContext::Goal::COMMAND_HALT:
715 requested_state = ExecutionState::HALTED;
716 break;
717 case CommandActionContext::Goal::COMMAND_TERMINATE:
719 requested_state = ExecutionState::IDLE;
720 break;
721 default:
722 throw std::logic_error("command_request is unkown");
723 }
724
725 command_timer_ptr_ = node_ptr_->create_wall_timer(
726 std::chrono::duration<double>(executor_param_listener_.get_params().tick_rate),
727 [this, requested_state, goal_handle_ptr, action_result_ptr = std::make_shared<CommandActionContext::Result>()]() {
728 // Check if canceling
729 if (goal_handle_ptr->is_canceling()) {
730 // Will abandon any progress
731 goal_handle_ptr->canceled(action_result_ptr);
732 command_timer_ptr_->cancel();
733 return;
734 }
735
736 const auto current_state = getExecutionState();
737
738 // If the execution state has become IDLE in the mean time, request failed if termination was not desired
739 if (requested_state != ExecutionState::IDLE && current_state == ExecutionState::IDLE) {
740 RCLCPP_ERROR(
741 logger_, "Failed to reach requested state %s due to cancellation of execution timer. Aborting.",
742 toStr(requested_state).c_str());
743 goal_handle_ptr->abort(action_result_ptr);
744 command_timer_ptr_->cancel();
745 return;
746 }
747
748 // Wait for the requested state to be reached
749 if (current_state != requested_state) return;
750
751 goal_handle_ptr->succeed(action_result_ptr);
752 command_timer_ptr_->cancel();
753 });
754}
755
756bool TreeExecutorNode::onTick()
757{
758 const ExecutorParameters params = executor_param_listener_.get_params();
759
760 // Set state change logging flag
761 getStateObserver().setLogging(params.state_change_logger);
762 return true;
763}
764
766{
767 const ExecutorParameters params = executor_param_listener_.get_params();
768
772
773 if (executor_options_.blackboard_parameters_dynamic_ && params.allow_dynamic_blackboard) {
774 TreeBlackboardSharedPtr bb_ptr = getGlobalBlackboardPtr();
775 std::vector<rclcpp::Parameter> new_parameters;
776 for (const BT::StringView & str : bb_ptr->getKeys()) {
777 const std::string key = std::string(str);
778 const BT::TypeInfo * type_info = bb_ptr->entryInfo(key);
779 const BT::Any * any = bb_ptr->getAnyLocked(key).get();
780
781 // BehaviorTree.CPP does some type validation logic for all data ports when instantiating the tree. It parses
782 // the XML file, finds the ports of all nodes and initializes blackboard entries wherever used with the
783 // corresponding type. This is done to to detect type mismatches at construction time. This means, that the
784 // blackboard will be initialized with empty instances of BT::Any by the time the tree is created. Therefore, we
785 // must additionally check whether the blackboard entry is empty or not using BT::Any::empty().
786 if (any->empty()) continue;
787
788 // If the entry has actually been set with any value during runtime, we update this node's parameters
789
790 if (translated_global_blackboard_entries_.find(key) == translated_global_blackboard_entries_.end()) {
791 // The key is new, so we must try to infer the parameter's type from BT::Any
792 const BT::Expected<rclcpp::ParameterValue> expected =
793 createParameterValueFromAny(*any, rclcpp::PARAMETER_NOT_SET);
794 if (expected) {
795 new_parameters.push_back(rclcpp::Parameter(BLACKBOARD_PARAM_PREFIX + "." + key, expected.value()));
796 translated_global_blackboard_entries_[key] = expected.value();
797 } else {
798 RCLCPP_WARN(
799 logger_, "Failed to translate new blackboard entry '%s' (Type: %s) to parameters: %s", key.c_str(),
800 type_info->typeName().c_str(), expected.error().c_str());
801 }
802 } else {
803 // The key is not new, so we can look up the parameter's type and update its value (if it changed)
804 const BT::Expected<rclcpp::ParameterValue> expected =
805 createParameterValueFromAny(*any, translated_global_blackboard_entries_[key].get_type());
806 if (expected) {
807 if (expected.value() != translated_global_blackboard_entries_[key]) {
808 new_parameters.push_back(rclcpp::Parameter(BLACKBOARD_PARAM_PREFIX + "." + key, expected.value()));
809 }
810 } else {
811 RCLCPP_WARN(
812 logger_, "Failed to translate blackboard entry '%s' (Type: %s) to parameters: %s", key.c_str(),
813 type_info->typeName().c_str(), expected.error().c_str());
814 }
815 }
816 }
817 if (!new_parameters.empty()) {
818 const rcl_interfaces::msg::SetParametersResult result = node_ptr_->set_parameters_atomically(new_parameters);
819 if (!result.successful) {
820 throw exceptions::TreeExecutorError(
821 "Unexpectedly failed to set parameters inferred from global blackboard. Reason: " + result.reason);
822 }
823 }
824 }
825
829
830 // Only send feedback if started in attached mode
831 if (start_action_context_.isValid()) {
832 TreeStateObserver & state_observer = getStateObserver();
833 auto feedback_ptr = start_action_context_.getFeedbackPtr(); // feedback from previous tick persists
834 feedback_ptr->execution_state_str = toStr(getExecutionState());
835 feedback_ptr->running_tree_identity = getTreeName();
836 auto running_action_history = state_observer.getRunningActionHistory();
837 if (!running_action_history.empty()) {
838 // If there are multiple nodes running (ParallelNode), join the IDs to a single string
839 feedback_ptr->running_action_name = auto_apms_util::join(running_action_history, " + ");
840 feedback_ptr->running_action_timestamp =
841 std::chrono::duration<double>{std::chrono::high_resolution_clock::now().time_since_epoch()}.count();
842
843 // Reset the history cache
844 state_observer.flush();
845 }
846 start_action_context_.publishFeedback();
847 }
848
849 return true;
850}
851
852void TreeExecutorNode::onTermination(const ExecutionResult & result)
853{
854 if (!start_action_context_.isValid()) // Do nothing if started in detached mode
855 return;
856
857 auto result_ptr = start_action_context_.getResultPtr();
858 result_ptr->terminated_tree_identity = getTreeName();
859 switch (result) {
861 result_ptr->tree_result = StartActionContext::Result::TREE_RESULT_SUCCESS;
862 result_ptr->message = "Tree execution finished with status SUCCESS";
863 start_action_context_.succeed();
864 break;
866 result_ptr->tree_result = StartActionContext::Result::TREE_RESULT_FAILURE;
867 result_ptr->message = "Tree execution finished with status FAILURE";
868 start_action_context_.abort();
869 break;
871 result_ptr->tree_result = StartActionContext::Result::TREE_RESULT_NOT_SET;
872 if (start_action_context_.getGoalHandlePtr()->is_canceling()) {
873 result_ptr->message = "Tree execution canceled successfully";
874 start_action_context_.cancel();
875 } else {
876 result_ptr->message = "Tree execution terminated prematurely";
877 start_action_context_.abort();
878 }
879 break;
881 result_ptr->tree_result = StartActionContext::Result::TREE_RESULT_NOT_SET;
882 result_ptr->message = "An unexpected error occurred during tree execution";
883 start_action_context_.abort();
884 break;
885 default:
886 result_ptr->tree_result = StartActionContext::Result::TREE_RESULT_NOT_SET;
887 result_ptr->message = "Execution result unkown";
888 start_action_context_.abort();
889 break;
890 }
891
892 // Reset action context
893 start_action_context_.invalidate();
894}
895
896} // 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.
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.
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.
@ 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.
TreeBlackboardSharedPtr getGlobalBlackboardPtr()
Get a shared pointer to the global blackboard instance.
std::string getTreeName()
Get the name of the tree that is currently executing.
@ 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.
Configuration options for TreeExecutorNode.
TreeExecutorNodeOptions & enableScriptingEnumParameters(bool from_overrides, bool dynamic)
Configure whether the executor node accepts scripting enum parameters.
rclcpp::NodeOptions getROSNodeOptions() const
Get the ROS 2 node options that comply with the given options.
TreeExecutorNodeOptions(const rclcpp::NodeOptions &ros_node_options)
Constructor.
TreeExecutorNodeOptions & enableGlobalBlackboardParameters(bool from_overrides, bool dynamic)
Configure whether the executor node accepts global blackboard parameters.
TreeExecutorNodeOptions & setDefaultBuildHandler(const std::string &name)
Specify a default behavior tree build handler that will be used initially.
bool updateGlobalBlackboardWithParameterValues(const std::map< std::string, rclcpp::ParameterValue > &value_map, bool simulate=false)
Update the global blackboard using parameter values.
static std::string stripPrefixFromParameterName(const std::string &prefix, const std::string &param_name)
Get the name of a parameter without its prefix.
virtual void setUpBuilder(TreeBuilder &builder, const core::NodeManifest &node_manifest)
Callback invoked every time before any behavior trees are built.
std::map< std::string, rclcpp::ParameterValue > getParameterValuesWithPrefix(const std::string &prefix)
Assemble all parameters of this node that have a specific prefix.
std::shared_future< ExecutionResult > startExecution(const std::string &tree_build_request)
Start the behavior tree that is specified by a particular build request.
bool updateScriptingEnumsWithParameterValues(const std::map< std::string, rclcpp::ParameterValue > &value_map, bool simulate=false)
Update the internal buffer of scripting enums used when a behavior tree is created.
TreeConstructor makeTreeConstructor(const std::string &build_handler_request, const std::string &root_tree_name, const core::NodeManifest &node_manifest={}, const core::NodeManifest &node_overrides={})
Create a callback that builds a behavior tree according to a specific request.
void loadBuildHandler(const std::string &name)
Load a particular behavior tree build handler plugin.
TreeExecutorNode(const std::string &name, TreeExecutorNodeOptions executor_options)
Constructor allowing to specify a custom node name and executor options.
State observer for a particular behavior tree object that writes introspection and debugging informat...
virtual void flush() override
Reset the internal state variables.
const std::vector< std::string > & getRunningActionHistory() const
Get all names of action nodes that returned BT::NodeStatus::RUNNING since the last time TreeStateObse...
void setLogging(bool active)
Configure whether the observer should write to the logger.
Data structure for information about which behavior tree node plugin to load and how to configure the...
Class for configuring and instantiating behavior trees.
Definition builder.hpp:55
void cancel()
Terminate the current goal and mark it as canceled.
std::shared_ptr< GoalHandle > getGoalHandlePtr()
Get the goal handle managed by this ActionContext instance.
std::shared_ptr< Result > getResultPtr()
Access the internal action result buffer.
void succeed()
Terminate the current goal and mark it as succeeded.
void abort()
Terminate the current goal and mark it as aborted.
bool isValid()
Check if this ActionContext is valid (e.g. is managing a valid action goal handle).
bool contains(const ContainerT< ValueT, AllocatorT > &c, const ValueT &val)
Check whether a particular container structure contains a value.
Definition container.hpp:36
std::string printMap(const std::map< std::string, std::string > &map, const std::string &key_val_sep="=", const std::string &entry_sep=", ")
Converts a map to a string representation that is suited for printing to console.
Definition string.cpp:50
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
BT::Expected< BT::Any > createAnyFromParameterValue(const rclcpp::ParameterValue &val)
Convert a ROS 2 parameter value to a BT::Any object.
Definition parameter.cpp:20
BT::Expected< rclcpp::ParameterValue > createParameterValueFromAny(const BT::Any &any, rclcpp::ParameterType type)
Convert a BT::Any object to a ROS 2 parameter value.
Definition parameter.cpp:51