第1章:ROS通信机制深入
各位同学,咱们今天来聊聊ROS的通信机制。说实话,这是整个ROS的命脉所在。我刚开始学ROS那会儿,总觉得通信嘛,不就是发发消息、调调服务?后来做项目才发现,这里面的门道可不少。
1.1 话题通信(Publisher/Subscriber)编程实战
话题通信,说白了就是发布-订阅模式。一个节点发消息,其他节点收消息。这种模式在机器人里用得最多——比如激光雷达数据、摄像头图像、里程计信息,都是通过话题来传递的。
核心要点:话题通信是异步的、一对多的。发布者只管发,订阅者只管收,两者互不等待。
1.1.1 编写一个发布者节点
我习惯用C++写ROS节点,因为效率高。咱们先写一个简单的发布者,每隔1秒发布一个字符串消息。
// talker.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"
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(1); // 1Hz
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();
}
return 0;
}
嗯,这里要注意几个地方。第一,advertise的第二个参数是队列大小,我建议设成10以上,防止消息丢失。第二,ros::spinOnce()一定要加,不然回调函数不会执行。
个人经验:我在做AGV小车项目时,曾经因为忘记加spinOnce(),导致订阅者收不到消息。排查了半天才发现这个低级错误。所以,养成好习惯很重要。
1.1.2 编写一个订阅者节点
有发布就得有订阅。咱们写个订阅者,接收上面发布的消息。
// 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;
ros::Subscriber sub = nh.subscribe("chatter", 10, chatterCallback);
ros::spin(); // 进入循环,等待回调
return 0;
}
看到没?订阅者比发布者还简单。一个回调函数,一个ros::spin()就搞定了。为什么用spin()而不是spinOnce()?因为订阅者需要一直等待消息,用spin()更合适。
1.2 服务通信(Server/Client)编程实战
话题通信是单向的,服务通信是双向的。客户端发请求,服务端回响应。这就像你打电话问客服——你问一句,对方答一句。
应用场景:服务通信适合需要即时响应的操作,比如控制机器人运动、查询传感器状态、执行任务切换等。
1.2.1 编写一个服务端节点
咱们写一个简单的加法服务。客户端传入两个整数,服务端返回它们的和。
// server.cpp
#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"
bool add(beginner_tutorials::AddTwoInts::Request &req,
beginner_tutorials::AddTwoInts::Response &res)
{
res.sum = req.a + req.b;
ROS_INFO("request: a=%ld, b=%ld", (long int)req.a, (long int)req.b);
ROS_INFO("sending back response: [%ld]", (long int)res.sum);
return true;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_server");
ros::NodeHandle nh;
ros::ServiceServer service = nh.advertiseService("add_two_ints", add);
ROS_INFO("Ready to add two ints.");
ros::spin();
return 0;
}
这里有个坑,我当年踩过。服务回调函数必须返回bool值,表示是否成功处理。如果你返回false,客户端会收到一个错误响应。
1.2.2 编写一个客户端节点
// client.cpp
#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"
#include <cstdlib>
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_client");
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<beginner_tutorials::AddTwoInts>("add_two_ints");
beginner_tutorials::AddTwoInts srv;
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
if (client.call(srv))
{
ROS_INFO("Sum: %ld", (long int)srv.response.sum);
}
else
{
ROS_ERROR("Failed to call service add_two_ints");
return 1;
}
return 0;
}
避坑指南:我曾经在调用服务前没有检查服务是否可用,结果程序直接崩溃。建议用ros::service::waitForService()等待服务就绪,或者用client.waitForExistence()。
1.3 参数服务器操作
参数服务器,说白了就是一个全局的键值对存储。你可以在任何节点里读写参数,非常方便。
| 操作 | API | 说明 |
|---|---|---|
| 设置参数 | nh.setParam("key", value) | 设置一个参数 |
| 获取参数 | nh.getParam("key", value) | 获取参数值 |
| 删除参数 | nh.deleteParam("key") | 删除指定参数 |
| 检查参数 | nh.hasParam("key") | 检查参数是否存在 |
举个例子,咱们在启动文件中设置机器人的轮子半径:
<launch>
<param name="wheel_radius" value="0.1" />
<node name="robot_node" pkg="my_robot" type="robot_node" output="screen"/>
</launch>
然后在节点里读取:
double wheel_radius;
if (nh.getParam("wheel_radius", wheel_radius))
{
ROS_INFO("Wheel radius: %f", wheel_radius);
}
else
{
ROS_WARN("Parameter wheel_radius not set, using default 0.1");
wheel_radius = 0.1;
}
我的习惯:我一般把机器人的物理参数(轮距、轴距、最大速度等)都放在参数服务器里。这样换硬件时,改个配置文件就行,不用重新编译代码。
1.4 自定义消息与服务
ROS自带的类型(String、Int32等)够用吗?说实话,大部分时候不够。你想想看,一个激光雷达数据包含距离、角度、时间戳,用基本类型拼凑多麻烦?这时候就需要自定义消息了。
1.4.1 自定义消息
在包目录下创建msg文件夹,新建MyMessage.msg:
# MyMessage.msg
string name
int32 age
float32 score
time stamp
然后在CMakeLists.txt里添加:
add_message_files(
FILES
MyMessage.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
别忘了在package.xml里添加依赖:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
1.4.2 自定义服务
类似地,创建srv文件夹,新建MyService.srv:
# MyService.srv
int32 input
---
string output
横线上面是请求,下面是响应。这个格式很直观,对吧?
关键点:自定义消息和服务编译后,ROS会自动生成对应的头文件和类。你只需要在代码里#include就能用了。
小结
这一章咱们把ROS的三大通信机制都过了一遍。话题通信适合持续的数据流,服务通信适合即时的请求-响应,参数服务器适合全局配置。我个人觉得,理解这三者的区别和适用场景,比记住API更重要。
下节课咱们聊ROS的坐标变换系统——tf。那可是机器人定位的基石,到时候我会分享一些实际项目中的坑和技巧。
课后练习:写一个话题发布者,发布自定义消息(包含机器人位置x、y和朝向theta)。再写一个订阅者,打印接收到的位置信息。试试看,不难的。