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// 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_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
85rclcpp::Node::SharedPtr RosNodeContext::getRosNode() const
86{
87 const rclcpp::Node::SharedPtr node = nh_.lock();
88 if (!node) {
89 throw exceptions::RosNodeError(
90 "Cannot access the associated ROS 2 node from the RosNodeContext because the weak pointer expired. The tree "
91 "node doesn't take ownership of it.");
92 }
93 return node;
94}
95
96rclcpp::CallbackGroup::SharedPtr RosNodeContext::getWaitablesCallbackGroup() const { return cb_group_.lock(); }
97
98rclcpp::executors::SingleThreadedExecutor::SharedPtr RosNodeContext::getWaitablesExecutor() const
99{
100 return executor_.lock();
101}
102
103const NodeRegistrationOptions & RosNodeContext::getRegistrationOptions() const { return registration_options_; }
104
105BT::Expected<std::string> RosNodeContext::getTopicName(const BT::TreeNode * node) const
106{
107 std::string res = registration_options_.topic;
108 if (res.empty()) {
109 return nonstd::make_unexpected(
111 " - Cannot get the name of the node's associated ROS 2 topic: Registration option '" +
112 NodeRegistrationOptions::PARAM_NAME_ROS2TOPIC + "' is empty.");
113 }
114 BT::PortsRemapping input_ports = node->config().input_ports;
115
116 // Parameter registration_options_.topic may contain substrings, that that are to be replaced with values retrieved
117 // from a specific node input port. Must be something like (input:my_port) where 'my_port' is the key/name of the
118 // BT::InputPort to use. Anything before or after the expression is kept and used as a prefix respectively suffix.
119 const std::regex pattern(R"(\‍(input:([^)\s]+)\))");
120 const std::sregex_iterator replace_begin(res.begin(), res.end(), pattern);
121 const std::sregex_iterator replace_end = std::sregex_iterator();
122
123 // We iterate over each substitution expression. If there are none, this for loop has no effect, and we simply return
124 // the parameters value.
125 for (std::sregex_iterator it = replace_begin; it != replace_end; ++it) {
126 const std::smatch match = *it;
127 const std::string input_port_key = match[1].str();
128
129 // Search for the specified input port key in the list of input ports passed at construction time
130 if (input_ports.find(input_port_key) != input_ports.end()) {
131 // The input port has been found using input_port_key
132
133 // Make sure its value is either a blackboard pointer or a static string (Must not be empty)
134 if (input_ports.at(input_port_key).empty()) {
135 return nonstd::make_unexpected(
137 " - Cannot get the name of the node's associated ROS 2 topic: Input port '" + input_port_key +
138 "' required by substring '" + match.str() + "' must not be empty.");
139 }
140
141 // We try to get the value from the node input port. If the value is a string pointing at a blackboard entry, this
142 // may not work during construction time. In case the expected value contains an error, we forward it to indicate
143 // we weren't successful.
144 const BT::Expected<std::string> expected = node->getInput<std::string>(input_port_key);
145 if (expected) {
146 // Replace the respective substring with the value returned from getInput()
147 res.replace(match.position(), match.length(), expected.value());
148 } else {
149 // Return expected (contains error) if value couldn't be retrieved from input ports
150 return expected;
151 }
152 } else {
153 return nonstd::make_unexpected(
155 " - Cannot get the name of the node's associated ROS 2 topic: Input port '" + input_port_key +
156 "' required by substring '" + match.str() + "' doesn't exist.");
157 }
158 }
159 return res;
160}
161
171std::pair<std::string, std::string> parseAliasPortName(const std::string & str)
172{
173 // The description group is greedy ((.*) rather than ([^)]*)) so it extends to the last ')' in the string. This lets
174 // the description contain round brackets of its own while the outermost pair still delimits it.
175 static const std::regex alias_regex(R"(^\s*([^\s(]+)\s*(?:\‍((.*)\))?\s*$)");
176 std::smatch match;
177 if (std::regex_match(str, match, alias_regex)) {
178 return {match[1].str(), match[2].str()}; // {alias_name, description}
179 }
180 return {str, ""}; // Fallback if regex doesn't match
181}
182
183void RosNodeContext::modifyProvidedPortsListForRegistration(BT::PortsList & ports_list) const
184{
185 // Add port aliases if specified in the node manifest (original port is kept for compatibility with the
186 // implementation, but hidden in the node model)
187 std::map<std::string, std::string> original_alias_name_map;
188 for (const auto & [original_port_name, aliased_port_name] : registration_options_.port_alias) {
189 if (ports_list.find(original_port_name) == ports_list.end()) {
190 throw exceptions::NodeRegistrationError(
191 "Cannot alias port '" + original_port_name + "' which is not provided by class '" +
192 registration_options_.class_name + "'. The keys under " + NodeRegistrationOptions::PARAM_NAME_PORT_ALIAS +
193 " must refer to a port implemented by the node.");
194 }
195 BT::PortInfo port_info = ports_list.at(original_port_name);
196 const auto [aliased_name_cleaned, aliased_description] = parseAliasPortName(aliased_port_name);
197 original_alias_name_map[original_port_name] = aliased_name_cleaned;
198
199 // Update description if provided within round brackets
200 if (!aliased_description.empty()) {
201 port_info.setDescription(aliased_description);
202 }
203
204 // Insert additional port
205 ports_list.insert({aliased_name_cleaned, port_info});
206 }
207
208 // Modify the default value of the ports if specified in the node manifest
209 for (const auto & [port_name, new_default] : registration_options_.port_default) {
210 if (ports_list.find(port_name) == ports_list.end()) {
211 throw exceptions::NodeRegistrationError(
212 "Cannot set default value for port '" + port_name + "' which is not provided by class '" +
213 registration_options_.class_name + "'. The keys under " + NodeRegistrationOptions::PARAM_NAME_PORT_DEFAULT +
214 " must refer to a port implemented by the node.");
215 }
216 // We're passing the new default value as string. Conversion is done when getting the port value during
217 // execution (also allows blackboard pointers)
218 ports_list.at(port_name).setDefaultValue(new_default);
219
220 // If it's an aliased port, we also need to update the default of the original port and vice versa
221 if (original_alias_name_map.find(port_name) != original_alias_name_map.end()) {
222 // User provided the original port name for setting the default value, we update the alias port
223 const std::string aliased_port_name = original_alias_name_map.at(port_name);
224 ports_list.at(aliased_port_name).setDefaultValue(new_default);
225 } else {
226 for (const auto & [original_port_name, aliased_port_name] : original_alias_name_map) {
227 if (aliased_port_name == port_name) {
228 // User provided the aliased port name for setting the default value, we update the original port
229 ports_list.at(original_port_name).setDefaultValue(new_default);
230 break;
231 }
232 }
233 }
234 }
235}
236
237void RosNodeContext::copyAliasedPortValuesToOriginalPorts(const BT::TreeNode * node) const
238{
239 // The node owns its NodeConfig; config() only hands back a const reference. We copy the aliased port values directly
240 // onto the original ports here rather than going through BT::TreeNode::modifyPortsRemapping, because that only
241 // *updates* ports already present in the config. The original port is typically absent from the config when it has
242 // no default value and the user only ever sets the aliased port, so it must be *inserted* - otherwise the node reads
243 // an unset original port (e.g. an empty list for a std::vector<std::string> port, or an outright getInput() failure).
244 BT::NodeConfig & config = const_cast<BT::NodeConfig &>(node->config());
245
246 // Iterate the configured aliases (original -> aliased) rather than the node's populated ports: the value the user
247 // provides lives on the *aliased* port and must be copied onto the *original* port the node implementation reads via
248 // getInput()/getOutput().
249 for (const auto & [original_port_name, aliased_port_spec] : registration_options_.port_alias) {
250 const auto [aliased_port_name, _] = parseAliasPortName(aliased_port_spec);
251
252 // The aliased port may be an input or an output port; copy from whichever map holds its value. If the aliased port
253 // carries no value at all (neither set in the XML nor via a default), there is nothing to copy and the original
254 // port is left unset - exactly as if the port had not been provided.
255 if (const auto it = config.input_ports.find(aliased_port_name); it != config.input_ports.end()) {
256 config.input_ports[original_port_name] = it->second;
257 } else if (const auto out_it = config.output_ports.find(aliased_port_name); out_it != config.output_ports.end()) {
258 config.output_ports[original_port_name] = out_it->second;
259 }
260 }
261}
262
263} // 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::CallbackGroup::SharedPtr getWaitablesCallbackGroup() const
Get the callback group to use when adding ROS 2 waitables (subscriptions, clients,...
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.
rclcpp::Node::SharedPtr getRosNode() const
Get the associated ROS 2 node handle.
const NodeRegistrationOptions & getRegistrationOptions() const
Get the node registration options.
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.
BT::Expected< std::string > getTopicName(const BT::TreeNode *node) const
Resolve the ROS 2 topic name from the node's registration options (node manifest 'topic' feature),...
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.
rclcpp::executors::SingleThreadedExecutor::SharedPtr getWaitablesExecutor() const
Get the executor used for spinning the tree node's waitables.
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.