2、ROS核心概念:节点、话题、服务、参数服务器、消息
好,咱们正式开始接触ROS的“灵魂”了。
说实话,我第一次接触ROS时,最头疼的就是这些概念。节点、话题、服务……听着都像一回事儿。但等你真正上手写一个机器人程序,就会发现——这些概念就像乐高积木,缺一块都拼不起来。
这一章,我带你一个一个拆开看。别急,咱们慢慢来。
2.1 节点(Node)——机器人里的“小工人”
节点是什么?说白了,就是一个独立的可执行程序。它负责完成某一件具体的事。
比如,你有一个激光雷达,那它的驱动程序就是一个节点。你有一个电机控制器,那它的控制程序也是一个节点。你有一个路径规划算法,那它还是一个节点。
每个节点都是独立的。它们可以运行在同一台电脑上,也可以分布在不同的电脑上。节点之间通过ROS的通信机制互相联系。
核心要点:
- 一个节点就是一个进程
- 节点之间互相独立,互不依赖
- 节点通过话题、服务等方式通信
我在项目中遇到过一个问题:一开始把所有功能都写在一个程序里,结果改一个地方就要重新编译整个项目,调试起来特别痛苦。后来改成多节点架构,每个节点只做一件事,改起来轻松多了。
2.2 话题(Topic)——节点间的“广播频道”
话题是ROS里最常用的通信方式。你可以把它想象成一个广播频道。
一个节点往话题里发消息,其他节点可以订阅这个频道来接收消息。发布者和订阅者之间不需要知道对方是谁,只需要知道话题的名字和消息的类型。
我的习惯:话题命名最好用“/”分隔的层次结构,比如/robot/left_wheel/velocity。这样一看就知道是哪个机器人的哪个部件。
举个例子:
# 发布者节点
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 10);
ros::Rate rate(10);
while (ros::ok()) {
std_msgs::String msg;
msg.data = "hello world";
pub.publish(msg);
rate.sleep();
}
# 订阅者节点
void chatterCallback(const std_msgs::String::ConstPtr& msg) {
ROS_INFO("I heard: [%s]", msg->data.c_str());
}
ros::Subscriber sub = nh.subscribe("chatter", 10, chatterCallback);
你想想看,这种设计有什么好处?
嗯,最大的好处就是解耦。发布者不需要关心谁在听,订阅者也不需要关心谁在说。你随时可以增加一个新的订阅者,或者替换掉一个发布者,完全不影响其他节点。
2.3 服务(Service)——一问一答的“请求-响应”模式
话题是单向的、持续的。但有些场景你需要一问一答。
比如,你让机器人“去拿桌子上的杯子”。这不是一个持续的数据流,而是一个请求,然后等待机器人回答“好的,我去了”或者“不行,我够不着”。
这就是服务。
我曾经踩过的坑:一开始我分不清什么时候用话题、什么时候用服务。后来总结了一个简单规则:
- 如果数据是持续更新的(比如传感器数据),用话题
- 如果是一次性请求+响应的(比如“打开灯”),用服务
服务定义需要两个文件:请求(Request)和响应(Response)。
# 服务定义文件:AddTwoInts.srv
int64 a
int64 b
---
int64 sum
服务端代码:
bool add(ros::ServiceEvent<beginner_tutorials::AddTwoInts::Request,
beginner_tutorials::AddTwoInts::Response> &event) {
event.response.sum = event.request.a + event.request.b;
return true;
}
ros::ServiceServer service = nh.advertiseService("add_two_ints", add);
客户端代码:
ros::ServiceClient client = nh.serviceClient<beginner_tutorials::AddTwoInts>("add_two_ints");
beginner_tutorials::AddTwoInts srv;
srv.request.a = 3;
srv.request.b = 4;
if (client.call(srv)) {
ROS_INFO("Sum: %ld", (long int)srv.response.sum);
}
2.4 参数服务器(Parameter Server)——全局的“小黑板”
参数服务器,说白了就是一个全局的键值对存储。所有节点都可以读写它。
我习惯用它来存一些配置参数,比如机器人的最大速度、PID参数、传感器阈值等。这样改参数时不用重新编译代码,直接改参数文件就行。
常用操作:
- 设置参数:
rosparam set /robot/max_speed 1.0 - 获取参数:
rosparam get /robot/max_speed - 加载参数文件:
rosparam load params.yaml
在代码里读写参数也很简单:
// 读取参数
double max_speed;
nh.param("max_speed", max_speed, 0.5); // 默认值0.5
// 设置参数
nh.setParam("max_speed", 1.0);
我的建议:参数服务器适合存“不常变”的配置。如果你需要频繁更新数据,还是用话题更合适。
2.5 消息(Message)——通信的“数据格式”
消息就是ROS里通信的数据格式。话题和服务都依赖消息来定义数据结构。
ROS自带了很多标准消息类型,比如:
| 消息类型 | 说明 |
|---|---|
std_msgs/String |
字符串 |
std_msgs/Int32 |
32位整数 |
geometry_msgs/Twist |
速度指令(线速度+角速度) |
sensor_msgs/LaserScan |
激光雷达数据 |
nav_msgs/Odometry |
里程计数据 |
你也可以自定义消息。比如,你想发一个“机器人状态”消息,包含位置、速度、电量:
# RobotStatus.msg
string robot_name
float32 x
float32 y
float32 velocity
float32 battery_level
定义好后,在CMakeLists.txt里加上:
add_message_files(FILES RobotStatus.msg)
generate_messages(DEPENDENCIES std_msgs)
然后就可以在代码里用了:
robot_status::RobotStatus msg;
msg.robot_name = "turtlebot";
msg.x = 1.0;
msg.y = 2.0;
msg.velocity = 0.5;
msg.battery_level = 85.0;
pub.publish(msg);
2.6 它们是怎么配合的?
咱们用一个实际场景串起来:
- 激光雷达节点发布
/scan话题,消息类型是sensor_msgs/LaserScan - 里程计节点发布
/odom话题,消息类型是nav_msgs/Odometry - 路径规划节点订阅
/scan和/odom,计算出目标速度,发布到/cmd_vel话题 - 电机控制节点订阅
/cmd_vel,把速度指令发给电机 - 如果你想“重置机器人位置”,可以调用
/reset_pose服务 - 所有节点的参数(比如最大速度、PID系数)都从参数服务器读取
你看,每个节点各司其职,通过话题和服务互相配合。这就是ROS的模块化设计思想。
总结一下:
- 节点:干活的工人
- 话题:广播频道,持续通信
- 服务:一问一答,一次性通信
- 参数服务器:全局小黑板,存配置
- 消息:通信的数据格式
嗯,这一章的内容就到这里。下一章咱们会动手写第一个ROS节点,到时候这些概念就真正活起来了。