بدون توضیح

LiuYang ea0930498d Automate VKFly dialect regeneration and staleness check 1 هفته پیش
doc 1715e7887b Add platform command forwarding callbacks 3 هفته پیش
motian_bridge ea0930498d Automate VKFly dialect regeneration and staleness check 1 هفته پیش
msg_definitions @ 6738b07dcf ea0930498d Automate VKFly dialect regeneration and staleness check 1 هفته پیش
tests ea0930498d Automate VKFly dialect regeneration and staleness check 1 هفته پیش
tools ea0930498d Automate VKFly dialect regeneration and staleness check 1 هفته پیش
.gitignore 89b33e5557 Add MQTT MAVLink bridge with dialect submodule 3 هفته پیش
.gitmodules 89b33e5557 Add MQTT MAVLink bridge with dialect submodule 3 هفته پیش
AGENTS.md 679bef9fad Add per-aircraft state object and automatic parameter sync 3 هفته پیش
README.md ea0930498d Automate VKFly dialect regeneration and staleness check 1 هفته پیش
config.example.json 679bef9fad Add per-aircraft state object and automatic parameter sync 3 هفته پیش
pyproject.toml 89b33e5557 Add MQTT MAVLink bridge with dialect submodule 3 هفته پیش

README.md

摩天云 MQTT–MAVLink 串口桥

使用子模块 msg_definitions/msg_definitions/VKFly.xml 生成的 MAVLink 2 方言解析串口数据(也接受 MAVLink 1 通用帧),按平台 Base JSON 协议上报核心遥测;不执行文档中的飞行业务指令。现有原始字节块透传保持不变:飞控串口读到的每个字节块转换为大写 HEX,包装为 DataType=526 发布到 /PUBLISH/<SystemID>;订阅 /OBTAIN/<SystemID>,把同类型消息的 HEX Data 解码后原样写入串口。原始读取块不等于 MAVLink 帧边界。

结构化上报

均发布到 /RECEIVE/<SystemID>,使用 DataType、UAVType、Data、SystemID、带时区的 TimeStamp 外壳:

DataType 来源 行为
258 飞行状态 HEARTBEAT、ATTITUDE、GLOBAL_POSITION_INT、VFR_HUD、GPS_RAW_INT、HOME_POSITION 心跳有效时每秒合并一次;未知字段不发送
257 电池 仅 VK_BMS_STATUS 有新值时至多每秒一次;bat_id=0/1 分别映射左右电池
260 提示 STATUSTEXT 收到就上报,保留原始严重级别和 UTF-8 文本
274 飞行状态 HEARTBEAT 解锁标志 首次获知及改变时上报布尔值;true=解锁、false=上锁,并非实际离地/着陆

只解析首个有效飞控 HEARTBEAT 对应的 system/component ID。串口重连重选来源并清空缓存;心跳超过 5 秒暂停 #258,其他来源字段超过 5 秒自动剔除。除下面所述的参数表自动同步外,只被动接收,不请求飞控遥测;如缺少字段,请先确认飞控串口是否广播对应消息。缺失消息类型可用 --debug 查看。#257 在双电池时不假定并联/串联或容量一致,不填写推算的总电压/电量。

参数表自动同步

锁定飞控 HEARTBEAT 后,默认会主动发送一次 PARAM_REQUEST_LIST,把飞控回传的 PARAM_VALUE 累积到本机该飞机的参数表(仅存内存,不上报、不落盘)。param_request_timeout 秒内未收齐时按 param_request_max_attempts 重试(默认 15 秒、最多 3 次),收齐或放弃后停止。串口重连视为换了飞控,会重新同步一次。将 request_params_on_start 设为 false 可关闭该行为,回到完全被动接收。#282 中需要读取现值的参数(如 FS_CONF_A)会用到这张表;平台下发的 #281 复用同一套请求逻辑,不受该开关影响。

安装和运行

