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 时间间隔

这里有个坑,我当年踩过——bytechar 在 ROS2 里已经废弃了。别再用它们,直接用 int8uint8 就行。

注意:ROS2 没有 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

常量用全大写加下划线,这是约定俗成的写法。常量在代码生成后,会变成对应语言的静态常量,用起来很方便。

小技巧:常量定义必须放在消息字段之前。ROS2 的解析器是按顺序处理的,如果你把常量混在字段中间,可能会解析出错。我习惯把常量放在文件最前面,用空行隔开。

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 搞懂,服务就学了一半了。