AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
ros_publisher_node.hpp
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#pragma once
16
17#include <memory>
18#include <string>
19
20#include "auto_apms_behavior_tree_core/exceptions.hpp"
21#include "auto_apms_behavior_tree_core/node/ros_node_context.hpp"
22#include "auto_apms_util/string.hpp"
23#include "behaviortree_cpp/condition_node.h"
24#include "rclcpp/qos.hpp"
25
27{
28
62template <class MessageT>
63class RosPublisherNode : public BT::ConditionNode
64{
65 using Publisher = typename rclcpp::Publisher<MessageT>;
66
67public:
68 using MessageType = MessageT;
69 using Config = BT::NodeConfig;
70 using Context = RosNodeContext;
71
83 const std::string & instance_name, const Config & config, Context context, const rclcpp::QoS & qos = {10});
84
85 virtual ~RosPublisherNode() = default;
86
94 static BT::PortsList providedBasicPorts(BT::PortsList addition)
95 {
96 BT::PortsList basic = {BT::InputPort<std::string>("topic", "Name of the ROS 2 topic to publish to.")};
97 basic.insert(addition.begin(), addition.end());
98 return basic;
99 }
100
105 static BT::PortsList providedPorts() { return providedBasicPorts({}); }
106
117 virtual bool setMessage(MessageT & msg);
118
124 bool createPublisher(const std::string & topic_name);
125
130 std::string getTopicName() const;
131
132protected:
133 const Context context_;
134 const rclcpp::Logger logger_;
135
136 BT::NodeStatus tick() override final;
137
138private:
139 const rclcpp::QoS qos_;
140 std::string topic_name_;
141 bool dynamic_client_instance_ = false;
142 std::shared_ptr<Publisher> publisher_;
143};
144
145// #####################################################################################################################
146// ################################ DEFINITIONS ##############################################
147// #####################################################################################################################
148
149template <class MessageT>
151 const std::string & instance_name, const Config & config, Context context, const rclcpp::QoS & qos)
152: BT::ConditionNode(instance_name, config),
153 context_(context),
154 logger_(context.getChildLogger(auto_apms_util::toSnakeCase(instance_name))),
155 qos_{qos}
156{
157 // Consider aliasing in ports and copy the values from aliased to original ports
158 this->modifyPortsRemapping(context_.copyAliasedPortValuesToOriginalPorts(this));
159
160 if (const BT::Expected<std::string> expected_name = context_.getTopicName(this)) {
161 createPublisher(expected_name.value());
162 } else {
163 // We assume that determining the topic name requires a blackboard pointer, which cannot be evaluated at
164 // construction time. The expression will be evaluated each time before the node is ticked the first time after
165 // successful execution.
166 dynamic_client_instance_ = true;
167 }
168}
169
170template <class MessageT>
171inline bool RosPublisherNode<MessageT>::createPublisher(const std::string & topic_name)
172{
173 if (topic_name.empty()) {
174 throw exceptions::RosNodeError(
175 context_.getFullyQualifiedTreeNodeName(this) + " - Argument topic_name is empty when trying to create a client.");
176 }
177
178 // Check if the publisher with given name is already set up
179 if (publisher_ && topic_name_ == topic_name) return true;
180
181 rclcpp::Node::SharedPtr node = context_.nh_.lock();
182 if (!node) {
183 throw exceptions::RosNodeError(
184 context_.getFullyQualifiedTreeNodeName(this) +
185 " - The weak pointer to the ROS 2 node expired. The tree node doesn't "
186 "take ownership of it.");
187 }
188
189 publisher_ = node->template create_publisher<MessageT>(topic_name, qos_);
190 topic_name_ = topic_name;
191 RCLCPP_DEBUG(
192 logger_, "%s - Created publisher for topic '%s'.", context_.getFullyQualifiedTreeNodeName(this).c_str(),
193 topic_name.c_str());
194
195 // Wait for at least one subscriber to be connected
196 RCLCPP_DEBUG(
197 logger_, "%s - Waiting for at least one subscriber to connect to topic '%s'...",
198 context_.getFullyQualifiedTreeNodeName(this).c_str(), topic_name_.c_str());
199
200 const auto start_time = context_.getCurrentTime();
201 while (rclcpp::ok() && publisher_->get_subscription_count() == 0) {
202 if ((context_.getCurrentTime() - start_time) > context_.registration_options_.wait_timeout) {
203 RCLCPP_DEBUG(
204 logger_, "%s - Timeout waiting for subscriber to connect to topic '%s' after %.2f seconds.",
205 context_.getFullyQualifiedTreeNodeName(this).c_str(), topic_name_.c_str(),
206 context_.registration_options_.wait_timeout.count());
207 break;
208 }
209 rclcpp::sleep_for(std::chrono::milliseconds(5));
210 }
211
212 if (publisher_->get_subscription_count() > 0) {
213 RCLCPP_DEBUG(
214 logger_, "%s - At least one subscriber found.", context_.getFullyQualifiedTreeNodeName(this).c_str(),
215 topic_name_.c_str());
216 } else if (context_.registration_options_.allow_unreachable) {
217 RCLCPP_WARN(
218 logger_, "%s - No subscriber connected to topic '%s', but continuing as allow_unreachable is true.",
219 context_.getFullyQualifiedTreeNodeName(this).c_str(), topic_name_.c_str());
220 } else {
221 throw exceptions::RosNodeError(
222 context_.getFullyQualifiedTreeNodeName(this) + " - No subscriber connected to topic '" + topic_name_ +
223 "' after waiting for " + std::to_string(context_.registration_options_.wait_timeout.count()) + " seconds.");
224 }
225 return true;
226}
227
228template <class MessageT>
229inline BT::NodeStatus RosPublisherNode<MessageT>::tick()
230{
231 if (!rclcpp::ok()) {
232 halt();
233 return BT::NodeStatus::FAILURE;
234 }
235
236 // If client has been set up in derived constructor, event though this constructor couldn't, we discard the intention
237 // of dynamically creating the client
238 if (dynamic_client_instance_ && publisher_) {
239 dynamic_client_instance_ = false;
240 }
241
242 // Try again to create the client on first tick if this was not possible during construction or if client should be
243 // created from a blackboard entry on the start of every iteration
244 if (status() == BT::NodeStatus::IDLE && dynamic_client_instance_) {
245 const BT::Expected<std::string> expected_name = context_.getTopicName(this);
246 if (expected_name) {
247 createPublisher(expected_name.value());
248 } else {
249 throw exceptions::RosNodeError(
250 context_.getFullyQualifiedTreeNodeName(this) +
251 " - Cannot create the publisher because the topic name couldn't be resolved using "
252 "the expression specified in the node's registration options (" +
253 NodeRegistrationOptions::PARAM_NAME_ROS2TOPIC + ": " + context_.registration_options_.topic +
254 "). Error message: " + expected_name.error());
255 }
256 }
257
258 if (!publisher_) {
259 throw exceptions::RosNodeError(context_.getFullyQualifiedTreeNodeName(this) + " - publisher_ is nullptr.");
260 }
261
262 MessageT msg;
263 if (!setMessage(msg)) {
264 return BT::NodeStatus::FAILURE;
265 }
266 publisher_->publish(msg);
267 return BT::NodeStatus::SUCCESS;
268}
269
270template <class MessageT>
271inline bool RosPublisherNode<MessageT>::setMessage(MessageT & /*msg*/)
272{
273 return true;
274}
275
276template <class MessageT>
278{
279 return topic_name_;
280}
281
282} // namespace auto_apms_behavior_tree::core
Additional parameters specific to ROS 2 determined at runtime by TreeBuilder.
Generic behavior tree node wrapper for a ROS 2 publisher.
std::string getTopicName() const
Get the name of the topic name this node publishes to.
virtual bool setMessage(MessageT &msg)
Callback invoked when ticked to define the message to be published.
static BT::PortsList providedBasicPorts(BT::PortsList addition)
Derived nodes implementing the static method RosPublisherNode::providedPorts may call this method to ...
RosPublisherNode(const std::string &instance_name, const Config &config, Context context, const rclcpp::QoS &qos={10})
Constructor.
static BT::PortsList providedPorts()
If a behavior tree requires input/output data ports, the developer must define this method accordingl...
bool createPublisher(const std::string &topic_name)
Create the ROS 2 publisher.
Core API for AutoAPMS's behavior tree implementation.
Definition behavior.hpp:32
Fundamental helper classes and utility functions.