第二章:系统架构设计——基于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节点。