4、定义.msg文件:msg文件语法规则、基本数据类型、数组与嵌套消息
好,咱们接着往下走。上一章我们搭好了自定义消息的工程架子,这一章就来填里面的肉——也就是怎么写 .msg 文件。
说实话,我刚接触 ROS2 时,觉得 .msg 文件不就是定义几个字段嘛,有啥好学的?结果第一次写复杂消息时,嵌套结构就把我绕晕了。嗯,咱们今天就把这块彻底讲透。
4.1 msg文件长什么样?
一个 .msg 文件,说白了就是一个纯文本文件。每一行定义一个字段。格式极其简单:
字段类型 字段名称
比如:
int32 id
string name
float64 speed
这就定义了一条包含三个字段的消息。我习惯把字段名写成小写加下划线,这是 ROS 社区的惯例,你最好也这么干。
mySpeed。ROS2 的代码生成器虽然能处理,但后续和其他工具链配合时容易出问题。老老实实用 my_speed。
4.2 基本数据类型
ROS2 支持的数据类型比 ROS1 更规整。我整理了一张表,你写的时候直接查就行:
| 类型 | C++ 对应 | 说明 |
|---|---|---|
bool |
bool |
布尔值,true/false |
int8 |
int8_t |
有符号8位整数 |
uint8 |
uint8_t |
无符号8位整数 |
int16 |
int16_t |
有符号16位整数 |
uint16 |
uint16_t |
无符号16位整数 |
int32 |
int32_t |
有符号32位整数 |
uint32 |
uint32_t |
无符号32位整数 |
int64 |
int64_t |
有符号64位整数 |
uint64 |
uint64_t |
无符号64位整数 |
float32 |
float |
32位浮点数 |
float64 |
double |
64位浮点数 |
string |
std::string |
字符串 |
time |
builtin_interfaces::Time |
时间戳 |
duration |
builtin_interfaces::Duration |
时间间隔 |
这里有个坑,我当年踩过——byte 和 char 在 ROS2 里已经废弃了。别再用它们,直接用 int8 或 uint8 就行。
double 这个类型名,你得用 float64。我第一次从 ROS1 迁移时,写了个 double value,编译直接报错。别学我。
4.3 数组类型
数组在 ROS2 里用得很频繁。语法就是在基本类型后面加个方括号:
int32[] numbers # 可变长度数组
int32[5] fixed_numbers # 固定长度数组(5个元素)
string[] names # 字符串数组
float32[3] point # 三维坐标点
我个人习惯,能用固定长度就用固定长度。为什么?因为固定长度数组在序列化时效率更高,内存分配也更可控。我在做激光雷达数据解析时,点云数据用固定长度数组,性能提升很明显。
可变长度数组也不是不能用,只是你要知道:它会在运行时动态分配内存。如果你的消息发布频率很高,比如 100Hz,那动态分配的开销就不可忽视了。
int32[3] point = [0, 0, 0]。不过 ROS2 对默认值的支持还在完善中,有些版本可能不兼容,建议你查一下当前版本的文档。
4.4 嵌套消息
这才是重头戏。嵌套消息,说白了就是一个消息里包含另一个消息。比如你要描述一个机器人,它有一个头部信息和一个位置信息:
# 先定义 Header.msg
builtin_interfaces/Time stamp
string frame_id
# 再定义 Pose.msg
float64 x
float64 y
float64 theta
# 最后定义 RobotStatus.msg
Header header
Pose pose
string robot_name
注意看,嵌套消息的写法就是 包名/消息名。如果是在同一个包内,包名可以省略。但我建议你每次都写全,这样别人看你的 .msg 文件时,一眼就知道消息来自哪里。
我曾经在一个项目里,团队有人省略了包名,结果后来重构时,消息文件挪了位置,编译直接炸了。从那以后,我要求所有嵌套引用都写全路径。
4.5 常量定义
有时候你需要在消息里定义一些常量,比如状态码:
# 定义常量
uint8 STATE_IDLE = 0
uint8 STATE_RUNNING = 1
uint8 STATE_ERROR = 2
# 消息字段
uint8 current_state
string error_message
常量用全大写加下划线,这是约定俗成的写法。常量在代码生成后,会变成对应语言的静态常量,用起来很方便。
4.6 注释与空行
ROS2 的 .msg 文件支持注释,用 # 开头:
# 这是注释
int32 id # 行尾注释也可以
空行会被忽略,你可以用空行来分组,让文件更易读。我个人习惯把相关的字段放在一起,用空行隔开不同的逻辑组。
4.7 一个完整的例子
咱们把今天讲的东西串起来,写一个完整的 .msg 文件:
# 文件: BatteryStatus.msg
# 电池状态消息
# 常量定义
uint8 BATTERY_OK = 0
uint8 BATTERY_LOW = 1
uint8 BATTERY_CRITICAL = 2
# 头部信息
std_msgs/Header header
# 电池数据
float32 voltage
float32 current
float32 temperature
float32 charge_percentage
# 状态
uint8 battery_status
# 历史数据(可变长度数组)
float32[] voltage_history
你看,这个文件里包含了常量、基本类型、嵌套消息、数组。写完之后,放到 msg/ 目录下,编译一下,代码就自动生成了。
- 字段类型和字段名之间用空格隔开
- 嵌套消息用
包名/消息名格式 - 数组用方括号,固定长度加数字
- 常量用全大写,放在文件最前面
- 注释用
#,空行随意
嗯,这一章的内容就到这。下一章咱们会讲 .srv 文件,也就是服务接口的定义。到时候你会发现,服务其实就是请求和响应两个消息拼在一起,所以把 .msg 搞懂,服务就学了一半了。