python3 -m venv .venv
.venv/bin/python -m pip install -e .
cp config.example.json config.local.json
# 修改 config.local.json:真实 SystemID、平台分配的 UAVType、串口及 MQTT 参数
export MOTIAN_MQTT_PASSWORD='由部署环境提供的密码'
.venv/bin/motian-mavlink-bridge --config config.local.json

config.local.json 已被忽略,不要提交凭据。无账号时将 mqtt_username 和 mqtt_password_env 均设为 null。mqtt_tls=true 使用系统可信 CA 或 mqtt_ca_file;不要关闭证书校验。serial_port 可以是 /dev/tty* 或 pyserial 支持的 URL。配置文件中的 uav_type: 0 只是示例,正式值须由平台分配。

publish_raw_mavlink 默认为 true,设置为 false 可停止 /PUBLISH/ 原始上行,下行 #526 不受影响。下行 JSON 必须有 DataType(没有尾随空格)、UAVType、Data、SystemID、TimeStamp,设备标识须与本机配置一致。Data 必须是非空、偶数长度的 HEX 字符串,最多 max_command_bytes 字节。其他 DataType 默认只记日志并丢弃;不会伪造 ACK。所有 retained 下行均丢弃,MQTT 使用 QoS 0、不保留发布消息。MQTT 或串口断线期间不主动缓存数据;重连后不会主动重放旧指令。

扩展飞控业务时,通过 Bridge.register_handler(data_type, handler) 注册非 526 命令回调。回调收到 BaseMessage 和桥实例,可调用 bridge.publish_report(data_type, data) 向 /RECEIVE/<SystemID> 上报。透传上行通过同一方法的 passthrough=True 发布到 /PUBLISH/<SystemID>。回调应自行处理飞控交互和业务 ACK,不要把未实现的命令视为成功。

已实现的平台控制指令

Bridge 在收到飞控 HEARTBEAT 并锁定目标 system/component 后,支持把以下 /OBTAIN/<SystemID> 指令转换为 VKFly MAVLink:

DataType 平台指令 MAVLink
281 获取飞机底层参数 PARAM_REQUEST_LIST
282 配置飞机底层参数 PARAM_SET,按下面的参数映射发送
2050 无人机解锁 当前按平台文档实现:Data=false 发送解锁;Data=true 暂留 TODO,不写串口
2054 无人机起飞 COMMAND_LONG / MAV_CMD_NAV_TAKEOFF,高度 NaN 表示使用飞控默认值
2055 无人机降落 COMMAND_LONG / MAV_CMD_NAV_LAND
2070 指点飞行 COMMAND_INT / MAV_CMD_DO_REPOSITION,相对高度坐标系
2073 高空紧急停桨(停桨后开伞) COMMAND_LONG / MAV_CMD_DO_PARACHUTE,param1=PARACHUTE_RELEASE("Release parachute and kill motors")
2074 地面紧急停桨(停桨后不开伞) COMMAND_LONG / MAV_CMD_COMPONENT_ARM_DISARM,param1=0(上锁)、param2=21196(强制),不触发开伞

#2073 的映射采用飞控通用的 MAV_CMD_DO_PARACHUTE/PARACHUTE_RELEASE(开伞并停桨);#2074 采用强制上锁 MAV_CMD_COMPONENT_ARM_DISARM(param2=21196)停桨而不开伞,避免 MAV_CMD_DO_FLIGHTTERMINATION 在多旋翼上可能触发的开伞。若 VKFly 有专用时序命令,需与飞控方确认后调整。

#282 已确认的参数映射:

