3 · 话题通信实战
好,咱们直接动手。前两章把概念和工具铺垫完了,这一章我带你写点真东西。你会亲手搓一个发布者、一个订阅者,然后用 launch 文件把它们撮合到一起。说白了,就是 ROS 里最核心的“发-收”模型,你搞懂了它,SLAM 里那些传感器数据流、里程计、地图话题,全是一个套路。
3.1 编写一个简单的发布者节点
我习惯用 C++ 写节点,因为 SLAM 里对实时性要求高。不过 Python 版本也给你,你挑顺手的上手就行。先看 C++ 的发布者,我们叫它 talker。
// talker.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
int main(int argc, char **argv)
{
ros::init(argc, argv, "talker");
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 10);
ros::Rate loop_rate(10); // 10 Hz
int count = 0;
while (ros::ok())
{
std_msgs::String msg;
std::stringstream ss;
ss << "hello SLAM " << count;
msg.data = ss.str();
ROS_INFO("发布: %s", msg.data.c_str());
pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
++count;
}
return 0;
}
嗯,这里要注意:advertise 的第二个参数是队列大小,10 表示最多缓存 10 条消息。我在项目里遇到过队列设太小导致丢消息,尤其是激光雷达数据,建议根据频率调大一点。
3.2 编写一个简单的订阅者节点
有发就有收。订阅者节点 listener 会一直监听 /chatter 话题,收到消息就打印出来。
// 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;
ros::Subscriber sub = nh.subscribe("chatter", 10, chatterCallback);
ros::spin(); // 阻塞等待回调
return 0;
}
你看,订阅者比发布者还简单。核心就三行:初始化、订阅、spin。这里我特别想说一下 ros::spin() 和 ros::spinOnce() 的区别。你想想看,如果你的节点还要做其他计算(比如跑 SLAM 前端),就不能用 spin() 卡死,得用 spinOnce() 配合循环。我在做多传感器融合时,就因为这个踩过坑——用了 spin 导致其他传感器回调饿死。
3.3 使用 launch 文件启动多节点
手动开两个终端,一个 rosrun talker,一个 rosrun listener,太原始了。生产环境里我们全用 launch 文件。下面这个 launch 把两个节点一起拉起来,还能设置参数。
<?xml version="1.0"?>
<launch>
<node name="talker_node" pkg="beginner_tutorials" type="talker" output="screen"/>
<node name="listener_node" pkg="beginner_tutorials" type="listener" output="screen"/>
</launch>
就这几行。你把它保存成 talker_listener.launch,然后 roslaunch beginner_tutorials talker_listener.launch,两个节点同时跑起来。output="screen" 表示把日志打印到终端,调试时很有用。
| launch 属性 | 作用 | 我常用的值 |
|---|---|---|
| name | 节点别名(覆盖节点名) | 用有意义的别名,如 lidar_driver |
| pkg | 功能包名 | 与 CMakeLists 一致 |
| type | 可执行文件名 | 编译出的二进制名 |
| output | 日志输出方式 | screen / log |
| args | 命令行参数 | 比如波特率、端口号 |
ros::init 决定。但如果你在 launch 里重映射了名称,会覆盖代码里的名字。我建议统一用 launch 里的 name,方便管理。
3.4 把一切串起来:编译与运行
假设你已经在工作空间 catkin_ws/src/beginner_tutorials 里写好了代码。别忘了在 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})
然后 catkin_make。我个人习惯在编译前先 source devel/setup.bash,不然容易找不到包。运行 launch 后,你会看到 talker 每秒发 10 次 "hello SLAM X",listener 同步打印出来。嗯,这就是 ROS 最基础的通信闭环。
最后,你可以用 rqt_graph 看看节点图,确认 talker 和 listener 通过 /chatter 连在一起。这一步我每次都会做,因为肉眼看到箭头连通,心里才踏实。