AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
composition_nodes.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 <sstream>
16#include <string>
17
18#include "auto_apms_behavior_tree_core/node.hpp"
19#include "composition_interfaces/srv/load_node.hpp"
20#include "composition_interfaces/srv/unload_node.hpp"
21#include "rcutils/error_handling.h"
22#include "rcutils/logging.h"
23
24#define INPUT_KEY_CONTAINER "container"
25#define INPUT_KEY_PACKAGE "package"
26#define INPUT_KEY_PLUGIN "plugin"
27#define INPUT_KEY_NODE_NAME "node_name"
28#define INPUT_KEY_NODE_NAMESPACE "node_namespace"
29#define INPUT_KEY_LOG_LEVEL "log_level"
30#define INPUT_KEY_REMAP_RULES "remap_rules"
31#define PORT_KEY_UNIQUE_ID "unique_id"
32#define OUTPUT_KEY_FULL_NODE_NAME "full_node_name"
33
35{
36
48class CompositionLoadNode : public core::RosServiceNode<composition_interfaces::srv::LoadNode>
49{
50public:
51 using RosServiceNode::RosServiceNode;
52
53 static BT::PortsList providedPorts()
54 {
55 // We do not use the default 'topic' port — the service name is derived from the 'container' port
56 // via the NodeRegistrationOptions::topic pattern "(input:container)/_container/load_node".
57 return {
58 BT::InputPort<std::string>(INPUT_KEY_CONTAINER, "Name of the component container to load the node into."),
59 BT::InputPort<std::string>(INPUT_KEY_PACKAGE, "Name of the ROS 2 package containing the composable node plugin."),
60 BT::InputPort<std::string>(INPUT_KEY_PLUGIN, "Name of the composable node plugin to load."),
61 BT::InputPort<std::string>(
62 INPUT_KEY_NODE_NAME, "", "Assigned name of the loaded node. Leave empty to use the plugin's default name."),
63 BT::InputPort<std::string>(
64 INPUT_KEY_NODE_NAMESPACE, "",
65 "Assigned namespace of the loaded node. Leave empty to use the plugin's default namespace."),
66 BT::InputPort<std::string>(
67 INPUT_KEY_LOG_LEVEL, "UNSET",
68 "Log level for the loaded node. Must be one of [UNSET, DEBUG, INFO, WARN, ERROR, FATAL] "
69 "(case-insensitive). UNSET (default) means the container's log level is inherited."),
70 BT::InputPort<std::string>(
71 INPUT_KEY_REMAP_RULES, "",
72 "Semicolon-separated list of static remapping rules applied to the loaded node "
73 "(e.g. 'old_topic:=new_topic;foo:=bar')."),
74 BT::OutputPort<uint64_t>(
75 PORT_KEY_UNIQUE_ID, "Container-local unique ID assigned to the loaded node (0 if loading failed)."),
76 BT::OutputPort<std::string>(
77 OUTPUT_KEY_FULL_NODE_NAME, "Full node name (namespace/name) of the loaded node (empty if loading failed)."),
78 };
79 }
80
81 bool setRequest(Request::SharedPtr & request) override final
82 {
83 const BT::Expected<std::string> expected_package = getInput<std::string>(INPUT_KEY_PACKAGE);
84 if (!expected_package || expected_package.value().empty()) {
85 RCLCPP_ERROR(
86 logger_, "%s - Package name must not be empty.", context_.getFullyQualifiedTreeNodeName(this).c_str());
87 RCLCPP_DEBUG_EXPRESSION(
88 logger_, !expected_package, "%s - Error message: %s", context_.getFullyQualifiedTreeNodeName(this).c_str(),
89 expected_package.error().c_str());
90 return false;
91 }
92 request->package_name = expected_package.value();
93
94 const BT::Expected<std::string> expected_plugin = getInput<std::string>(INPUT_KEY_PLUGIN);
95 if (!expected_plugin || expected_plugin.value().empty()) {
96 RCLCPP_ERROR(
97 logger_, "%s - Plugin name must not be empty.", context_.getFullyQualifiedTreeNodeName(this).c_str());
98 RCLCPP_DEBUG_EXPRESSION(
99 logger_, !expected_plugin, "%s - Error message: %s", context_.getFullyQualifiedTreeNodeName(this).c_str(),
100 expected_plugin.error().c_str());
101 return false;
102 }
103 request->plugin_name = expected_plugin.value();
104 requested_plugin_name_ = expected_plugin.value();
105
106 request->node_name = getInput<std::string>(INPUT_KEY_NODE_NAME).value_or("");
107 request->node_namespace = getInput<std::string>(INPUT_KEY_NODE_NAMESPACE).value_or("");
108
109 // Initialize output ports with defaults so downstream nodes always read valid values
110 setOutput(PORT_KEY_UNIQUE_ID, uint64_t{0});
111 setOutput(OUTPUT_KEY_FULL_NODE_NAME, std::string{});
112
113 // Interpret log level string the same way as the Logger node
114 const std::string level_str = getInput<std::string>(INPUT_KEY_LOG_LEVEL).value_or("UNSET");
115 int log_level = 0;
116 if (
117 rcutils_logging_severity_level_from_string(level_str.c_str(), rcutils_get_default_allocator(), &log_level) !=
118 RCUTILS_RET_OK) {
119 const std::string error = rcutils_get_error_string().str;
120 rcutils_reset_error();
121 RCLCPP_ERROR(
122 logger_, "%s - Cannot convert log level '%s' to a valid severity: %s",
123 context_.getFullyQualifiedTreeNodeName(this).c_str(), level_str.c_str(), error.c_str());
124 return false;
125 }
126 request->log_level = static_cast<uint8_t>(log_level);
127
128 // Parse semicolon-separated remap rules and trim surrounding whitespace from each rule
129 const std::string remap_str = getInput<std::string>(INPUT_KEY_REMAP_RULES).value_or("");
130 if (!remap_str.empty()) {
131 std::istringstream ss(remap_str);
132 std::string rule;
133 while (std::getline(ss, rule, ';')) {
134 const auto first = rule.find_first_not_of(" \t");
135 const auto last = rule.find_last_not_of(" \t");
136 if (first != std::string::npos) {
137 request->remap_rules.push_back(rule.substr(first, last - first + 1));
138 }
139 }
140 }
141
142 return true;
143 }
144
145 BT::NodeStatus onResponseReceived(const Response::SharedPtr & response) override final
146 {
147 if (!response->success) {
148 RCLCPP_ERROR(
149 logger_, "%s - Failed to load composable node '%s' via service '%s': %s",
150 context_.getFullyQualifiedTreeNodeName(this).c_str(), requested_plugin_name_.c_str(), getServiceName().c_str(),
151 response->error_message.c_str());
152 return BT::NodeStatus::FAILURE;
153 }
154 setOutput(PORT_KEY_UNIQUE_ID, response->unique_id);
155 setOutput(OUTPUT_KEY_FULL_NODE_NAME, response->full_node_name);
156 return BT::NodeStatus::SUCCESS;
157 }
158
159private:
160 std::string requested_plugin_name_;
161};
162
170class CompositionUnloadNode : public core::RosServiceNode<composition_interfaces::srv::UnloadNode>
171{
172public:
173 using RosServiceNode::RosServiceNode;
174
175 static BT::PortsList providedPorts()
176 {
177 // We do not use the default 'topic' port — the service name is derived from the 'container' port
178 // via the NodeRegistrationOptions::topic pattern "(input:container)/_container/unload_node".
179 return {
180 BT::InputPort<std::string>(INPUT_KEY_CONTAINER, "Name of the component container to unload the node from."),
181 BT::InputPort<uint64_t>(PORT_KEY_UNIQUE_ID, "Container-local unique ID of the composable node to unload."),
182 };
183 }
184
185 bool setRequest(Request::SharedPtr & request) override final
186 {
187 const BT::Expected<uint64_t> expected_id = getInput<uint64_t>(PORT_KEY_UNIQUE_ID);
188 if (!expected_id) {
189 RCLCPP_ERROR(
190 logger_, "%s - %s", context_.getFullyQualifiedTreeNodeName(this).c_str(), expected_id.error().c_str());
191 return false;
192 }
193 request->unique_id = expected_id.value();
194 requested_unique_id_ = expected_id.value();
195 return true;
196 }
197
198 BT::NodeStatus onResponseReceived(const Response::SharedPtr & response) override final
199 {
200 if (!response->success) {
201 RCLCPP_ERROR(
202 logger_, "%s - Failed to unload composable node (id: %s) via service '%s': %s",
203 context_.getFullyQualifiedTreeNodeName(this).c_str(), std::to_string(requested_unique_id_).c_str(),
204 getServiceName().c_str(), response->error_message.c_str());
205 return BT::NodeStatus::FAILURE;
206 }
207 return BT::NodeStatus::SUCCESS;
208 }
209
210private:
211 uint64_t requested_unique_id_{0};
212};
213
214} // namespace auto_apms_behavior_tree
215
Loads a composable node plugin into a running ROS 2 component container.
Unloads a composable node from a running ROS 2 component container using its unique ID.
Generic behavior tree node wrapper for a ROS 2 service client.
#define AUTO_APMS_BEHAVIOR_TREE_REGISTER_NODE(type)
Macro for registering a behavior tree node plugin.
Definition node.hpp:46
Powerful tooling for incorporating behavior trees for task development.
Definition behavior.hpp:32