第二章:系统架构设计——基于ROS2的节点图设计
好,咱们进入正题。上一章我们聊了需求分析,说白了就是搞清楚「要做什么」。那这一章,我们就得回答「怎么做」了。
我个人习惯,拿到需求后第一件事不是写代码,而是画节点图。你想想看,一个机器人系统少说十几个节点,多则上百个。没有一张清晰的架构图,后面写代码就是闭着眼睛过河。
2.1 节点图:系统的骨架
ROS2里,每个节点就是一个独立的可执行单元。它们之间通过Topic、Service、Action三种方式通信。我见过不少新手,上来就把所有逻辑塞进一个节点里,结果调试时想死的心都有。
嗯,这里要注意:节点拆分的原则是「高内聚、低耦合」。说白了,一个节点只干一件事,干好一件事。
举个例子,一个移动机器人底盘系统,我通常会拆成这样:
- 传感器节点:负责读取激光雷达、IMU、里程计数据
- 定位节点:接收传感器数据,输出机器人位姿
- 规划节点:根据目标点和当前位置,规划路径
- 控制节点:接收路径,输出速度指令给电机
- 状态机节点:管理整个系统的运行状态(启动、导航、避障、暂停等)
核心原则:每个节点只负责一个功能域。比如定位节点只管算位姿,别让它去管电机。
2.2 通信矩阵:定义Topic、Service、Action
节点图画好了,接下来就是定义它们之间怎么说话。这就是通信矩阵的活儿。
我建议你用一个表格把所有的通信关系列出来。这样做的好处是:一表在手,接口不愁。我在项目中遇到过好几次,因为接口没对齐,联调时两个节点对不上数据类型,改代码改到凌晨三点。
2.2.1 Topic:最常用的发布/订阅模式
Topic适合持续、高频、单向的数据流。比如传感器数据、状态信息。
| Topic名称 | 消息类型 | 发布者 | 订阅者 | 频率 | 说明 |
|---|---|---|---|---|---|
| /scan | sensor_msgs/LaserScan | 激光雷达节点 | 定位节点、避障节点 | 10Hz | 激光雷达原始数据 |
| /odom | nav_msgs/Odometry | 里程计节点 | 定位节点、控制节点 | 50Hz | 轮式里程计数据 |
| /cmd_vel | geometry_msgs/Twist | 控制节点 | 电机驱动节点 | 20Hz | 速度指令 |
| /robot_status | std_msgs/String | 状态机节点 | 所有节点 | 1Hz | 系统状态广播 |
我的经验:Topic的QoS设置很重要。比如激光雷达数据用「可靠」传输,状态信息用「尽力而为」就行。别一股脑全用默认值。
2.2.2 Service:请求/响应的同步调用
Service适合一次性、有反馈的操作。比如「启动导航」、「获取当前电量」。
| Service名称 | 服务类型 | 服务端 | 客户端 | 说明 |
|---|---|---|---|---|
| /start_navigation | std_srvs/Trigger | 状态机节点 | UI节点 | 启动导航任务 |
| /get_battery | my_interfaces/GetBattery | 电源管理节点 | UI节点、状态机节点 | 查询当前电量百分比 |
| /set_speed_limit | my_interfaces/SetSpeedLimit | 控制节点 | UI节点 | 动态调整最大速度 |
注意:Service是同步的,客户端会阻塞等待响应。如果服务端处理时间较长(比如超过1秒),建议改用Action。
2.2.3 Action:带反馈的异步任务
Action是ROS2里我最喜欢的一个通信方式。它适合长时间执行、可取消、有中间反馈的任务。比如「导航到目标点」、「执行机械臂轨迹」。
| Action名称 | Action类型 | 服务端 | 客户端 | 说明 |
|---|---|---|---|---|
| /navigate_to_pose | nav2_msgs/NavigateToPose | 导航节点 | 状态机节点、UI节点 | 导航到指定坐标,反馈进度 |
| /follow_path | nav2_msgs/FollowPath | 控制节点 | 规划节点 | 执行路径跟踪,反馈当前位置 |
| /dock_robot | my_interfaces/DockRobot | 充电对接节点 | 状态机节点 | 自动回充,反馈对接状态 |
为什么用Action而不是Topic+Service? 我曾经在一个项目里用Topic发导航目标,再用另一个Topic收反馈。结果反馈和状态对不上,调试了整整两天。后来换成Action,一个接口搞定目标、反馈、结果、取消,清爽多了。
2.3 通信矩阵总表
好了,我们把上面所有的通信关系汇总成一张大表。这就是你后续编码的「接口圣经」。
| 通信方式 | 名称 | 类型 | 发布者/服务端 | 订阅者/客户端 | 关键参数 |
|---|---|---|---|---|---|
| Topic | /scan | LaserScan | 激光雷达节点 | 定位、避障 | QoS:可靠, 10Hz |
| Topic | /odom | Odometry | 里程计节点 | 定位、控制 | QoS:可靠, 50Hz |
| Topic | /cmd_vel | Twist | 控制节点 | 电机驱动 | QoS:尽力而为, 20Hz |
| Topic | /robot_status | String | 状态机节点 | 所有节点 | QoS:尽力而为, 1Hz |
| Service | /start_navigation | Trigger | 状态机节点 | UI节点 | 超时:5s |
| Service | /get_battery | GetBattery | 电源管理 | UI、状态机 | 超时:2s |
| Service | /set_speed_limit | SetSpeedLimit | 控制节点 | UI节点 | 超时:1s |
| Action | /navigate_to_pose | NavigateToPose | 导航节点 | 状态机、UI | 超时:300s, 反馈:1Hz |
| Action | /follow_path | FollowPath | 控制节点 | 规划节点 | 超时:600s, 反馈:10Hz |
| Action | /dock_robot | DockRobot | 充电对接 | 状态机 | 超时:120s, 反馈:2Hz |
2.4 避坑指南
最后,分享几个我踩过的坑:
- 我曾经把Topic和Service混用。比如用Service发高频数据,结果服务端处理不过来,系统直接卡死。记住:高频数据用Topic,低频操作用Service。
- 我曾经忘记定义Action的反馈消息。结果客户端收不到进度,用户以为系统死机了。Action的三个部分(goal、feedback、result)一个都不能少。
- 我曾经在多个节点里定义了同名的Topic,结果数据互相覆盖。建议在通信矩阵里统一命名,别偷懒。
我的习惯:每次设计完通信矩阵,我会打印出来贴在工位上。写代码时看一眼,省得边写边查。你也不妨试试。
好了,这一章就到这里。下一章我们开始动手写代码,把这张通信矩阵变成真正的ROS2节点。