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,所有自定义接口都放里面。这样其他包依赖它就行,不用到处找。
创建步骤:
- 在工作空间 src 目录下,用
ros2 pkg create创建包,记得加--build-type ament_cmake。 - 在包目录下创建
msg、srv、action三个文件夹。 - 把定义好的接口文件放进去。
- 修改
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 等,能用现成的就别自己造。实在不够用,再定义自己的。
geometry_msgs,就要在 rosidl_generate_interfaces 的 DEPENDENCIES 里加上它。否则编译会报找不到类型。
2.6 接口设计的最佳实践
做接口设计,说白了就是定协议。协议定得好,后面开发就顺畅。我总结了几条经验:
- 字段命名要清晰:别用
a、b、data1这种名字。用target_pose、battery_level,一看就懂。 - 类型选对:整数用
int32还是uint8?时间戳用builtin_interfaces/Time还是std_msgs/Header?想清楚再定。 - 不要过度设计:接口不是越复杂越好。够用就行,后面可以扩展。我见过有人一个消息里塞了 50 个字段,结果大部分都用不上。
- 版本兼容:接口一旦发布,尽量别改字段名或类型。非要改的话,考虑加新字段,别删旧的。
2.7 知识体系结构图
下面这张图展示了本章的核心逻辑,从接口类型到创建流程,再到嵌套设计:
2.8 避坑指南
最后,分享几个我踩过的坑:
- 字段类型写错:比如把
float64写成float,编译直接报错。ROS2 里浮点数只有float32和float64,没有float。 - 忘记 source:编译完接口包,一定要
source install/setup.bash,否则其他包找不到接口。我经常忘了这步,然后一脸懵逼地查半天。 - 接口包名别用下划线开头:比如
_my_interfaces,ROS2 会忽略这种包。我试过,编译通过了但就是找不到接口。 - action 文件格式:三段之间用
---隔开,每段前后不能有多余空行。我遇到过因为多了一个空行,action 编译失败的情况。
嗯,接口定义这块,说白了就是“磨刀不误砍柴工”。花点时间把接口设计好,后面写节点代码会顺畅很多。下一节咱们就开始写真正的节点代码了,到时候你会感谢现在认真设计接口的自己。