AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
ros_node_context.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_core/node/ros_node_context.hpp"
16
17#include <iterator>
18#include <regex>
19
20#include "auto_apms_behavior_tree_core/exceptions.hpp"
21#include "auto_apms_util/logging.hpp"
22
24{
26 rclcpp::Node::SharedPtr ros_node, rclcpp::CallbackGroup::SharedPtr tree_node_waitables_callback_group,
27 rclcpp::executors::SingleThreadedExecutor::SharedPtr tree_node_waitables_executor,
28 const NodeRegistrationOptions & options)
29: ros_node_name_(ros_node ? ros_node->get_name() : ""),
30 fully_qualified_ros_node_name_(ros_node ? ros_node->get_fully_qualified_name() : ""),
31 base_logger_(ros_node ? ros_node->get_logger() : rclcpp::get_logger("")),
32 nh_(ros_node),
33 cb_group_(tree_node_waitables_callback_group),
34 executor_(tree_node_waitables_executor),
35 registration_options_(options)
36{
37}
38
39std::string RosNodeContext::getROSNodeName() const { return ros_node_name_; }
40
41std::string RosNodeContext::getFullyQualifiedRosNodeName() const { return fully_qualified_ros_node_name_; }
42
43rclcpp::Logger RosNodeContext::getBaseLogger() const { return base_logger_; }
44
45rclcpp::Logger RosNodeContext::getChildLogger(const std::string & name)
46{
47 const rclcpp::Logger child_logger = base_logger_.get_child(name);
48 if (!registration_options_.logger_level.empty()) {
49 try {
50 auto_apms_util::setLoggingSeverity(child_logger, registration_options_.logger_level);
51 } catch (const auto_apms_util::exceptions::SetLoggingSeverityError & e) {
52 RCLCPP_ERROR(
53 base_logger_,
54 "Failed to set the logging severity for the child logger using the node's registration options: %s", e.what());
55 }
56 }
57 return child_logger;
58}
59
61{
62 if (const rclcpp::Node::SharedPtr node = nh_.lock()) {
63 return node->now();
64 }
65 return rclcpp::Clock(RCL_ROS_TIME).now();
66}
67
68std::string RosNodeContext::getFullyQualifiedTreeNodeName(const BT::TreeNode * node, bool with_class_name) const
69{
70 // NOTE: registrationName() is empty during construction as this member is first set after the factory constructed the
71 // object
72 const std::string instance_name = node->name();
73 const std::string registration_name = node->registrationName();
74 if (!registration_name.empty() && instance_name != registration_name) {
75 if (with_class_name) {
76 return instance_name + " (" + registration_name + " : " + registration_options_.class_name + ")";
77 }
78 return instance_name + " (" + registration_name + ")";
79 }
80 return with_class_name ? (instance_name + " (" + registration_options_.class_name + ")") : instance_name;
81}
82
83YAML::Node RosNodeContext::getExtraOptions() const { return registration_options_.extra; }
84
85BT::Expected<std::string> RosNodeContext::getTopicName(const BT::TreeNode * node) const
86{
87 std::string res = registration_options_.topic;
88 if (res.empty()) {
89 return nonstd::make_unexpected(
91 " - Cannot get the name of the node's associated ROS 2 topic: Registration option '" +
92 NodeRegistrationOptions::PARAM_NAME_ROS2TOPIC + "' is empty.");
93 }
94 BT::PortsRemapping input_ports = node->config().input_ports;
95
96 // Parameter registration_options_.topic may contain substrings, that that are to be replaced with values retrieved
97 // from a specific node input port. Must be something like (input:my_port) where 'my_port' is the key/name of the
98 // BT::InputPort to use. Anything before or after the expression is kept and used as a prefix respectively suffix.
99 const std::regex pattern(R"(\‍(input:([^)\s]+)\))");
100 const std::sregex_iterator replace_begin(res.begin(), res.end(), pattern);
101 const std::sregex_iterator replace_end = std::sregex_iterator();
102
103 // We iterate over each substitution expression. If there are none, this for loop has no effect, and we simply return
104 // the parameters value.
105 for (std::sregex_iterator it = replace_begin; it != replace_end; ++it) {
106 const std::smatch match = *it;
107 const std::string input_port_key = match[1].str();
108
109 // Search for the specified input port key in the list of input ports passed at construction time
110 if (input_ports.find(input_port_key) != input_ports.end()) {
111 // The input port has been found using input_port_key
112
113 // Make sure its value is either a blackboard pointer or a static string (Must not be empty)
114 if (input_ports.at(input_port_key).empty()) {
115 return nonstd::make_unexpected(
117 " - Cannot get the name of the node's associated ROS 2 topic: Input port '" + input_port_key +
118 "' required by substring '" + match.str() + "' must not be empty.");
119 }
120
121 // We try to get the value from the node input port. If the value is a string pointing at a blackboard entry, this
122 // may not work during construction time. In case the expected value contains an error, we forward it to indicate
123 // we weren't successful.
124 const BT::Expected<std::string> expected = node->getInput<std::string>(input_port_key);
125 if (expected) {
126 // Replace the respective substring with the value returned from getInput()
127 res.replace(match.position(), match.length(), expected.value());
128 } else {
129 // Return expected (contains error) if value couldn't be retrieved from input ports
130 return expected;
131 }
132 } else {
133 return nonstd::make_unexpected(
135 " - Cannot get the name of the node's associated ROS 2 topic: Input port '" + input_port_key +
136 "' required by substring '" + match.str() + "' doesn't exist.");
137 }
138 }
139 return res;
140}
141
147std::pair<std::string, std::string> parseAliasPortName(const std::string & str)
148{
149 static const std::regex alias_regex(R"(^\s*([^\s(]+)\s*(?:\‍(([^)]*)\))?\s*$)");
150 std::smatch match;
151 if (std::regex_match(str, match, alias_regex)) {
152 return {match[1].str(), match[2].str()}; // {alias_name, description}
153 }
154 return {str, ""}; // Fallback if regex doesn't match
155}
156
157void RosNodeContext::modifyProvidedPortsListForRegistration(BT::PortsList & ports_list) const
158{
159 // Add port aliases if specified in the node manifest (original port is kept for compatibility with the
160 // implementation, but hidden in the node model)
161 std::map<std::string, std::string> original_alias_name_map;
162 for (const auto & [original_port_name, aliased_port_name] : registration_options_.port_alias) {
163 if (ports_list.find(original_port_name) == ports_list.end()) {
164 throw exceptions::NodeRegistrationError(
165 "Cannot alias port '" + original_port_name + "' which is not provided by class '" +
166 registration_options_.class_name + "'. The keys under " + NodeRegistrationOptions::PARAM_NAME_PORT_ALIAS +
167 " must refer to a port implemented by the node.");
168 }
169 BT::PortInfo port_info = ports_list.at(original_port_name);
170 const auto [aliased_name_cleaned, aliased_description] = parseAliasPortName(aliased_port_name);
171 original_alias_name_map[original_port_name] = aliased_name_cleaned;
172
173 // Update description if provided within round brackets
174 if (!aliased_description.empty()) {
175 port_info.setDescription(aliased_description);
176 }
177
178 // Insert additional port
179 ports_list.insert({aliased_name_cleaned, port_info});
180 }
181
182 // Modify the default value of the ports if specified in the node manifest
183 for (const auto & [port_name, new_default] : registration_options_.port_default) {
184 if (ports_list.find(port_name) == ports_list.end()) {
185 throw exceptions::NodeRegistrationError(
186 "Cannot set default value for port '" + port_name + "' which is not provided by class '" +
187 registration_options_.class_name + "'. The keys under " + NodeRegistrationOptions::PARAM_NAME_PORT_DEFAULT +
188 " must refer to a port implemented by the node.");
189 }
190 // We're passing the new default value as string. Conversion is done when getting the port value during
191 // execution (also allows blackboard pointers)
192 ports_list.at(port_name).setDefaultValue(new_default);
193
194 // If it's an aliased port, we also need to update the default of the original port and vice versa
195 if (original_alias_name_map.find(port_name) != original_alias_name_map.end()) {
196 // User provided the original port name for setting the default value, we update the alias port
197 const std::string aliased_port_name = original_alias_name_map.at(port_name);
198 ports_list.at(aliased_port_name).setDefaultValue(new_default);
199 } else {
200 for (const auto & [original_port_name, aliased_port_name] : original_alias_name_map) {
201 if (aliased_port_name == port_name) {
202 // User provided the aliased port name for setting the default value, we update the original port
203 ports_list.at(original_port_name).setDefaultValue(new_default);
204 break;
205 }
206 }
207 }
208 }
209}
210
211BT::PortsRemapping RosNodeContext::copyAliasedPortValuesToOriginalPorts(const BT::TreeNode * node) const
212{
213 const BT::PortsRemapping & input_ports = node->config().input_ports;
214 const BT::PortsRemapping & output_ports = node->config().output_ports;
215
216 BT::PortsRemapping remapping;
217
218 // Lambda for handling aliasing
219 auto process_ports = [&](const BT::PortsRemapping & node_ports) {
220 for (const auto & [original_key, port_info] : node_ports) {
221 // Check if this original port has been aliased
222 const auto alias_it = registration_options_.port_alias.find(original_key);
223 if (alias_it != registration_options_.port_alias.end()) {
224 // Port has been aliased, copy value from aliased port to original port
225 const auto [aliased_port_name, _] = parseAliasPortName(alias_it->second);
226 auto aliased_port_it = node_ports.find(aliased_port_name);
227 if (aliased_port_it != node_ports.end()) {
228 // Aliased port exists, copy its value to the original port
229 remapping[original_key] = aliased_port_it->second;
230 } else {
231 throw exceptions::NodeRegistrationError(
232 "Error processing port aliasing: Cannot find aliased port '" + aliased_port_name + "' for original port '" +
233 original_key + "' in provided ports by node '" + getFullyQualifiedTreeNodeName(node, true) + "'.");
234 }
235 } else {
236 // No aliasing for this port, nothing to do
237 }
238 }
239 };
240
241 // Process input and output ports
242 process_ports(input_ports);
243 process_ports(output_ports);
244
245 return remapping;
246}
247
248} // namespace auto_apms_behavior_tree::core
std::string getROSNodeName() const
Get the name of the ROS 2 node passed to the constructor.
rclcpp::Logger getChildLogger(const std::string &name)
Get a child logger created using the associated ROS 2 node.
rclcpp::Time getCurrentTime() const
Get the current time using the associated ROS 2 node.
RosNodeContext(rclcpp::Node::SharedPtr ros_node, rclcpp::CallbackGroup::SharedPtr tree_node_waitables_callback_group, rclcpp::executors::SingleThreadedExecutor::SharedPtr tree_node_waitables_executor, const NodeRegistrationOptions &options)
Constructor.
std::string getFullyQualifiedTreeNodeName(const BT::TreeNode *node, bool with_class_name=true) const
Create a string representing the detailed name of a behavior tree node.
std::string getFullyQualifiedRosNodeName() const
Get the fully qualified name of the ROS 2 node passed to the constructor.
YAML::Node getExtraOptions() const
Get the extra YAML options provided during node registration.
rclcpp::Logger getBaseLogger() const
Get the logger of the associated ROS 2 node.
void setLoggingSeverity(const rclcpp::Logger &logger, const std::string &severity)
Set the logging severity of a ROS 2 logger.
Definition logging.cpp:25
Core API for AutoAPMS's behavior tree implementation.
Definition behavior.hpp:32
std::pair< std::string, std::string > parseAliasPortName(const std::string &str)
Parse alias port name and optional description from format "alias_name" or "alias_name (description)"...
Parameters for loading and registering a behavior tree node class from a shared library using e....
std::string topic
Name of the ROS 2 communication interface to connect with.
std::map< std::string, std::string > port_alias
Provides the possibility to rename ports implemented by class_name.
std::string class_name
Fully qualified name of the behavior tree node plugin class.