4、MAVLink XML 定义语言:消息定义文件的结构与代码生成

好,我们进入第四章。这一章要聊的是 MAVLink 的“骨架”——XML 定义语言。

说白了,MAVLink 里所有的消息、枚举、命令,都不是凭空写死在代码里的。它们都源自一套 XML 文件。这套文件定义了“无人机能发什么消息”、“每个消息里装什么数据”。然后,通过一个叫 mavgen 的工具,自动生成 C、Python 等语言的代码。

我刚开始接触 MAVLink 时,也想过直接手写消息结构体。后来发现,那简直是给自己挖坑。XML 定义 + 代码生成,才是正规军的路子。你想想看,维护一套 XML,就能同步更新所有语言的代码,多省心。

4.1 消息定义文件的结构

MAVLink 的 XML 定义文件,通常以 .xml 结尾。它们放在 MAVLink 仓库的 message_definitions 目录下。比如 common.xml、ardupilotmega.xml,这些都是标准定义。

一个典型的 XML 文件,结构很清晰。我把它拆成几个部分:

  • 文件头:包含 XML 版本、编码、以及 MAVLink 的命名空间。
  • 版本与包含:声明协议版本,并可以 include 其他 XML 文件。比如 common.xml 会被很多厂商的 XML 文件引用。
  • 枚举类型:定义各种常量,比如 MAV_CMD、MAV_STATE。
  • 消息定义:核心部分。每个 <message> 标签定义一条 MAVLink 消息。

来看一个简化版的例子:

<?xml version="1.0"?>
<mavlink>
    <include>common.xml</include>
    <version>2</version>

    <enums>
        <enum name="MY_CUSTOM_STATE">
            <description>自定义飞行器状态</description>
            <entry value="0" name="MY_STATE_IDLE">
                <description>空闲</description>
            </entry>
            <entry value="1" name="MY_STATE_ACTIVE">
                <description>激活</description>
            </entry>
        </enum>
    </enums>

    <messages>
        <message id="150" name="MY_CUSTOM_MESSAGE">
            <description>一条自定义消息</description>
            <field type="uint8_t" name="custom_value">自定义数值</field>
            <field type="float" name="custom_float">自定义浮点数</field>
        </message>
    </messages>
</mavlink>

嗯,这里要注意:id 字段不能冲突。MAVLink 协议里,消息 ID 是唯一的。标准消息占用了 0-255 的范围,自定义消息建议从 256 以后开始。我曾经因为偷懒,直接用了 50,结果和标准消息冲突,调试了一整天。

4.2 枚举类型与消息定义

枚举类型,说白了就是给一堆数字起个名字。比如 MAV_CMD_NAV_WAYPOINT 等于 16。这样代码里写 MAV_CMD_NAV_WAYPOINT,比直接写 16 好理解多了。

定义枚举时,有几个坑:

  • 值必须唯一:同一个枚举里,不能有两个相同的值。
  • 使用 <description>:给每个枚举值和枚举本身加描述。生成的代码里,这些描述会变成注释。
  • 位掩码枚举:用 <enum bitmask="true"> 来定义。比如 MAV_MODE_FLAG,每个值是 2 的幂。

消息定义更直接。每个 <message> 包含:

属性/子标签 说明 示例
id 消息的唯一标识,0-255 标准,256+ 自定义 id="150"
name 消息名称,生成代码时变成结构体名 name="MY_CUSTOM_MESSAGE"
<description> 消息的说明文字 描述消息用途
<field> 定义字段,包含 type、name、description type="uint8_t" name="value"
<extensions> 用于 MAVLink 2 的扩展字段 兼容旧版本

我个人习惯,在定义消息时,尽量把相关的数据放在一条消息里。比如位置信息,就把经纬度、高度、速度都放一起。别拆成三四条消息,那样收发都麻烦。

4.3 使用 mavgen 生成 C/Python 代码

XML 定义好了,怎么变成能用的代码?用 mavgen。

mavgen 是 MAVLink 官方提供的代码生成器。它读取 XML 文件,输出 C、Python、C# 等语言的代码。我主要用 C 和 Python。

基本用法:

# 生成 C 语言代码
python -m mavgenerate --lang C --output ./generated_c --xml ./my_definitions.xml

# 生成 Python 代码
python -m mavgenerate --lang Python --output ./generated_python --xml ./my_definitions.xml

生成后,你会得到一堆文件。C 语言会生成 mavlink.h、mavlink_types.h 以及各个消息的 .h 文件。Python 会生成 mavlink.py 等模块。

