3. MAVLink XML 定义文件:从语法到代码生成

好,咱们进入第三章。这一章我打算把 MAVLink 的“源代码”彻底讲透——也就是那些 .xml 文件。你想想看,MAVLink 所有消息的结构、字段、枚举,源头都在这里。不懂 XML 定义,你后面做二次开发就会很被动。

我个人习惯,拿到一个新协议栈,第一件事就是去翻它的 XML 定义。为什么?因为文档可能过时,但 XML 是活的。MAVLink 官方仓库里那几百个 .xml 文件,就是整个协议族的“宪法”。

3.1 XML 定义文件的结构

一个标准的 MAVLink XML 文件,长什么样?我直接给你看一个最简版本:

<?xml version="1.0"?>
<mavlink>
    <version>2</version>
    <dialect>1</dialect>
    
    <enums>
        <enum name="MAV_TYPE">
            <description>无人机/飞行器类型</description>
            <entry value="0" name="MAV_TYPE_GENERIC">
                <description>通用类型</description>
            </entry>
            <entry value="1" name="MAV_TYPE_FIXED_WING">
                <description>固定翼</description>
            </entry>
        </enum>
    </enums>
    
    <messages>
        <message id="0" name="HEARTBEAT">
            <description>心跳包,维持通信</description>
            <field type="uint8_t" name="type" enum="MAV_TYPE">飞行器类型</field>
            <field type="uint8_t" name="autopilot">自动驾驶仪类型</field>
            <field type="uint8_t" name="base_mode">系统基础模式</field>
            <field type="uint32_t" name="custom_mode">自定义模式</field>
            <field type="uint8_t" name="system_status">系统状态</field>
            <field type="uint8_t" name="mavlink_version">MAVLink 版本</field>
        </message>
    </messages>
</mavlink>

嗯,这里要注意几个关键点:

  • <mavlink> 是根元素,所有内容都包在里面
  • <version> 指定 MAVLink 协议版本(1 或 2)
  • <dialect> 是方言版本号,你自己扩展时记得递增
  • <enums> 定义枚举类型,比如飞行器类型、模式等
  • <messages> 定义消息,每个消息有唯一的 id

核心原则:消息 ID 必须全局唯一。MAVLink 官方预留了 0-255 给通用消息,256-65535 给自定义扩展。我建议你自定义消息从 1000 以后开始,避免和未来官方版本冲突。

3.2 消息定义语法详解

咱们拆开来看。一个消息定义包含三部分:消息头、字段列表、可选扩展。

3.2.1 消息头

消息头就是 <message id="X" name="XXX"> 这一行。id 决定了消息在总线上的优先级——id 越小,优先级越高。HEARTBEAT 用 id=0,就是因为它必须被优先处理。

3.2.2 字段定义

每个字段由 <field> 标签定义,核心属性就三个:

属性 说明 示例
type 数据类型,支持 uint8_t、int16_t、float、char[10] 等 type="float"
name 字段名,建议用下划线命名法 name="roll_angle"
enum(可选) 关联的枚举类型,自动生成代码时会做类型映射 enum="MAV_MODE"

数组怎么定义?很简单:type="float[3]" 表示三个 float 组成的数组。我在做姿态解算时,经常用 float[4] 来传四元数。

小技巧:字段顺序会影响内存对齐。把相同大小的字段排在一起,能减少填充字节。比如所有 uint8_t 放一起,再放 uint16_t,最后放 float。这样生成的 C 结构体更紧凑。

3.2.3 枚举定义

枚举用 <enum> 标签,每个枚举值用 <entry> 定义。这里有个坑——我曾经在项目里遇到过一个 bug,就是因为两个枚举值用了相同的数字。MAVLink 不会报错,但解析时会出问题。

<enum name="MAV_STATE">
    <description>系统状态枚举</description>
    <entry value="0" name="MAV_STATE_UNINIT">
        <description>未初始化</description>
    </entry>
    <entry value="1" name="MAV_STATE_BOOT">
        <description>启动中</description>
    </entry>
    <entry value="2" name="MAV_STATE_CALIBRATING">
        <description>校准中</description>
    </entry>
    <entry value="3" name="MAV_STATE_STANDBY">
        <description>待命</description>
    </entry>
    <entry value="4" name="MAV_STATE_ACTIVE">
        <description>激活</description>
    </entry>
</enum>

3.3 自定义消息的 XML 编写

好,重头戏来了。假设我们要给一个农业无人机定义一个“喷洒状态”消息,包含喷洒流量、剩余药量、喷嘴压力。怎么做?

第一步,确定消息 ID。我选 1001,避开官方范围。

第二步,设计字段。流量用 float,药量用 uint16_t(单位毫升),压力用 float。

第三步,写 XML:

<message id="1001" name="SPRAY_STATUS">
    <description>农业无人机喷洒状态</description>
    <field type="float" name="flow_rate">喷洒流量,单位 L/min</field>
    <field type="uint16_t" name="remaining_volume">剩余药量,单位 mL</field>
    <field type="float" name="nozzle_pressure">喷嘴压力,单位 bar</field>
    <field type="uint8_t" name="spray_mode" enum="SPRAY_MODE">喷洒模式</field>
    <field type="uint8_t" name="pump_status">水泵状态:0-停止,1-运行,2-故障</field>
