3. ROS基础编程:Publisher与Subscriber实现、自定义消息、launch文件编写
好,咱们今天来聊聊ROS里最核心的通信方式——话题(Topic)。说白了,Publisher和Subscriber就是ROS世界里“说话”和“听话”的两个人。你想想看,一个机器人身上那么多传感器,激光雷达、摄像头、IMU,它们各自采集数据,然后通过话题发布出去;其他模块,比如导航、建图,就订阅这些话题来获取数据。这套机制,是ROS的基石。
我个人习惯,每开始一个新项目,第一件事就是先把话题通信的框架搭好。这就像盖房子先打地基,地基稳了,后面加什么功能都顺手。
3.1 编写一个简单的Publisher(发布者)
我们先写一个发布者节点。假设我们要发布一个“Hello ROS”的字符串消息。
创建一个包,名字就叫ros_basics,依赖roscpp、std_msgs:
cd ~/catkin_ws/src
catkin_create_pkg ros_basics roscpp std_msgs
然后在src文件夹里新建一个文件,叫talker.cpp:
#include <ros/ros.h>
#include <std_msgs/String.h>
int main(int argc, char **argv)
{
// 初始化ROS节点,名字叫"talker"
ros::init(argc, argv, "talker");
ros::NodeHandle nh;
// 创建一个Publisher,发布到"chatter"话题,队列大小1000
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 1000);
// 设置循环频率为10Hz
ros::Rate loop_rate(10);
int count = 0;
while (ros::ok())
{
std_msgs::String msg;
msg.data = "Hello ROS! Count: " + std::to_string(count);
ROS_INFO("Publishing: %s", msg.data.c_str());
pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
++count;
}
return 0;
}
嗯,这里要注意一点:ros::spinOnce()不能少。它负责处理回调函数,虽然我们这个例子里没有订阅回调,但养成习惯总是好的。我曾经在一个项目里忘了加spinOnce(),结果程序跑起来后,订阅端死活收不到数据,排查了半天才发现是这里的问题。
3.2 编写一个简单的Subscriber(订阅者)
有发布者,就得有订阅者。新建一个listener.cpp:
#include <ros/ros.h>
#include <std_msgs/String.h>
// 回调函数,当收到消息时自动调用
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("I heard: [%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()会一直阻塞,直到节点关闭。每次有消息到达,回调函数就会被触发。
我个人觉得,理解回调机制是ROS编程的一个门槛。你想想看,你的程序不是主动去“拿”数据,而是数据来了“通知”你。这种事件驱动的模式,在机器人系统里非常高效。
3.3 自定义消息类型
光用std_msgs::String肯定不够。实际项目中,我们经常需要传递复杂的数据结构。比如,我要发布一个“超声波传感器”的数据,包含距离、时间戳、传感器ID。这时候就需要自定义消息。
在ros_basics包下新建一个msg文件夹,创建Sonar.msg:
float32 distance
time stamp
int32 sensor_id
然后,修改CMakeLists.txt,添加以下内容:
find_package(catkin REQUIRED COMPONENTS
roscpp
std_msgs
message_generation
)
add_message_files(
FILES
Sonar.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
别忘了在package.xml里添加依赖:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
编译一下:
cd ~/catkin_ws
catkin_make
现在,你就可以在代码里使用ros_basics::Sonar这个类型了。用法和标准消息一模一样。
Header类型,但忘了在CMakeLists.txt里添加std_msgs依赖。结果编译报错,提示找不到Header。记住,Header是std_msgs的一部分,必须显式依赖。
3.4 launch文件编写
手动一个个启动节点太麻烦了。launch文件就是用来批量启动节点的。它用XML格式,简单直观。
在ros_basics包下新建launch文件夹,创建talker_listener.launch:
<launch>
<node name="talker" pkg="ros_basics" type="talker" output="screen"/>
<node name="listener" pkg="ros_basics" type="listener" output="screen"/>
</launch>
启动方式:
roslaunch ros_basics talker_listener.launch
你看,一行命令,两个节点同时跑起来了。output="screen"表示把日志输出到终端,方便调试。
launch文件还有很多高级用法,比如:
- 参数服务器:通过
<param>标签设置参数 - 命名空间:通过
ns属性隔离不同机器人 - 包含其他launch文件:通过
<include>标签复用配置
举个例子,设置一个参数:
<launch>
<param name="sonar_frequency" value="20.0"/>
<node name="sonar_node" pkg="ros_basics" type="sonar_publisher" output="screen"/>
</launch>
在代码里,你可以通过nh.getParam("sonar_frequency", freq)来获取这个参数。
3.5 实战:一个完整的传感器数据采集示例
我们来整合一下。假设我们要采集一个虚拟的超声波传感器数据,并实时显示。
发布者(sonar_publisher.cpp):
#include <ros/ros.h>
#include <ros_basics/Sonar.h>
int main(int argc, char **argv)
{
ros::init(argc, argv, "sonar_publisher");
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<ros_basics::Sonar>("sonar_data", 10);
ros::Rate loop_rate(5);
int sensor_id = 1;
while (ros::ok())
{
ros_basics::Sonar msg;
msg.distance = 1.0 + (rand() % 100) / 100.0; // 模拟1.0~2.0米
msg.stamp = ros::Time::now();
msg.sensor_id = sensor_id;
ROS_INFO("Sonar %d: %.2f m", msg.sensor_id, msg.distance);
pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
}
return 0;
}
订阅者(sonar_subscriber.cpp):
#include <ros/ros.h>
#include <ros_basics/Sonar.h>
void sonarCallback(const ros_basics::Sonar::ConstPtr& msg)
{
ROS_INFO("Received: Sensor %d, Distance: %.2f m, Time: %.2f",
msg->sensor_id, msg->distance, msg->stamp.toSec());
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "sonar_subscriber");
ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe("sonar_data", 10, sonarCallback);
ros::spin();
return 0;
}
launch文件(sonar.launch):
<launch>
<node name="sonar_pub" pkg="ros_basics" type="sonar_publisher" output="screen"/>
<node name="sonar_sub" pkg="ros_basics" type="sonar_subscriber" output="screen"/>
</launch>
运行一下:
roslaunch ros_basics sonar.launch
你会看到发布者每秒发5次数据,订阅者实时打印出来。这就是一个最基础的传感器数据采集流程。
好了,这一章的内容就到这里。Publisher和Subscriber是ROS通信的“左膀右臂”,自定义消息让你能灵活封装数据,launch文件则让系统启动变得优雅。下一章,我们会深入服务(Service)和动作(Action)通信,敬请期待。