ROS2 发布者和订阅者中的回调函数
简介
在ROS2发布者和订阅者的简单教程中,函数指针在回调函数中扮演重要角色:
发布者
:
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
//...
};
订阅者
:
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
//...
};
本文旨在使读者熟悉以下用法:
std::bind(&MinimalSubscriber::topic_callback, this, _1))
并掌握ROS2中最基本的部分:发布者和订阅者。
C++11函数指针和std::bind
的基础知识
函数指针
在C语言和古老的C++中,函数指针不是特定类型。在C++11之后,函数指针被封装在特定类型std::function
中(参见cpp参考)。
一个简单的例子(省略相关头文件):
std::function<double(const std::vector<double>&)> fn;
double sum(const std::vector<double>& data) {
return std::accumulate(data.begin(), data.end(), 0.);
}
fn = sum;
std::vector<double> data = {0.5, 1.0, 2.3};
auto res1 = fn(data);
std::bind
std::bind
是一种将函数“绑定”到对象的方法。这有点难以理解。但是,std::bind
最简单的用法是创建“部分函数”。我们从这里开始:
假设我们有一个函数:
f(a,b,c);
我们想要一个函数:
g := f(a, 4, b)
可以使用std::bind
:
auto g = bind(f, _1, 4, _2);
在这种用法中,没有“对象”需要绑定。std::bind
的唯一目的是传递一个参数。
std::bind
有三个参数(参见参考):被绑定的函数, 被绑定到的对象,以及将传递给被绑定函数的参数。通常,std::bind
将成员函数绑定到类对象以实现回调。实际上它本身就是一个函数指针。
ROS2中的回调函数实现
在订阅创建中:
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
这里的_1
是std::placeholders::_1
,用于传递MinimalSubscriber::topic_callback
的参数。
如果你现在理解了std::bind
的用法,你可能 会问:为什么我们不直接将std::function
用作this->create_subscription
的最后一个参数呢?在这种情况下,如果你写:
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, &MinimalSubscriber::topic_callback)
你会遇到编译错误,因为成员函数需要一个隐式的this
指针,指向它们被调用的对象。这是std::bind
灵活性的一个很好的例子。
然而,没有人说回调函数必须是成员函数,如果你将一个自由函数作为回调函数,直接使用函数指针是可能的。
此外,参数数量为1,即消息本身。但是,你可能会遇到想要传递更多参数的情况。std::bind
可以轻松实现这一点:
auto callback = std::bind(callback_func, std::placeholders::_1, topic_name);
上面是一个将主题名作为参数传递的例子。
应用示例
Tinker底盘硬件使用CAN总线作为通信介质,需要一个CAN适配器接口。在CAN适配器实现中,总是需要一个接收
回调函数。但是,回调依赖于特定的电机类型。因此,我们不能在CanAdapter
类中实现回调。这里有一个技巧来解耦我们的代码,使用函数指针:
在类声明中,我们为构造函 数留下一个函数指针参数。
class CanBusNode : public rclcpp::Node
{
public:
using FrameCallback = std::function<void(const can_msgs::msg::Frame::SharedPtr)>;
CanBusNode(const std::string & node_name, FrameCallback callback);
private:
rclcpp::Publisher<can_msgs::msg::Frame>::SharedPtr to_can_bus_publisher_;
rclcpp::Subscription<can_msgs::msg::Frame>::SharedPtr from_can_bus_subscriber_;
FrameCallback frame_callback_;
};
在构造中,我们使用此函数指针作为CAN订阅器的回调函数。这里的回调函数不是它的成员函数,尽管仍然需要std::bind
。
CanBusNode::CanBusNode(const std::string & node_name, FrameCallback callback)
: Node(node_name), frame
_callback_(callback)
{
// 初始化发布者
to_can_bus_publisher_ = this->create_publisher<can_msgs::msg::Frame>("to_can_bus", 10);
// 使用提供的回调函数初始化订阅器
from_can_bus_subscriber_ = this->create_subscription<can_msgs::msg::Frame>(
"from_can_bus", 500, std::bind(callback, std::placeholders::_1));
}
更高级的实现回调函数方式
虽然ROS2文档使用std::bind
创建回调,但现在已经有些过时了。时尚的方法是使用lambda表达式:
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, [this] (const std_msgs::msg::String msg){this->callback(msg)}););
这是更优雅的方法。
有关lambda表达式的更多信息,请参见cpp参考。
在ROS2中使用lambda表达式,请参阅此帖子。
在Python中实现回调函数
ROS2文档提供了实现回调函数的示例:文档。要传递多个参数,我们可以在python中使用lambda函数:
node.create_subscription(std_msgs.msg.String, "my_topic", lambda msg: common_callback(msg, other_args), 10)
有关python lambda的基础知识,请参见参考。