AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
node_registration_options.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/node_registration_options.hpp"
16
17#include "auto_apms_behavior_tree_core/exceptions.hpp"
18#include "auto_apms_behavior_tree_core/node/node_manifest.hpp"
19
21{
22
23bool NodeRegistrationOptions::valid() const { return !class_name.empty(); }
24
26{
27 // Compare by canonical YAML serialization. This is the single source of truth for registration-options equality:
28 // it covers every field (including the free-form 'extra' node) without requiring per-field comparison and keeps the
29 // semantics identical on both sides of the comparison.
30 const auto fingerprint = [](const NodeRegistrationOptions & options) {
31 YAML::Emitter emitter;
32 emitter << YAML::Flow << YAML::convert<NodeRegistrationOptions>::encode(options);
33 return std::string(emitter.c_str());
34 };
35 return fingerprint(*this) == fingerprint(other);
36}
37
38bool NodeRegistrationOptions::operator!=(const NodeRegistrationOptions & other) const { return !(*this == other); }
39
40} // namespace auto_apms_behavior_tree::core
41
43namespace YAML
44{
45
46Node convert<auto_apms_behavior_tree::core::NodeRegistrationOptions>::encode(const Options & rhs)
47{
48 Node node(NodeType::Map);
49 // Note: We intentionally do not encode the 'parent' field, since we directly evaluate it during decoding
50 node[Options::PARAM_NAME_CLASS] = rhs.class_name;
51 node[Options::PARAM_NAME_DESCRIPTION] = rhs.description;
52 node[Options::PARAM_NAME_ROS2TOPIC] = rhs.topic;
53 node[Options::PARAM_NAME_PORT_ALIAS] = rhs.port_alias;
54 node[Options::PARAM_NAME_PORT_DEFAULT] = rhs.port_default;
55 node[Options::PARAM_NAME_HIDDEN_PORTS] = rhs.hidden_ports;
56 node[Options::PARAM_NAME_WAIT_TIMEOUT] = rhs.wait_timeout.count();
57 node[Options::PARAM_NAME_REQUEST_TIMEOUT] = rhs.request_timeout.count();
58 node[Options::PARAM_NAME_ALLOW_UNREACHABLE] = rhs.allow_unreachable;
59 node[Options::PARAM_NAME_LOGGER_LEVEL] = rhs.logger_level;
60 node[Options::PARAM_NAME_EXTRA] = rhs.extra;
61 return node;
62}
63
64bool convert<auto_apms_behavior_tree::core::NodeRegistrationOptions>::decode(
66{
67 using namespace auto_apms_behavior_tree;
68 using namespace auto_apms_behavior_tree::core;
69 using Options = NodeRegistrationOptions;
70
71 if (!node.IsMap())
72 throw auto_apms_util::exceptions::YAMLFormatError(
73 "YAML::Node for auto_apms_behavior_tree::core::NodeRegistrationOptions must be map but is type " +
74 std::to_string(node.Type()) + " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
75
76 // Resolve parent field first (if present) — sets rhs to the inherited parent options as a base.
77 // This avoids the need to track overrides separately: we simply apply the remaining fields on top afterward.
78 if (const Node parent_val = node[Options::PARAM_NAME_PARENT]) {
79 if (!parent_val.IsScalar()) {
80 throw auto_apms_util::exceptions::YAMLFormatError(
81 "Value for key '" + std::string(Options::PARAM_NAME_PARENT) + "' must be scalar but is type " +
82 std::to_string(parent_val.Type()) + " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
83 }
84 const std::string parent_str = parent_val.as<std::string>();
85
86 const std::string id_sep = _AUTO_APMS_BEHAVIOR_TREE_CORE__RESOURCE_IDENTITY_ALIAS_SEP;
87 const std::string node_sep = _AUTO_APMS_BEHAVIOR_TREE_CORE__NODE_NAMESPACE_DEFAULT_SEP;
88
89 // Find the first '.' that follows the '::' separator to split manifest identity from node name
90 const std::size_t id_sep_pos = parent_str.find(id_sep);
91 const std::size_t dot_pos =
92 parent_str.find(node_sep, id_sep_pos != std::string::npos ? id_sep_pos + id_sep.size() : 0);
93
94 if (dot_pos == std::string::npos) {
95 throw exceptions::NodeManifestError(
96 "Invalid parent '" + parent_str + "'. Expected format: '<package_name>" + id_sep + "<manifest_alias>" +
97 node_sep + "<node_name>'.");
98 }
99
100 const std::string manifest_identity_str = parent_str.substr(0, dot_pos);
101 const std::string parent_node_name = parent_str.substr(dot_pos + node_sep.size());
102
103 if (parent_node_name.empty()) {
104 throw exceptions::NodeManifestError("Invalid parent '" + parent_str + "'. Parent node name must not be empty.");
105 }
106
107 try {
108 const NodeManifest parent_manifest =
109 NodeManifest::fromResource(NodeManifestResourceIdentity(manifest_identity_str));
110 if (!parent_manifest.contains(parent_node_name)) {
111 throw exceptions::NodeManifestError(
112 "Parent node '" + parent_node_name + "' does not exist in manifest '" + manifest_identity_str + "'.");
113 }
114 rhs = parent_manifest[parent_node_name];
115 } catch (const exceptions::NodeManifestError &) {
116 throw;
117 } catch (const std::exception & e) {
118 throw exceptions::NodeManifestError(
119 "Failed to load parent manifest '" + manifest_identity_str + "': " + e.what());
120 }
121 }
122
123 // Apply all non-parent fields from the YAML node onto rhs.
124 // For the parent case these act as overrides; for the no-parent case they are the full definition.
125 for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) {
126 const std::string key = it->first.as<std::string>();
127 const Node val = it->second;
128
129 if (key == Options::PARAM_NAME_PARENT) continue; // already handled above
130
131 if (key == Options::PARAM_NAME_EXTRA) {
132 rhs.extra = val;
133 continue;
134 }
135
136 if (key == Options::PARAM_NAME_PORT_ALIAS) {
137 if (!val.IsMap()) {
138 throw auto_apms_util::exceptions::YAMLFormatError(
139 "Value for key '" + key + "' must be a map but is type " + std::to_string(val.Type()) +
140 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
141 }
142 for (const auto & [k, v] : val.as<std::map<std::string, std::string>>()) rhs.port_alias[k] = v;
143 continue;
144 }
145
146 if (key == Options::PARAM_NAME_PORT_DEFAULT) {
147 if (!val.IsMap()) {
148 throw auto_apms_util::exceptions::YAMLFormatError(
149 "Value for key '" + key + "' must be a map but is type " + std::to_string(val.Type()) +
150 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
151 }
152 for (const auto & [k, v] : val.as<std::map<std::string, std::string>>()) rhs.port_default[k] = v;
153 continue;
154 }
155
156 if (key == Options::PARAM_NAME_HIDDEN_PORTS) {
157 if (!val.IsSequence()) {
158 throw auto_apms_util::exceptions::YAMLFormatError(
159 "Value for key '" + key + "' must be a sequence but is type " + std::to_string(val.Type()) +
160 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
161 }
162 for (const auto & entry : val.as<std::vector<std::string>>()) {
163 if (std::find(rhs.hidden_ports.begin(), rhs.hidden_ports.end(), entry) == rhs.hidden_ports.end())
164 rhs.hidden_ports.push_back(entry);
165 }
166 continue;
167 }
168
169 if (key == Options::PARAM_NAME_CLASS) {
170 if (!val.IsScalar()) {
171 throw auto_apms_util::exceptions::YAMLFormatError(
172 "Value for key '" + key + "' must be scalar but is type " + std::to_string(val.Type()) +
173 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
174 }
175 rhs.class_name = val.as<std::string>();
176 continue;
177 }
178 if (key == Options::PARAM_NAME_DESCRIPTION) {
179 if (!val.IsScalar()) {
180 throw auto_apms_util::exceptions::YAMLFormatError(
181 "Value for key '" + key + "' must be scalar but is type " + std::to_string(val.Type()) +
182 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
183 }
184 rhs.description = val.as<std::string>();
185 continue;
186 }
187 if (key == Options::PARAM_NAME_ROS2TOPIC) {
188 if (!val.IsScalar()) {
189 throw auto_apms_util::exceptions::YAMLFormatError(
190 "Value for key '" + key + "' must be scalar but is type " + std::to_string(val.Type()) +
191 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
192 }
193 rhs.topic = val.as<std::string>();
194 continue;
195 }
196 if (key == Options::PARAM_NAME_WAIT_TIMEOUT) {
197 if (!val.IsScalar()) {
198 throw auto_apms_util::exceptions::YAMLFormatError(
199 "Value for key '" + key + "' must be scalar but is type " + std::to_string(val.Type()) +
200 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
201 }
202 rhs.wait_timeout = std::chrono::duration<double>(val.as<double>());
203 continue;
204 }
205 if (key == Options::PARAM_NAME_REQUEST_TIMEOUT) {
206 if (!val.IsScalar()) {
207 throw auto_apms_util::exceptions::YAMLFormatError(
208 "Value for key '" + key + "' must be scalar but is type " + std::to_string(val.Type()) +
209 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
210 }
211 rhs.request_timeout = std::chrono::duration<double>(val.as<double>());
212 continue;
213 }
214 if (key == Options::PARAM_NAME_ALLOW_UNREACHABLE) {
215 if (!val.IsScalar()) {
216 throw auto_apms_util::exceptions::YAMLFormatError(
217 "Value for key '" + key + "' must be scalar but is type " + std::to_string(val.Type()) +
218 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
219 }
220 rhs.allow_unreachable = val.as<bool>();
221 continue;
222 }
223 if (key == Options::PARAM_NAME_LOGGER_LEVEL) {
224 if (!val.IsScalar()) {
225 throw auto_apms_util::exceptions::YAMLFormatError(
226 "Value for key '" + key + "' must be scalar but is type " + std::to_string(val.Type()) +
227 " (0: Undefined - 1: Null - 2: Scalar - 3: Sequence - 4: Map).");
228 }
229 rhs.logger_level = val.as<std::string>();
230 continue;
231 }
232
233 // Unknown parameter
234 throw auto_apms_util::exceptions::YAMLFormatError("Unknown parameter name '" + key + "'.");
235 }
236 return true;
237}
238
239} // namespace YAML
bool contains(const std::string &node_name) const
Determine if a behavior tree node has been added to the manifest.
Core API for AutoAPMS's behavior tree implementation.
Definition behavior.hpp:32
Powerful tooling for incorporating behavior trees for task development.
Definition behavior.hpp:32
Parameters for loading and registering a behavior tree node class from a shared library using e....
std::string description
Short description of the behavior tree node's purpose and use-case.
bool valid() const
Verify that the options are valid (e.g. all required values are set).
NodeRegistrationOptions()=default
Create the default node registration options.
std::string topic
Name of the ROS 2 communication interface to connect with.
std::chrono::duration< double > request_timeout
Period [s] (measured from sending a request) after the node aborts waiting for a server response.
bool operator!=(const NodeRegistrationOptions &other) const
Determine whether two registration options are not equivalent.
std::map< std::string, std::string > port_default
Provides the possibility to define custom default values for the ports implemented by class_name.
std::vector< std::string > hidden_ports
List of port names to hide in the node model for visualization tools like Groot2.
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.
std::string logger_level
Minimum ROS 2 logging severity level for this particular node. Empty means to inherit the parent logg...
bool operator==(const NodeRegistrationOptions &other) const
Determine whether two registration options are equivalent.