AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
ros_subscriber_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 <algorithm>
18#include <functional>
19#include <memory>
20#include <mutex>
21#include <string>
22#include <unordered_map>
23#include <vector>
24
25#include "auto_apms_behavior_tree_core/exceptions.hpp"
26#include "auto_apms_behavior_tree_core/node/base/ros_condition_node.hpp"
27#include "auto_apms_util/logging.hpp"
28#include "rclcpp/executors.hpp"
29#include "rclcpp/qos.hpp"
30
32{
33
62template <class MessageT>
63class RosSubscriberNode : public RosConditionNode
64{
65 using Subscriber = typename rclcpp::Subscription<MessageT>;
66
67 struct SubscriberInstance
68 {
69 SubscriberInstance(
70 rclcpp::Node::SharedPtr node, rclcpp::CallbackGroup::SharedPtr group, const std::string & topic_name,
71 const rclcpp::QoS & qos);
72
73 std::shared_ptr<Subscriber> subscriber;
74 std::vector<std::pair<const void *, std::function<void(const std::shared_ptr<MessageT>)>>> callbacks;
75 std::shared_ptr<MessageT> last_msg;
76 std::string name;
77
78 void addCallback(
79 const void * callback_owner, const std::function<void(const std::shared_ptr<MessageT>)> & callback);
80 void removeCallback(const void * callback_owner);
81 void broadcast(const std::shared_ptr<MessageT> & msg);
82 };
83
84public:
85 using MessageType = MessageT;
86 using Config = BT::NodeConfig;
87 using Context = RosNodeContext;
88
100 const std::string & instance_name, const Config & config, Context context, const rclcpp::QoS & qos = {10});
101
102 virtual ~RosSubscriberNode()
103 {
104 if (sub_instance_) {
105 sub_instance_->removeCallback(this);
106 }
107 }
108
116 static BT::PortsList providedBasicPorts(BT::PortsList addition)
117 {
118 BT::PortsList basic = {BT::InputPort<std::string>("topic", "Name of the ROS 2 topic to subscribe to.")};
119 basic.insert(addition.begin(), addition.end());
120 return basic;
121 }
122
127 static BT::PortsList providedPorts() { return providedBasicPorts({}); }
128
139 virtual BT::NodeStatus onTick(const std::shared_ptr<MessageT> & last_msg_ptr);
140
150 virtual BT::NodeStatus onMessageReceived(const MessageT & msg);
151
157 bool createSubscriber(const std::string & topic_name);
158
163 std::string getTopicName() const;
164
165protected:
166 BT::NodeStatus tick() override final;
167
168private:
169 const rclcpp::QoS qos_;
170 std::string topic_name_;
171 bool dynamic_client_instance_ = false;
172 std::shared_ptr<SubscriberInstance> sub_instance_;
173 std::shared_ptr<MessageT> last_msg_;
174};
175
176// #####################################################################################################################
177// ################################ DEFINITIONS ##############################################
178// #####################################################################################################################
179
180template <class MessageT>
181inline RosSubscriberNode<MessageT>::SubscriberInstance::SubscriberInstance(
182 rclcpp::Node::SharedPtr node, rclcpp::CallbackGroup::SharedPtr group, const std::string & topic_name,
183 const rclcpp::QoS & qos)
184{
185 rclcpp::SubscriptionOptions option;
186 option.callback_group = group;
187
188 // The callback will broadcast to all the instances of RosSubscriberNode<MessageT>
189 auto callback = [this](const std::shared_ptr<MessageT> msg) {
190 this->last_msg = msg;
191 this->broadcast(msg);
192 };
193 subscriber = node->create_subscription<MessageT>(topic_name, qos, callback, option);
194 name = topic_name;
195}
196
197template <class MessageT>
198inline void RosSubscriberNode<MessageT>::SubscriberInstance::addCallback(
199 const void * callback_owner, const std::function<void(const std::shared_ptr<MessageT>)> & callback)
200{
201 callbacks.emplace_back(callback_owner, callback);
202}
203
204template <class MessageT>
205inline void RosSubscriberNode<MessageT>::SubscriberInstance::removeCallback(const void * callback_owner)
206{
207 callbacks.erase(
208 std::remove_if(
209 callbacks.begin(), callbacks.end(), [callback_owner](const auto & pair) { return pair.first == callback_owner; }),
210 callbacks.end());
211}
212
213template <class MessageT>
214inline void RosSubscriberNode<MessageT>::SubscriberInstance::broadcast(const std::shared_ptr<MessageT> & msg)
215{
216 for (auto & callback_pair : callbacks) {
217 callback_pair.second(msg);
218 }
219}
220
221template <class MessageT>
223 const std::string & instance_name, const Config & config, Context context, const rclcpp::QoS & qos)
224: RosConditionNode{instance_name, config, context}, qos_{qos}
225{
226 // Port aliasing is already applied by RosConditionNode ctor.
227
228 if (const BT::Expected<std::string> expected_name = context_.getTopicName(this)) {
229 createSubscriber(expected_name.value());
230 } else {
231 // We assume that determining the topic name requires a blackboard pointer, which cannot be evaluated at
232 // construction time. The expression will be evaluated each time before the node is ticked the first time after
233 // successful execution.
234 dynamic_client_instance_ = true;
235 }
236}
237
238template <class MessageT>
239inline bool RosSubscriberNode<MessageT>::createSubscriber(const std::string & topic_name)
240{
241 if (topic_name.empty()) {
242 throw exceptions::RosNodeError(
243 context_.getFullyQualifiedTreeNodeName(this) + " - Argument topic_name is empty when trying to create a client.");
244 }
245
246 // Check if the subscriber with given name is already set up
247 if (sub_instance_ && topic_name == sub_instance_->name) return true;
248
249 rclcpp::Node::SharedPtr node = context_.getRosNode();
250 rclcpp::CallbackGroup::SharedPtr group = context_.getWaitablesCallbackGroup();
251 if (!group) {
252 throw exceptions::RosNodeError(
253 context_.getFullyQualifiedTreeNodeName(this) +
254 " - The weak pointer to the ROS 2 callback group expired. The tree node doesn't "
255 "take ownership of it.");
256 }
257
258 // Reuse a subscriber shared across tree nodes with the same topic name, or create one on first use. The shared
259 // instance broadcasts every received message to all the nodes that registered a callback below.
260 sub_instance_ = this->template getSharedEntity<SubscriberInstance>(
261 topic_name, [&] { return std::make_shared<SubscriberInstance>(node, group, topic_name, qos_); });
262
263 // Check if there was a message received before the creation of this subscriber action
264 if (sub_instance_->last_msg) {
265 last_msg_ = sub_instance_->last_msg;
266 }
267
268 // add "this" as received of the broadcaster
269 sub_instance_->addCallback(this, [this](const std::shared_ptr<MessageT> msg) { last_msg_ = msg; });
270
271 return true;
272}
273
274template <class MessageT>
275inline BT::NodeStatus RosSubscriberNode<MessageT>::tick()
276{
277 if (!rclcpp::ok()) {
278 halt();
279 return BT::NodeStatus::FAILURE;
280 }
281
282 // If client has been set up in derived constructor, event though this constructor couldn't, we discard the intention
283 // of dynamically creating the client
284 if (dynamic_client_instance_ && sub_instance_) {
285 dynamic_client_instance_ = false;
286 }
287
288 // Try again to create the client on first tick if this was not possible during construction or if client should be
289 // created from a blackboard entry on the start of every iteration
290 if (status() == BT::NodeStatus::IDLE && dynamic_client_instance_) {
291 const BT::Expected<std::string> expected_name = context_.getTopicName(this);
292 if (expected_name) {
293 createSubscriber(expected_name.value());
294 } else {
295 throw exceptions::RosNodeError(
296 context_.getFullyQualifiedTreeNodeName(this) +
297 " - Cannot create the subscriber because the topic name couldn't be resolved using "
298 "the expression specified by the node's registration parameters (" +
299 NodeRegistrationOptions::PARAM_NAME_ROS2TOPIC + ": " + context_.getRegistrationOptions().topic +
300 "). Error message: " + expected_name.error());
301 }
302 }
303
304 if (!sub_instance_) {
305 throw exceptions::RosNodeError(context_.getFullyQualifiedTreeNodeName(this) + " - sub_instance_ is nullptr.");
306 }
307
308 auto check_status = [this](BT::NodeStatus status) {
309 if (!isStatusCompleted(status)) {
310 throw exceptions::RosNodeError(
311 context_.getFullyQualifiedTreeNodeName(this) + " - The callback must return either SUCCESS or FAILURE.");
312 }
313 return status;
314 };
315 auto status = check_status(onTick(last_msg_));
316 last_msg_.reset();
317 return status;
318}
319
320template <class MessageT>
321inline BT::NodeStatus RosSubscriberNode<MessageT>::onTick(const std::shared_ptr<MessageT> & last_msg_ptr)
322{
323 if (!last_msg_ptr) return BT::NodeStatus::FAILURE;
324 return onMessageReceived(*last_msg_ptr);
325}
326
327template <class MessageT>
328inline BT::NodeStatus RosSubscriberNode<MessageT>::onMessageReceived(const MessageT & /*msg*/)
329{
330 return BT::NodeStatus::SUCCESS;
331}
332
333template <class MessageT>
335{
336 if (sub_instance_) return sub_instance_->name;
337 return "unknown";
338}
339
340} // 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.
rclcpp::Node::SharedPtr getRosNode() const
Get the associated ROS 2 node handle.
Generic behavior tree node wrapper for a ROS 2 subscriber.
RosSubscriberNode(const std::string &instance_name, const Config &config, Context context, const rclcpp::QoS &qos={10})
Constructor.
std::string getTopicName() const
Get the name of the topic name this node subscribes to.
virtual BT::NodeStatus onMessageReceived(const MessageT &msg)
Callback invoked when the node is ticked and a valid message has been received.
static BT::PortsList providedBasicPorts(BT::PortsList addition)
Derived nodes implementing the static method RosSubscriberNode::providedPorts may call this method to...
bool createSubscriber(const std::string &topic_name)
Create the ROS 2 subscriber.
static BT::PortsList providedPorts()
If a behavior tree requires input/output data ports, the developer must define this method accordingl...
virtual BT::NodeStatus onTick(const std::shared_ptr< MessageT > &last_msg_ptr)
Callback invoked when the node is ticked.
Core API for AutoAPMS's behavior tree implementation.
Definition behavior.hpp:32