小技巧:生成代码时,建议指定 --protocol 2.0。MAVLink 2 支持扩展字段和更长的消息 ID,兼容性更好。我现在的项目都直接用 MAVLink 2。

生成后的 C 代码,使用起来很简单:

#include "mavlink.h"

mavlink_message_t msg;
uint8_t buf[MAVLINK_MAX_PACKET_LEN];

// 打包消息
mavlink_msg_my_custom_message_pack(1, 200, &msg, custom_value, custom_float);

// 序列化到缓冲区
uint16_t len = mavlink_msg_to_send_buffer(buf, &msg);

// 发送 buf 到串口或网络

Python 代码更直接:

from pymavlink.dialects.v20 import my_definitions as mavlink

# 创建消息
msg = mavlink.MAVLink_my_custom_message_pack(
    system_id=1,
    component_id=200,
    custom_value=42,
    custom_float=3.14
)

# 编码为字节流
buf = msg.pack()

你看,几行代码就搞定了。不用自己手动拼字节流,也不用担心大小端问题。mavgen 都帮你处理好了。

4.4 自定义消息的创建与注册

标准消息不够用?那就自己造。自定义消息的流程,我总结为三步:

  1. 定义 XML:在自定义的 XML 文件里,添加 <message>,注意 ID 从 256 开始。
  2. 生成代码:用 mavgen 生成 C/Python 代码。
  3. 注册到系统:在飞控或地面站里,注册消息处理回调。

举个例子。假设我要定义一条“电池详细信息”的消息:

<message id="256" name="BATTERY_INFO_EXTENDED">
    <description>扩展电池信息,包含单节电芯电压</description>
    <field type="uint8_t" name="battery_id">电池编号</field>
    <field type="uint16_t[6]" name="cell_voltages">6节电芯电压,单位 mV</field>
    <field type="int16_t" name="temperature">电池温度,单位 0.1°C</field>
    <field type="uint8_t" name="state_of_health">健康度,百分比</field>
</message>

生成代码后,在飞控端注册处理函数:

// C 语言注册回调
mavlink_register_channel_handler(MAVLINK_COMM_0, 
    mavlink_msg_battery_info_extended_decode, 
    my_battery_callback);

void my_battery_callback(const mavlink_message_t *msg) {
    mavlink_battery_info_extended_t bat;
    mavlink_msg_battery_info_extended_decode(msg, &bat);
    // 处理电池数据
    printf("Cell 1: %d mV\n", bat.cell_voltages[0]);
}
注意:自定义消息的 ID 不要和标准消息冲突。标准消息列表可以在 common.xml 里查到。我曾经在项目里用了 ID 200,结果和 MISSION_ITEM 冲突,导致航点上传失败。后来统一从 256 开始,再没出过问题。

Python 端注册更简单:

def handle_battery(msg):
    print(f"Battery ID: {msg.battery_id}")
    print(f"Cell voltages: {msg.cell_voltages}")

# 注册回调
mavlink_connection.register_message_handler(
    handle_battery, 
    'BATTERY_INFO_EXTENDED'
)

嗯,这里要提醒一下:自定义消息只在你的系统内部有效。如果你要和别人的飞控通信,对方也得有同样的 XML 定义。所以,团队协作时,最好把自定义 XML 文件放在版本控制里,大家一起维护。

4.5 本章知识体系

为了让你更直观地理解本章内容,我画了一张图。它展示了 XML 定义、mavgen 生成、以及最终代码之间的关系。

MAVLink XML 定义与代码生成流程 MAVLink XML 定义与代码生成流程 XML 定义文件 枚举类型 消息定义 包含关系 mavgen C 语言代码 Python 代码 其他语言代码 说明: 1. XML 文件是唯一的数据源,所有代码都由它生成。 2. mavgen 支持多种语言输出,但核心逻辑一致。 3. 自定义消息时,只需修改 XML,重新生成即可。 4. 团队协作时,XML 文件应纳入版本控制。

这张图很直白。你只需要维护左边的 XML 文件,剩下的交给 mavgen。它帮你生成 C、Python 等语言的代码。这样,你就不用重复造轮子了。

核心要点:XML 定义是 MAVLink 的“宪法”。所有消息、枚举、命令,都从这里来。mavgen 是“立法委员”,把宪法翻译成各种语言。自定义消息时,记住 ID 从 256 开始,避免冲突。