4、话题通信实战:发布者与订阅者的C++实现、自定义消息类型
好,咱们今天来点真格的。
前面几章我们把ROS的概念、文件系统、命令行工具都过了一遍。说实话,那些都是开胃菜。真正让ROS跑起来、让机器人动起来的核心,就是话题通信。
我个人习惯把话题通信比作「广播电台」。发布者就是电台主持人,一直在那说话;订阅者就是听众,打开收音机就能听到。主持人不知道谁在听,听众也不知道主持人长啥样。但信息就这么传过去了。
这一章,我们就亲手搭建一个这样的「电台」。
4.1 发布者(Publisher)的C++实现
先写发布者。说白了,就是往某个话题上发消息的节点。
我刚开始学的时候,总觉得写ROS节点很神秘。后来发现,套路极其固定。你只要记住几个步骤就行。
发布者核心步骤:
- 初始化ROS节点
- 创建节点句柄
- 通过句柄发布话题
- 循环发布消息
来,直接看代码。我习惯在功能包的src目录下新建一个文件,叫talker.cpp。
// talker.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
int main(int argc, char **argv)
{
// 1. 初始化节点,节点名就叫"talker"
ros::init(argc, argv, "talker");
// 2. 创建节点句柄
ros::NodeHandle nh;
// 3. 发布话题"chatter",消息类型是std_msgs::String
// 队列大小设为1000,意思是如果订阅者处理不过来,最多缓存1000条
ros::Publisher chatter_pub = nh.advertise<std_msgs::String>("chatter", 1000);
// 4. 设置循环频率,10Hz
ros::Rate loop_rate(10);
int count = 0;
while (ros::ok())
{
std_msgs::String msg;
std::stringstream ss;
ss << "hello world " << count;
msg.data = ss.str();
// 发布消息
chatter_pub.publish(msg);
ROS_INFO("我发布了: %s", msg.data.c_str());
// 休眠,控制频率
loop_rate.sleep();
++count;
}
return 0;
}
嗯,这里要注意一点。ros::ok()这个函数很关键。它会检查节点是否被要求关闭。比如你在终端按了Ctrl+C,它就会返回false,循环就退出了。我曾经见过有人写死循环,结果节点关不掉,只能kill进程。
4.2 订阅者(Subscriber)的C++实现
有发的,就得有收的。订阅者代码同样套路固定。
小技巧:我建议你先把发布者跑起来,再启动订阅者。这样订阅者一启动就能收到数据。反过来,如果订阅者先启动,它就会在那干等,直到发布者上线。
新建一个listener.cpp文件:
// listener.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"
// 回调函数,收到消息时自动调用
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("我听到了: [%s]", msg->data.c_str());
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "listener");
ros::NodeHandle nh;
// 订阅话题"chatter",队列大小1000
// 一旦有消息,就调用chatterCallback
ros::Subscriber sub = nh.subscribe("chatter", 1000, chatterCallback);
// 进入循环,等待回调
ros::spin();
return 0;
}
看到ros::spin()了吗?这个函数会一直阻塞,直到节点关闭。它背后的原理是:不断检查有没有新消息到达,有的话就触发回调函数。
为什么不用while循环?因为ros::spin()内部做了优化,不会浪费CPU。你想想看,如果自己写while(true)加sleep,效率就差远了。
4.3 自定义消息类型
标准消息类型(比如std_msgs::String)只能传简单的数据。但实际项目中,我们经常需要传复杂结构。比如机器人的位置、速度、姿态——这些信息打包在一起才方便。
这时候就需要自定义消息了。
避坑指南:我曾经在项目里直接往话题里发JSON字符串,结果解析起来麻烦得要死。后来老老实实自定义消息,代码瞬间清爽了。别走弯路。
步骤很简单,就三步:
- 在功能包下创建
msg目录 - 编写
.msg文件 - 修改
CMakeLists.txt和package.xml
举个例子,我们定义一个机器人位置的消息:
// 文件: msg/RobotPos.msg
float32 x
float32 y
float32 theta
string name
然后修改CMakeLists.txt,找到这几行,取消注释并修改:
find_package(catkin REQUIRED COMPONENTS
roscpp
std_msgs
message_generation # 加上这个
)
add_message_files(
FILES
RobotPos.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
catkin_package(
CATKIN_DEPENDS message_runtime # 加上这个
)
再修改package.xml:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
编译一下:
cd ~/catkin_ws
catkin_make
编译成功后,你的自定义消息类型就生成了。在代码里可以这样用:
#include "你的包名/RobotPos.h"
// 发布者
ros::Publisher pub = nh.advertise<你的包名::RobotPos>("robot_pos", 10);
你的包名::RobotPos pos_msg;
pos_msg.x = 1.5;
pos_msg.y = 2.3;
pos_msg.theta = 0.78;
pos_msg.name = "turtlebot";
pub.publish(pos_msg);
// 订阅者
void posCallback(const 你的包名::RobotPos::ConstPtr& msg)
{
ROS_INFO("位置: x=%f, y=%f", msg->x, msg->y);
}
ros::Subscriber sub = nh.subscribe("robot_pos", 10, posCallback);
4.4 编译与运行
代码写完了,怎么跑起来?
首先,确保CMakeLists.txt里有这两行:
add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
然后编译:
cd ~/catkin_ws
catkin_make
启动roscore,再分别运行两个节点:
# 终端1
roscore
# 终端2
rosrun 你的包名 talker
# 终端3
rosrun 你的包名 listener
你会看到发布者每秒发10条消息,订阅者实时收到并打印出来。
核心总结:
- 发布者用
advertise,订阅者用subscribe - 订阅者通过回调函数处理消息
- 自定义消息需要三步:写msg文件、改CMakeLists、改package.xml
- 编译后自动生成头文件,直接include就能用
这一章的内容,说白了就是ROS通信的基石。你把这个搞懂了,后面学服务通信、动作通信都会轻松很多。下一章我们聊聊话题通信的进阶用法——包括消息过滤器、时间同步这些实战中经常遇到的东西。
嗯,今天就到这儿。动手试试吧。