</message>

等等,这里用到了 SPRAY_MODE 枚举,我们得先定义它:

<enum name="SPRAY_MODE">
    <description>喷洒模式</description>
    <entry value="0" name="SPRAY_MODE_IDLE">空闲</entry>
    <entry value="1" name="SPRAY_MODE_CONTINUOUS">连续喷洒</entry>
    <entry value="2" name="SPRAY_MODE_INTERMITTENT">间歇喷洒</entry>
    <entry value="3" name="SPRAY_MODE_PRECISION">精准喷洒</entry>
</enum>

注意:自定义枚举的命名不要和官方枚举冲突。我习惯在自定义枚举前加项目缩写,比如 AG_SPRAY_MODE,这样一眼就能看出是农业项目的扩展。

3.4 使用 mavgen 生成代码

XML 写好了,怎么变成能用的 C 或 Python 代码?mavgen 就是干这个的。

mavgen 是 MAVLink 官方提供的代码生成工具,支持 C、Python、C#、Java 等多种语言。我个人最常用 C 和 Python 两个目标。

3.4.1 安装 mavgen

mavgen 是 MAVLink 工具链的一部分,通过 pip 安装:

pip install mavlink

或者从源码安装:

git clone https://github.com/mavlink/mavlink.git
cd mavlink
python setup.py install

3.4.2 生成 C 代码

假设你的自定义 XML 文件叫 agriculture.xml,生成 C 代码的命令:

python -m mavgenerate -o output_dir --lang C --xml agriculture.xml

这会生成一堆文件:

  • mavlink.h — 主头文件,包含所有消息定义
  • agriculture.h — 你的方言头文件
  • mavlink_helpers.h — 编解码辅助函数

我记得第一次用 mavgen 时,没注意输出目录,结果文件散落一地。后来我习惯建一个 generated/ 目录专门放生成代码。

3.4.3 生成 Python 代码

Python 版本更简单:

python -m mavgenerate -o output_dir --lang Python --xml agriculture.xml

生成的是一个 Python 包,里面每个消息都是一个类。用起来很直观:

from pymavlink.dialects.v20 import agriculture as dialect

# 创建喷洒状态消息
msg = dialect.SPRAY_STATUS()
msg.flow_rate = 2.5
msg.remaining_volume = 10000
msg.nozzle_pressure = 3.0
msg.spray_mode = dialect.SPRAY_MODE_CONTINUOUS
msg.pump_status = 1

# 序列化
buf = msg.pack()
print(f"序列化后长度: {len(buf)} 字节")

3.4.4 mavgen 的常用选项

选项 说明 示例
--lang 目标语言,支持 C、Python、C#、Java、JavaScript --lang C
--wire-protocol 协议版本,1.0 或 2.0,默认 2.0 --wire-protocol 2.0
--validate 生成前验证 XML 语法 --validate
--append 追加模式,不覆盖已有文件 --append

我的习惯:每次生成前都加 --validate 选项。XML 语法错误很隐蔽,少个尖括号或者字段名拼错,mavgen 不会报错,但生成的代码会有问题。提前验证能省很多调试时间。

3.5 从 XML 到代码的完整流程

说了这么多,咱们用一张图把整个流程串起来:

MAVLink XML 定义到代码生成流程 XML 定义文件 mavgen 代码生成器 C 头文件/源文件 Python 包 C#/Java/JS 等 嵌入式固件集成 地面站/分析工具 Web 前端/可视化 💡 核心思路: XML 定义是“一次编写,多语言生成”。你只需要维护 XML 文件,mavgen 帮你搞定所有语言的编解码代码。

你看,整个流程就是:写 XML → 跑 mavgen → 得到各语言代码 → 集成到你的项目里。一次定义,到处使用。

3.6 避坑指南

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

  • 字段顺序影响兼容性:同一个消息,不同版本的 XML 如果字段顺序变了,新旧设备通信会解析错乱。所以消息一旦发布,字段顺序就别改了。
  • 枚举值不要重复:我之前说过,mavgen 不会检查枚举值重复。你手动写两个 entry 用同一个 value,生成的代码里会有两个同名常量,编译直接报错。
  • XML 编码必须是 UTF-8:有一次我从 Windows 复制了一个 XML 文件到 Linux,没注意编码是 GBK,mavgen 解析到中文字段描述时直接崩溃。后来我所有 XML 文件第一行都写死 <?xml version="1.0" encoding="UTF-8"?>。
  • 消息 ID 不要用 0-255:这是官方保留范围。我曾经图省事用了 50,结果和 HEARTBEAT 冲突了,调试了一下午才发现。

嗯,这一章的内容就这些。XML 定义是 MAVLink 二次开发的基石,花点时间把语法吃透,后面写自定义协议会非常顺手。