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// 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#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/base/ros_condition_node.hpp"
22#include "auto_apms_util/logging.hpp"
23#include "rclcpp/qos.hpp"
24
26{
27
61template <class MessageT>
62class RosPublisherNode : public RosConditionNode
63{
64 using Publisher = typename rclcpp::Publisher<MessageT>;
65
66public:
67 using MessageType = MessageT;
68 using Config = BT::NodeConfig;
69 using Context = RosNodeContext;
70
82 const std::string & instance_name, const Config & config, Context context, const rclcpp::QoS & qos = {10});
83
84 virtual ~RosPublisherNode() = default;
85
93 static BT::PortsList providedBasicPorts(BT::PortsList addition)
94 {
95 BT::PortsList basic = {BT::InputPort<std::string>("topic", "Name of the ROS 2 topic to publish to.")};
96 basic.insert(addition.begin(), addition.end());
97 return basic;
98 }
99
104 static BT::PortsList providedPorts() { return providedBasicPorts({}); }
105
116 virtual bool setMessage(MessageT & msg);
117
123 bool createPublisher(const std::string & topic_name);
124
129 std::string getTopicName() const;
130
131protected:
132 BT::NodeStatus tick() override final;
133
134private:
135 const rclcpp::QoS qos_;
136 std::string topic_name_;
137 bool dynamic_client_instance_ = false;
138 std::shared_ptr<Publisher> publisher_;
139};
140
141// #####################################################################################################################
142// ################################ DEFINITIONS ##############################################
143// #####################################################################################################################
144
145template <class MessageT>
147 const std::string & instance_name, const Config & config, Context context, const rclcpp::QoS & qos)
148: RosConditionNode(instance_name, config, context), qos_{qos}
149{
150 // Port aliasing is already applied by RosConditionNode ctor.
151
152 if (const BT::Expected<std::string> expected_name = context_.getTopicName(this)) {
153 createPublisher(expected_name.value());
154 } else {
155 // We assume that determining the topic name requires a blackboard pointer, which cannot be evaluated at
156 // construction time. The expression will be evaluated each time before the node is ticked the first time after
157 // successful execution.
158 dynamic_client_instance_ = true;
159 }
160}
161
162template <class MessageT>
163inline bool RosPublisherNode<MessageT>::createPublisher(const std::string & topic_name)
164{
165 if (topic_name.empty()) {
166 throw exceptions::RosNodeError(
167 context_.getFullyQualifiedTreeNodeName(this) + " - Argument topic_name is empty when trying to create a client.");
168 }
169
170 // Check if the publisher with given name is already set up
171 if (publisher_ && topic_name_ == topic_name) return true;
172
173 rclcpp::Node::SharedPtr node = context_.getRosNode();
174
175 // Reuse a publisher shared across tree nodes with the same topic name, or create one on first use.
176 publisher_ = this->template getSharedEntity<Publisher>(
177 topic_name, [&] { return node->template create_publisher<MessageT>(topic_name, qos_); });
178 topic_name_ = topic_name;
179 RCLCPP_DEBUG(
180 logger_, "%s - Created publisher for topic '%s'.", context_.getFullyQualifiedTreeNodeName(this).c_str(),
181 topic_name.c_str());
182
183 // Wait for at least one subscriber to be connected
184 RCLCPP_DEBUG(
185 logger_, "%s - Waiting for at least one subscriber to connect to topic '%s'...",
186 context_.getFullyQualifiedTreeNodeName(this).c_str(), topic_name_.c_str());
187
188 const auto start_time = context_.getCurrentTime();
189 while (rclcpp::ok() && publisher_->get_subscription_count() == 0) {
190 if ((context_.getCurrentTime() - start_time) > context_.getRegistrationOptions().wait_timeout) {
191 RCLCPP_DEBUG(
192 logger_, "%s - Timeout waiting for subscriber to connect to topic '%s' after %.2f seconds.",
193 context_.getFullyQualifiedTreeNodeName(this).c_str(), topic_name_.c_str(),
194 context_.getRegistrationOptions().wait_timeout.count());
195 break;
196 }
197 rclcpp::sleep_for(std::chrono::milliseconds(5));
198 }
199
200 if (publisher_->get_subscription_count() > 0) {
201 RCLCPP_DEBUG(
202 logger_, "%s - At least one subscriber found.", context_.getFullyQualifiedTreeNodeName(this).c_str(),
203 topic_name_.c_str());
204 } else if (context_.getRegistrationOptions().allow_unreachable) {
205 RCLCPP_WARN(
206 logger_, "%s - No subscriber connected to topic '%s', but continuing as allow_unreachable is true.",
207 context_.getFullyQualifiedTreeNodeName(this).c_str(), topic_name_.c_str());
208 } else {
209 throw exceptions::RosNodeError(
210 context_.getFullyQualifiedTreeNodeName(this) + " - No subscriber connected to topic '" + topic_name_ +
211 "' after waiting for " + std::to_string(context_.getRegistrationOptions().wait_timeout.count()) + " seconds.");
212 }
213 return true;
214}
215
216template <class MessageT>
217inline BT::NodeStatus RosPublisherNode<MessageT>::tick()
218{
219 if (!rclcpp::ok()) {
220 halt();
221 return BT::NodeStatus::FAILURE;
222 }
223
224 // If client has been set up in derived constructor, event though this constructor couldn't, we discard the intention
225 // of dynamically creating the client
226 if (dynamic_client_instance_ && publisher_) {
227 dynamic_client_instance_ = false;
228 }
229
230 // Try again to create the client on first tick if this was not possible during construction or if client should be
231 // created from a blackboard entry on the start of every iteration
232 if (status() == BT::NodeStatus::IDLE && dynamic_client_instance_) {
233 const BT::Expected<std::string> expected_name = context_.getTopicName(this);
234 if (expected_name) {
235 createPublisher(expected_name.value());
236 } else {
237 throw exceptions::RosNodeError(
238 context_.getFullyQualifiedTreeNodeName(this) +
239 " - Cannot create the publisher because the topic name couldn't be resolved using "
240 "the expression specified in the node's registration options (" +
241 NodeRegistrationOptions::PARAM_NAME_ROS2TOPIC + ": " + context_.getRegistrationOptions().topic +
242 "). Error message: " + expected_name.error());
243 }
244 }
245
246 if (!publisher_) {
247 throw exceptions::RosNodeError(context_.getFullyQualifiedTreeNodeName(this) + " - publisher_ is nullptr.");
248 }
249
250 MessageT msg;
251 if (!setMessage(msg)) {
252 return BT::NodeStatus::FAILURE;
253 }
254 publisher_->publish(msg);
255 return BT::NodeStatus::SUCCESS;
256}
257
258template <class MessageT>
259inline bool RosPublisherNode<MessageT>::setMessage(MessageT & /*msg*/)
260{
261 return true;
262}
263
264template <class MessageT>
266{
267 return topic_name_;
268}
269
270} // namespace auto_apms_behavior_tree::core
std::shared_ptr< InstanceT > getSharedEntity(const std::string &entity_name, const std::function< std::shared_ptr< InstanceT >()> &factory)
Retrieve a process-wide shared ROS 2 entity, creating it via factory on first use.
Additional parameters specific to ROS 2 determined at runtime by TreeBuilder.
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),...
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