2. 自定义消息与服务:msg/srv/action 文件定义、接口包创建与编译、复杂嵌套类型设计

好,咱们进入第二章。说实话,ROS2 里最核心的“通信语言”就是接口定义。你想想看,机器人系统里那么多节点——传感器、规划器、执行器——它们之间怎么“对话”?靠的就是消息、服务、动作这三种接口。

我个人习惯,拿到一个新项目,第一件事不是写代码,而是先把接口定义好。接口定了,整个系统的骨架就出来了。今天咱们就聊聊怎么定义这些接口,怎么创建接口包,以及怎么处理那些复杂的嵌套类型。

2.1 接口的三种形态:msg、srv、action

先理清概念。ROS2 里接口分三种,各有各的用途:

  • msg(消息):发布/订阅模式,单向数据流。比如激光雷达发数据,你只管收,不需要回复。
  • srv(服务):请求/响应模式,一问一答。比如你让机械臂“去抓那个杯子”,它抓完了告诉你“好了”或者“失败了”。
  • action(动作):带反馈的服务,适合长时间任务。比如导航到某个点,过程中不断告诉你“走到哪了”,最后说“到了”。

嗯,这里要注意:action 其实是 srv 的升级版。它多了反馈机制,可以中途取消。我在项目里做机械臂抓取时,就经常用 action——抓取过程中如果目标移动了,可以随时取消重来。

2.2 接口文件长什么样?

直接看例子。msg 文件很简单,就是字段类型加字段名:

# 文件:my_interfaces/msg/SensorData.msg
int32 id
float64 temperature
float64 humidity
string device_name

srv 文件分两部分,用 --- 隔开:

# 文件:my_interfaces/srv/SetTarget.srv
int32 target_id
float64 target_value
---
bool success
string message

action 文件分三部分,两个 ---:

# 文件:my_interfaces/action/MoveTo.action
# 目标
geometry_msgs/Point target_pose
---
# 结果
bool reached
float64 duration
---
# 反馈
float64 distance_remaining

为什么 action 要分三段?说白了,目标是你发起的请求,结果是最终答复,反馈是过程中的状态更新。我刚开始用的时候老搞混,后来记住一句话:目标是你想要的,结果是最终给的,反馈是过程报的。

2.3 创建接口包

接口包本质上就是一个专门放 .msg、.srv、.action 文件的包。我建议你单独建一个包,比如 my_robot_interfaces,所有自定义接口都放里面。这样其他包依赖它就行,不用到处找。

创建步骤:

  1. 在工作空间 src 目录下,用 ros2 pkg create 创建包,记得加 --build-type ament_cmake。
  2. 在包目录下创建 msg、srv、action 三个文件夹。
  3. 把定义好的接口文件放进去。
  4. 修改 CMakeLists.txt 和 package.xml。

CMakeLists.txt 里关键配置:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/SensorData.msg"
  "srv/SetTarget.srv"
  "action/MoveTo.action"
  DEPENDENCIES geometry_msgs
)

package.xml 里要加:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

我曾经犯过一个低级错误——忘了加 member_of_group 那行,结果编译死活找不到接口。排查了半天,最后发现是这行漏了。嗯,这种坑踩过一次就记住了。

2.4 编译与验证

编译接口包和普通包一样:

cd ~/ros2_ws
colcon build --packages-select my_robot_interfaces
source install/setup.bash

编译成功后,可以用命令行验证:

ros2 interface show my_robot_interfaces/msg/SensorData
ros2 interface show my_robot_interfaces/srv/SetTarget
ros2 interface show my_robot_interfaces/action/MoveTo

如果能看到你定义的内容,说明接口包编译成功了。我每次改完接口都会跑一遍这个命令,确保没写错字段类型或名字。

2.5 复杂嵌套类型设计

实际项目中,很少用单一类型。比如一个机器人状态消息,可能包含位置、姿态、速度、电池信息等等。这时候就需要嵌套了。

举个例子,定义一个机器人状态消息:

# 文件:my_robot_interfaces/msg/RobotStatus.msg
string robot_name
builtin_interfaces/Time timestamp
geometry_msgs/Pose pose
geometry_msgs/Twist twist
float64 battery_level
my_robot_interfaces/msg/SensorData[] sensors

看到了吗?geometry_msgs/Pose 是标准类型,my_robot_interfaces/msg/SensorData[] 是自定义类型的数组。这就是嵌套。

设计嵌套接口时,我有个原则:尽量复用已有的接口。ROS2 标准库里有 geometry_msgs、std_msgs、sensor_msgs 等,能用现成的就别自己造。实在不够用,再定义自己的。

小技巧: 嵌套类型在 CMakeLists.txt 里要声明依赖。比如用了 geometry_msgs,就要在 rosidl_generate_interfaces 的 DEPENDENCIES 里加上它。否则编译会报找不到类型。

2.6 接口设计的最佳实践

做接口设计,说白了就是定协议。协议定得好,后面开发就顺畅。我总结了几条经验:

  • 字段命名要清晰:别用 a、b、data1 这种名字。用 target_pose、battery_level,一看就懂。
  • 类型选对:整数用 int32 还是 uint8?时间戳用 builtin_interfaces/Time 还是 std_msgs/Header?想清楚再定。
  • 不要过度设计:接口不是越复杂越好。够用就行,后面可以扩展。我见过有人一个消息里塞了 50 个字段,结果大部分都用不上。
  • 版本兼容:接口一旦发布,尽量别改字段名或类型。非要改的话,考虑加新字段,别删旧的。
注意: 接口文件里不能有中文注释!ROS2 的接口解析器对中文支持不好,我吃过这个亏。注释请用英文。

2.7 知识体系结构图

下面这张图展示了本章的核心逻辑,从接口类型到创建流程,再到嵌套设计:

ROS2 自定义接口知识体系 msg(消息) 发布/订阅,单向 srv(服务) 请求/响应,双向 action(动作) 带反馈,可取消 创建接口包 → 编写 .msg/.srv/.action 文件 → 配置 CMakeLists.txt → colcon build 复杂嵌套类型设计 复用标准类型(geometry_msgs 等) + 自定义类型数组 在 CMakeLists.txt 中声明 DEPENDENCIES 最佳实践 命名清晰 · 类型选对 · 不过度设计 · 版本兼容 · 避免中文注释

2.8 避坑指南

最后,分享几个我踩过的坑:

  • 字段类型写错:比如把 float64 写成 float,编译直接报错。ROS2 里浮点数只有 float32 和 float64,没有 float。
  • 忘记 source:编译完接口包,一定要 source install/setup.bash,否则其他包找不到接口。我经常忘了这步,然后一脸懵逼地查半天。
  • 接口包名别用下划线开头:比如 _my_interfaces,ROS2 会忽略这种包。我试过,编译通过了但就是找不到接口。
  • action 文件格式:三段之间用 --- 隔开,每段前后不能有多余空行。我遇到过因为多了一个空行,action 编译失败的情况。

嗯,接口定义这块,说白了就是“磨刀不误砍柴工”。花点时间把接口设计好,后面写节点代码会顺畅很多。下一节咱们就开始写真正的节点代码了,到时候你会感谢现在认真设计接口的自己。