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 它们是怎么配合的?

咱们用一个实际场景串起来:

  1. 激光雷达节点发布/scan话题,消息类型是sensor_msgs/LaserScan
  2. 里程计节点发布/odom话题,消息类型是nav_msgs/Odometry
  3. 路径规划节点订阅/scan/odom,计算出目标速度,发布到/cmd_vel话题
  4. 电机控制节点订阅/cmd_vel,把速度指令发给电机
  5. 如果你想“重置机器人位置”,可以调用/reset_pose服务
  6. 所有节点的参数(比如最大速度、PID系数)都从参数服务器读取

你看,每个节点各司其职,通过话题和服务互相配合。这就是ROS的模块化设计思想。

总结一下:

  • 节点:干活的工人
  • 话题:广播频道,持续通信
  • 服务:一问一答,一次性通信
  • 参数服务器:全局小黑板,存配置
  • 消息:通信的数据格式

嗯,这一章的内容就到这里。下一章咱们会动手写第一个ROS节点,到时候这些概念就真正活起来了。