平台参数名 飞控 param_id
battery_low_threshold VCAP1_LOW_VAL
battery_critical_threshold VCAP2_LOW_VAL
height_limit ALT_LIM_UP1
distance_limit MAX_HOR_DIST
return_height RTL_ALT_M
return_speed MC_XY_CRUISE
obstacle_avoidance_enable OBAVOID_ACT,开=悬停避障、关=关闭
obstacle_avoidance_switch OBAVOID_ACT,与上项相同
obstacle_avoidance_distance OBAVOID_DIST
return_type RTL_EXEC_MODE,直线/正序/逆序分别为 0/1/2
precision_land_mode RTL_PLD_EN,关闭/二维码引导分别为 0/1
low_battery_action 修改 FS_CONF_A 的低两级电量保护位;需先获得当前 FS_CONF_A(参数表自动同步或执行 #281 后均可),以保留其他保护位

以下参数映射尚未由飞控协议明确,代码仅记录 TODO 并拒绝写串口:battery_emergency_threshold、geofence_action、visual_sensor_enable、april_tag_id、data_link_loss_action、rc_loss_action、parachute_safety_enable、four_g_lost_action、rtk_switch。未知参数同样不会直接作为 param_id 发送。

这些业务指令不会在尚未收到飞控 HEARTBEAT 时发送,以避免使用错误的目标 system/component。当前只负责下发帧;COMMAND_ACK 到平台业务应答的关联与超时重试尚未实现。PARAM_VALUE 会缓存在本机飞机对象的参数表中,但不向平台转发。

没有远程平台时的串口实测

可以先在本机启动一个临时 MQTT Broker,把它当作平台。桥接程序不需要远程平台才能解析串口和产生状态报文。

1. 确认飞控串口

插入飞控后查看设备名:

ls -l /dev/serial/by-id/
# 或
dmesg | tail -30

优先使用 /dev/serial/by-id/...,不要直接依赖会变化的 /dev/ttyUSB0。确认串口波特率与飞控配置一致。不要同时让其他地面站占用同一个串口。

2. 安装并启动本地 Broker

项目提供了基于 Python aMQTT 的本地测试脚本。aMQTT 是开发/测试依赖,不属于桥接服务的生产依赖:

.venv/bin/python -m pip install "amqtt>=0.12,<0.13"
.venv/bin/python tools/local_mqtt_broker.py

脚本默认监听 127.0.0.1:1883,无认证、无 TLS,仅供本机测试。不要把它暴露到公网。如果 1883 已占用,可以执行:

.venv/bin/python tools/local_mqtt_broker.py --port 1884

并同步修改桥接配置中的 mqtt_port。也可以使用 Mosquitto 或其他兼容 MQTT 3.1.1 的 Broker。

3. 准备本地测试配置

复制示例配置后,至少修改以下字段:

cp config.example.json config.local.json
{
  "system_id": "UAV000001",
  "uav_type": 0,
  "serial_port": "/dev/serial/by-id/你的飞控串口",
  "serial_baudrate": 57600,
  "mqtt_host": "127.0.0.1",
  "mqtt_port": 1883,
  "mqtt_client_id": "local-motian-test",
  "mqtt_username": null,
  "mqtt_password_env": null,
  "mqtt_tls": false,
  "publish_raw_mavlink": true,
  "request_params_on_start": true
}

配置文件中未列出的选项会使用默认值。uav_type 在没有平台分配值时可以先使用 0 做本地联调;正式接入平台时必须换成平台分配值。

4. 启动桥接服务

.venv/bin/motian-mavlink-bridge --config config.local.json --debug

看到 serial connected 后,保持该终端运行。程序会被动解析串口上已有的 MAVLink 消息:它不会主动向飞控请求消息流。如果日志出现 missing/stale MAVLink telemetry,说明飞控没有在这个串口广播对应消息,需在飞控端打开相应 MAVLink stream 或换用正确的串口。

5. 另开终端观察 MQTT 报文

观察结构化状态:

mosquitto_sub -h 127.0.0.1 -p 1883 \
  -t '/RECEIVE/UAV000001' -v

观察原始 MAVLink HEX 透传:

mosquitto_sub -h 127.0.0.1 -p 1883 \
  -t '/PUBLISH/UAV000001' -v

预期在 /RECEIVE/UAV000001 看到:

  • DataType: 258:每秒一条左右;至少收到 HEARTBEAT 后就可能出现,位置、姿态等字段取决于飞控是否发送对应消息;
  • DataType: 257:只有收到厂商 VK_BMS_STATUS 才会出现;
  • DataType: 260:飞控发送 STATUSTEXT 时出现;
  • DataType: 274:首次收到 HEARTBEAT 以及解锁状态改变时出现,true 表示解锁、false 表示上锁;
  • /PUBLISH/... 中的 DataType: 526:原始串口读取块的 HEX 内容。

如果本机没有 mosquitto_sub,可以使用 aMQTT 自带的 amqtt_sub:

.venv/bin/amqtt_sub --url mqtt://127.0.0.1:1883 \
  -t '/RECEIVE/UAV000001'

也可以使用任意 MQTT 客户端订阅这两个主题。桥接程序发布 QoS 0 且不保留消息,因此订阅端要先启动再启动桥接程序,避免错过实时数据。

6. 建议的安全测试顺序

  1. 先只接收观察,确认 HEARTBEAT、位置和姿态数据正常;
  2. 确认 DataType=258 的字段单位和数值符合预期;
  3. 再测试解锁状态变化,确认 #274;
  4. 确认无误后才测试下行 #526。下行会直接写飞控串口,严禁在桨叶安装、电机通电或飞行状态下盲目发送控制帧;
  5. 测试结束后停止桥接程序和临时 Broker。

本地检查

.venv/bin/python -m unittest discover -s tests -v

方言源文件由 msg_definitions Git 子模块提供,实际 XML 位于 msg_definitions/msg_definitions/。motian_bridge/dialects/v20/VKFly.py 是生成文件,不要手工修改。

更新方言并重新生成:

# 1. 拉取最新方言
git -C msg_definitions pull origin main

# 2. 重新生成(脚本内部仍是调用 pymavlink 的 mavgen)
.venv/bin/python tools/gen_dialect.py

# 3. 验证并跑测试
.venv/bin/python tools/check_dialect.py
.venv/bin/python -m unittest discover -s tests -v

# 4. 一起提交子模块指针与生成文件
git add msg_definitions motian_bridge/dialects/v20/VKFly.py
git commit -m 'Regenerate VKFly dialect from submodule'

tools/gen_dialect.py 会先检查 XML 是否齐全(未初始化子模块时给出明确提示),再在临时目录生成、与现有文件比对,只有内容变化时才覆盖,因此可以安全地反复执行。常用选项:

命令 作用
tools/gen_dialect.py 按需重新生成
tools/gen_dialect.py --dry-run 只报告是否需要重新生成,不写文件
tools/gen_dialect.py --check 校验已提交文件是否与 XML 一致,不一致时退出码非 0
tools/check_dialect.py 同上,供 pre-commit / CI 调用
--no-validate 跳过 mavgen 的 XML 校验,可忽略上游 bitmask 告警

也可以直接调用 mavgen(注意 --output 要写完整文件名,写目录名会生成 v20.py 从而破坏导入):

.venv/bin/python -m pymavlink.tools.mavgen --lang Python --wire-protocol 2.0 \
    --output motian_bridge/dialects/v20/VKFly.py msg_definitions/msg_definitions/VKFly.xml

建议把 CI 接入 --check,并在本地安装 pre-commit 钩子:

.venv/bin/python tools/check_dialect.py || \
  echo 'VKFly.py 已过期,请运行 .venv/bin/python tools/gen_dialect.py'

tests/test_dialect_sync.py 也会在单元测试里做同样的过期检查,所以提交前跑一次 unittest 就能发现忘记重新生成的情况。当前生成文件使用 pymavlink 2.4.50;生成时会显示 XML bitmask 枚举的已有警告,但会完成生成;换 pymavlink 版本后生成的字节内容可能不同,应一并提交并复测真实 MAVLink 帧。

协议仅定义 /PUBLISH/无人机id 为“透传包”,没有提供上行载荷示例。本实现按 Base JSON + DataType 526 + HEX Data 约定,正式联调前需平台确认。TimeStamp 为本机时区 ISO-8601 时间;应确保设备时间正确。MQTT QoS 0 降低重放风险,但不是严格端到端交付保证。此服务不做飞行安全仲裁,正式部署应限制 Broker 访问权限。