3. ROS2 基础概念:Node、Topic、Service、Action、Parameter
好,咱们正式开始接触 ROS2 的核心概念了。
说实话,我当年刚学 ROS 的时候,被这些名词搞得一头雾水。什么节点、话题、服务……听着像计算机网络课,又像操作系统课。后来做项目做多了才明白,ROS2 本质上就是一套 进程间通信(IPC) 的框架。你想想看,机器人上有那么多传感器、电机、算法模块,它们之间怎么说话?这就是 ROS2 要解决的事。
一句话总结:ROS2 是一个分布式通信框架。节点是“人”,话题是“广播”,服务是“一问一答”,动作是“带反馈的长任务”,参数是“配置文件”。
下面我一个个拆开讲。每个概念我都会结合项目里的实际场景来说,这样你理解起来会更接地气。
3.1 节点(Node)—— 机器人的“人”
节点是 ROS2 里最小的执行单元。你可以把每个节点想象成一个独立的人,每个人有自己的职责。
比如,激光雷达节点负责扫描环境,里程计节点负责计算位置,路径规划节点负责找路。它们各干各的,互不干扰。
我的习惯:我一般把每个传感器、每个算法模块都单独做成一个节点。这样调试的时候,哪个节点崩了,只影响它自己,不会把整个系统拖垮。曾经有一次我在项目里把激光驱动和定位写在一个节点里,结果激光数据一断,定位也跟着挂了……从那以后,我再也不偷懒了。
创建一个节点很简单,Python 里就这么写:
import rclpy
from rclpy.node import Node
class MyRobotNode(Node):
def __init__(self):
super().__init__('my_robot_node')
self.get_logger().info('节点已启动!')
def main(args=None):
rclpy.init(args=args)
node = MyRobotNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
你看,核心就三步:初始化、创建节点、spin 保持运行。spin 这个函数很有意思,它让节点一直活着,随时处理进来的数据。
3.2 话题(Topic)—— 机器人的“广播”
话题是 ROS2 里最常用的通信方式。它是 异步的、一对多的。
说白了,就是有人(发布者)在广播,其他人(订阅者)想听就听。发布者不知道谁在听,订阅者也不知道谁在发。这种解耦设计,在机器人系统里非常实用。
我举个例子。激光雷达节点每隔 0.1 秒发布一次扫描数据,话题名叫 /scan。定位节点订阅 /scan,建图节点也订阅 /scan。它们各取所需,互不干扰。
关键点:话题通信是“发出去就不管了”。发布者只管发,不关心有没有人收到。订阅者只管收,不关心是谁发的。这种模式适合周期性数据,比如传感器数据、状态信息。
代码示例,发布者:
class TalkerNode(Node):
def __init__(self):
super().__init__('talker')
self.publisher = self.create_publisher(String, 'chatter', 10)
timer_period = 0.5
self.timer = self.create_timer(timer_period, self.timer_callback)
def timer_callback(self):
msg = String()
msg.data = 'Hello, ROS2!'
self.publisher.publish(msg)
订阅者:
class ListenerNode(Node):
def __init__(self):
super().__init__('listener')
self.subscription = self.create_subscription(
String, 'chatter', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'收到: {msg.data}')
避坑指南:我曾经在项目里把队列大小(上面的 10)设成了 1,结果数据一多就丢包。后来才明白,队列大小要按你的处理速度来设。处理慢就设大点,但别太大,否则内存会爆。一般传感器数据设 10~50 就够用。
3.3 服务(Service)—— 一问一答
话题是广播,服务就是 打电话。你打过去,对方接起来,你问一句,他答一句,然后挂断。
服务是 同步的、一对一的。客户端发请求,服务端处理完返回响应。这期间客户端会等着。
什么时候用服务?比如你让机器人去抓一个杯子。你发一个请求:“去抓那个杯子”,机器人抓完了回复你:“抓到了”或者“没抓到”。这种需要确认结果的操作,用服务就很合适。
定义服务接口(.srv 文件):
int64 a
int64 b
---
int64 sum
服务端代码:
class AddTwoIntsServer(Node):
def __init__(self):
super().__init__('add_two_ints_server')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_callback)
def add_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f'{request.a} + {request.b} = {response.sum}')
return response
我的经验:服务调用不要用在高频场景。比如控制电机,每秒要发几百次指令,用服务就不行——客户端等响应的时间太长了。这种场景应该用话题。服务适合那种“偶尔调用一次,但需要确认结果”的操作。
3.4 动作(Action)—— 带反馈的长任务
动作是服务的 升级版。
你想想看,如果让机器人从 A 点导航到 B 点,这个过程可能要几十秒。用服务的话,你发一个请求,然后干等几十秒?太傻了。用话题的话,你又不知道任务到底完成了没有。
动作就是来解决这个问题的。它有三个部分:
- Goal(目标):告诉机器人你要干什么
- Feedback(反馈):机器人一边干活一边告诉你进度,比如“走了 30% 了”
- Result(结果):任务完成后的最终结果
动作接口定义(.action 文件):
# 目标
geometry_msgs/PoseStamped target_pose
---
# 结果
bool success
string message
---
# 反馈
float32 progress
什么时候用动作?导航、机械臂运动、扫地机器人回充……只要是长时间执行、需要中途反馈、还能随时取消的任务,都用动作。
我记得有一次做巡检机器人,导航到每个点位大概要 2 分钟。一开始我用服务,结果 UI 界面卡死了。换成动作后,界面可以实时显示进度条,用户体验好太多了。
3.5 参数(Parameter)—— 机器人的“配置文件”
参数就是节点的配置项。比如激光雷达的扫描频率、PID 控制器的系数、导航的最大速度……这些值如果写死在代码里,每次改都要重新编译,太麻烦了。
ROS2 的参数系统允许你在 运行时动态修改 这些值。
声明参数:
self.declare_parameter('max_speed', 0.5)
self.declare_parameter('pid_kp', 1.2)
读取参数:
max_speed = self.get_parameter('max_speed').value
在启动时加载参数文件(YAML 格式):
# config/params.yaml
my_robot_node:
ros__parameters:
max_speed: 0.8
pid_kp: 1.5
pid_ki: 0.01
注意:参数虽然可以动态修改,但不是所有参数都适合运行时改。比如激光雷达的波特率,改了之后需要重新初始化硬件,这种参数最好在启动时设好就别动了。我一般把参数分为“热参数”(运行时可以改)和“冷参数”(启动时固定),在代码里用注释标清楚。
3.6 五种机制的对比
最后,我用一张表帮你理清这五种机制的区别。这张表我每次培训新人都会拿出来讲,很实用。
| 机制 | 通信模式 | 同步/异步 | 典型场景 | 是否带反馈 |
|---|---|---|---|---|
| Topic | 发布/订阅 | 异步 | 传感器数据、状态广播 | 否 |
| Service | 请求/响应 | 同步 | 开关控制、查询状态 | 一次性结果 |
| Action | 目标/反馈/结果 | 异步 | 导航、机械臂运动 | 持续反馈 + 最终结果 |
| Parameter | 键值对 | 同步/异步 | 配置项、调参 | 否 |
嗯,到这里,ROS2 的五大基础概念就讲完了。你可能会觉得有点抽象,没关系,后面每一章都会用到这些概念。等你亲手写几个节点、跑几个话题之后,这些概念就会变成你的肌肉记忆。