Python MAVLink 通信实战详解:从飞控连接、遥测接收到任务下发

3 阅读15分钟

Python MAVLink 通信实战详解:从飞控连接、遥测接收到任务下发

很多开发者刚接触无人机协议开发时,最容易卡在“能连上、看得懂、发得出、控得稳”这四步。本文结合 UAV-Stack-Knowledge-Base 中的通信模板代码,用 Python + pymavlink 讲清 MAVLink 通信链路的核心流程,并给出可直接上手的实战示例。

Python MAVLink 通信实战详解:从飞控连接、遥测接收到任务下发

做无人机开发,很多人第一个坑不是算法,也不是飞控参数,而是通信链路根本没打通

我在和一些开发者、系统集成团队交流时,发现一个很常见的情况:大家知道 MAVLink 是事实上的通用协议,也知道 Python 很适合做原型验证、地面站工具和自动化测试,但真正开始写代码时,问题马上就来了:串口怎么连?UDP 和 TCP 该怎么选?心跳包为什么一直等不到?收到的纬度经度为什么看起来像“错误数据”?发送了解锁和起飞命令,飞控为什么没反应?

这些问题如果没有一份“能跑起来”的模板代码,排查成本会很高。本文我就结合开源项目 UAV-Stack-Knowledge-Base 里的 03-Protocols-Dev/04-通信模版代码.md,把一套典型的 Python MAVLink 通信流程拆开讲透:从环境准备、飞控连接、遥测接收,到控制命令、航点任务,再到日志下载。你可以把它理解成一篇“协议开发起步手册”,也可以把其中的代码直接拿去做自己的测试工具。


一、为什么通信模板代码对无人机项目这么关键

在无人机系统里,通信不是一个边缘模块,而是贯穿全链路的基础设施。

我们无论是在做:

  • 飞控联调
  • 地面站开发
  • 任务规划系统
  • 自动巡检平台
  • 航拍控制工具
  • 数据回传与日志分析

最终都绕不开一个核心问题:如何稳定地与飞控建立协议通信,并正确收发 MAVLink 消息。

很多项目在初期都喜欢“边查文档边写代码”,但 MAVLink 的消息类型、字段单位、状态机、任务上传流程都比较细,如果没有模板,很容易出现下面这些典型问题:

  1. 连接建立了,但不知道目标 system/component 是谁
  2. 遥测能收,但字段换算错了,导致界面显示异常
  3. 控制命令发出去了,但没有 ACK,系统状态不可观测
  4. 任务上传顺序写错,飞控直接拒绝 mission
  5. 日志下载流程没补全,最后拿不到可分析的数据

所以我一直认为,通信模板代码的价值不在“代码量”,而在于它提供了一个可重复、可验证、可扩展的最小通信闭环。

UAV-Stack-Knowledge-Base 这份模板,恰好就覆盖了最常用的几个环节:

  • 连接飞控
  • 接收遥测
  • 发送控制命令
  • 上传航点任务
  • 请求和下载日志

对于无人机开发者来说,这是非常好的起点。


二、先跑起来:Python + pymavlink 建立第一条 MAVLink 链路

如果你是第一次接触 Python 侧的 MAVLink 开发,最常见的选择就是 pymavlink。它本质上是 MAVLink 协议在 Python 生态中的核心工具库,适合做:

  • 协议调试
  • 自动化测试
  • 快速原型
  • 地面控制脚本
  • 日志抓取工具

1. 环境安装

先安装依赖:

pip install pymavlink

这一步很简单,但我建议你在单独的虚拟环境中执行,避免和已有项目的串口库、网络库冲突。

2. 建立连接

模板代码里给了三种典型连接方式:串口、UDP、TCP。

from pymavlink import mavutil

# 通过串口连接
master = mavutil.mavlink_connection('/dev/ttyUSB0', baud=57600)

# 或通过 UDP 连接
# master = mavutil.mavlink_connection('udpin:127.0.0.1:14550')

# 或通过 TCP 连接
# master = mavutil.mavlink_connection('tcp:127.0.0.1:5760')

# 等待心跳包
master.wait_heartbeat()
print(f"连接成功:系统={master.target_system}, 组件={master.target_component}")

3. 三种连接方式怎么选

这是很多人上来就会问的问题,我给一个实用判断:

串口连接

适合:

  • 直连数传模块
  • 连接机载伴随计算机
  • 接飞控 USB/Telemetry 口做调试

优点:

  • 真实链路,最接近实际部署
  • 延迟低,依赖少

问题点:

  • 设备名经常变化,如 /dev/ttyUSB0/dev/ttyACM0
  • 波特率必须和飞控一致
UDP 连接

适合:

  • 仿真环境(SITL)
  • 本地地面站联调
  • 多进程测试

优点:

  • 配置简单
  • 调试方便
  • 非常适合和 ArduPilot / PX4 仿真一起用
TCP 连接

适合:

  • 某些转发服务
  • 远程调试链路
  • 对连接状态有更明确要求的场景

优点:

  • 有连接语义,调试网络状态更直观

4. 心跳包为什么重要

wait_heartbeat() 不是“可有可无”的一步,而是整个通信初始化的关键。

MAVLink 网络里,心跳包相当于:

  • 对端在线声明
  • system/component 识别入口
  • 后续命令路由依据

没有等到心跳,你虽然“创建了连接对象”,但并不意味着已经拿到了一个可用的飞控会话

在工程实践里,我通常还会加一个超时控制,避免程序无限阻塞:

from pymavlink import mavutil

master = mavutil.mavlink_connection('udpin:127.0.0.1:14550')
heartbeat = master.wait_heartbeat(timeout=10)

if heartbeat is None:
    raise TimeoutError("10 秒内未收到心跳,请检查飞控输出或网络配置")

print(f"连接成功:system={master.target_system}, component={master.target_component}")

如果你是在做生产级工具,这个超时机制一定要有。


三、看懂遥测数据:不是“收到了”就算完成

打通链路之后,第二步不是立刻发控制命令,而是先验证你能不能正确理解飞控的遥测消息。

模板代码里监听了三类高频消息:

  • GLOBAL_POSITION_INT
  • ATTITUDE
  • SYS_STATUS

代码如下:

# 持续接收消息
while True:
    msg = master.recv_match(type=['GLOBAL_POSITION_INT', 'ATTITUDE', 'SYS_STATUS'], blocking=True)
    
    if msg.get_type() == 'GLOBAL_POSITION_INT':
        lat = msg.lat / 1e7
        lon = msg.lon / 1e7
        alt = msg.alt / 1000.0
        print(f"位置:{lat:.6f}, {lon:.6f}, 高度:{alt:.1f}m")
    
    elif msg.get_type() == 'ATTITUDE':
        roll = msg.roll
        pitch = msg.pitch
        yaw = msg.yaw
        print(f"姿态:横滚={roll:.2f}, 俯仰={pitch:.2f}, 偏航={yaw:.2f}")
    
    elif msg.get_type() == 'SYS_STATUS':
        battery_voltage = msg.voltage_battery / 1000.0
        battery_current = msg.current_battery / 100.0
        battery_remaining = msg.battery_remaining
        print(f"电池:{battery_voltage:.1f}V, {battery_current:.1f}A, {battery_remaining}%")

1. 遥测解析最容易错在“单位换算”

这是新手非常容易忽视的地方。

GLOBAL_POSITION_INT
  • lat / lon:整数,需要除以 1e7
  • alt:毫米,需要除以 1000

如果你没有做换算,拿到的值会大得离谱,看起来像“协议错了”,其实只是单位没处理。

SYS_STATUS
  • voltage_battery:毫伏,转成伏特要除以 1000
  • current_battery:厘安,转成安培要除以 100

很多监控界面“电池 16854V”这种离谱显示,根源就在这。

2. 接收循环要考虑异常和退出机制

模板代码是教学式写法,方便理解,但在项目里我更建议你加上超时和中断控制:

try:
    while True:
        msg = master.recv_match(
            type=['GLOBAL_POSITION_INT', 'ATTITUDE', 'SYS_STATUS'],
            blocking=True,
            timeout=3
        )

        if msg is None:
            print("3 秒内未收到目标消息,检查链路状态...")
            continue

        msg_type = msg.get_type()

        if msg_type == 'GLOBAL_POSITION_INT':
            lat = msg.lat / 1e7
            lon = msg.lon / 1e7
            alt = msg.alt / 1000.0
            print(f"位置:{lat:.6f}, {lon:.6f}, {alt:.1f}m")

        elif msg_type == 'ATTITUDE':
            print(f"姿态:roll={msg.roll:.2f}, pitch={msg.pitch:.2f}, yaw={msg.yaw:.2f}")

        elif msg_type == 'SYS_STATUS':
            print(f"电池:{msg.voltage_battery/1000.0:.1f}V")

except KeyboardInterrupt:
    print("用户终止监听")

3. 一个真实场景:先验证遥测,再开放控制权限

我们之前做一个低空巡检项目时,地面站联调的第一版流程非常简单粗暴:连上飞控后,直接测试解锁、起飞、切模式。

结果现场暴露出两个问题:

  • 某些设备的 system id 识别不稳定
  • 遥测字段解析错误,导致“高度判断”逻辑失真

后面我们把联调流程改成了“三段式”:

  1. 等待心跳并识别目标系统
  2. 连续接收 10 秒遥测,校验位置、姿态、电池字段是否正常
  3. 只有校验通过,才允许发送控制命令

这个改动看起来很小,但对系统稳定性帮助很大。因为很多“控制失败”其实不是控制代码的问题,而是链路和状态认知出了偏差。


四、发送控制命令:解锁、起飞、模式切换的正确打开方式

通信能收还不够,真正体现模板价值的地方在于:能不能把控制动作稳定发出去,并知道飞控是否真的执行。

模板中给出的控制代码如下:

import time

# 解锁无人机
print("解锁...")
master.arducopter_arm()
time.sleep(1)

# 检查解锁状态
master.mav.command_ack_send(
    master.target_system,
    master.target_component,
    mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
    mavutil.mavlink.MAV_RESULT_ACCEPTED
)

# 起飞到 10 米高度
print("起飞...")
master.mav.command_long_send(
    master.target_system,
    master.target_component,
    mavutil.mavlink.MAV_CMD_NAV_TAKEOFF,
    0, 0, 0, 0, 0, 0, 0, 10  # 目标高度 10 米
)

# 等待起飞
time.sleep(5)

# 设置模式为 Loiter(定点)
print("切换到 Loiter 模式...")
master.set_mode(5)  # 5=Loiter

# 降落
# print("降落...")
# master.arducopter_disarm()

1. 这段代码适合做什么

它很适合作为:

  • 原型验证脚本
  • 飞控联调冒烟测试
  • 仿真环境下的控制链路验证

你可以很快确认以下问题:

  • ARM 指令是否能发出去
  • TAKEOFF 是否被飞控接受
  • 模式切换是否生效

2. 但在正式项目里,建议补上 ACK 监听

上面的代码里有一个容易误导初学者的地方:

master.mav.command_ack_send(...)

这行本身是在“发送 ACK”,而不是“读取 ACK”。在常规地面端/伴随计算机场景里,我们通常需要的是等待飞控返回 COMMAND_ACK,而不是主动发 ACK 给飞控。

更实用的写法是这样:

from pymavlink import mavutil
import time

master = mavutil.mavlink_connection('udpin:127.0.0.1:14550')
master.wait_heartbeat()

print("发送 ARM 命令")
master.arducopter_arm()

ack = master.recv_match(type='COMMAND_ACK', blocking=True, timeout=5)
if ack:
    print(f"ARM ACK: command={ack.command}, result={ack.result}")
else:
    print("未收到 ARM ACK")

print("发送 TAKEOFF 命令")
master.mav.command_long_send(
    master.target_system,
    master.target_component,
    mavutil.mavlink.MAV_CMD_NAV_TAKEOFF,
    0,
    0, 0, 0, 0, 0, 0,
    10
)

ack = master.recv_match(type='COMMAND_ACK', blocking=True, timeout=5)
if ack:
    print(f"TAKEOFF ACK: command={ack.command}, result={ack.result}")
else:
    print("未收到 TAKEOFF ACK")

3. 模式切换不要硬编码“5”

模板里用了:

master.set_mode(5)  # 5=Loiter

这在特定飞控固件和模式映射下可能成立,但从工程角度,我更建议写成“通过模式映射查 mode id”,这样可读性和兼容性更好:

mode = 'LOITER'
mode_mapping = master.mode_mapping()
if mode not in mode_mapping:
    raise ValueError(f"不支持模式: {mode}")

mode_id = mode_mapping[mode]
master.set_mode(mode_id)
print(f"已请求切换到模式: {mode}")

4. 控制命令的工程约束

在真实无人机系统里,解锁、起飞、降落这些命令,通常会受到以下因素限制:

  • GPS 是否可用
  • EKF 状态是否正常
  • 安全开关是否解除
  • 电池电量是否足够
  • 当前飞行模式是否允许切换
  • 是否通过地理围栏或预检逻辑

也就是说,“命令发出”不等于“命令成功执行”

如果你正在做企业级平台或行业应用系统,我建议把控制层设计成以下结构:

业务请求层 -> 控制指令封装层 -> MAVLink 发送层 -> ACK/状态校验层 -> 结果反馈层

这样做的好处是,飞行控制不再是“一次 send”,而是一个带状态闭环的动作。


五、任务规划与日志下载:从“能控”走向“可运营”

如果说解锁和起飞解决的是“控制链路”,那航点任务和日志下载解决的就是“业务链路”和“运维链路”。

1. 航点任务上传模板

模板里的航点上传代码如下:

def upload_mission(waypoints):
    """
    上传航点任务
    
    waypoints: [(lat, lon, alt), ...]
    """
    print("上传航点任务...")
    
    # 清除现有任务
    master.mav.mission_clear_all_send(
        master.target_system,
        master.target_component
    )
    
    # 上传航点
    for i, (lat, lon, alt) in enumerate(waypoints):
        master.mav.mission_item_int_send(
            master.target_system,
            master.target_component,
            i,  # 序号
            mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT,
            mavutil.mavlink.MAV_CMD_NAV_WAYPOINT,
            2, 0, 0, 0, 0, 0,
            int(lat * 1e7),
            int(lon * 1e7),
            alt
        )
        print(f"航点{i+1}: {lat}, {lon}, {alt}m")
    
    # 设置总航点数
    master.mav.mission_count_send(
        master.target_system,
        master.target_component,
        len(waypoints)
    )
    
    # 开始任务
    print("开始任务...")
    master.mav.command_long_send(
        master.target_system,
        master.target_component,
        mavutil.mavlink.MAV_CMD_MISSION_START,
        0, 0, 0, 0, 0, 0, 0, 0
    )

# 示例:上传 3 个航点
waypoints = [
    (30.1234, 120.5678, 50),
    (30.1245, 120.5689, 50),
    (30.1256, 120.5700, 50),
]
upload_mission(waypoints)

2. 这份模板的价值在哪里

它把“任务化飞行”的核心思路表达得很清楚:

  • 清理旧任务
  • 定义航点
  • 上传任务
  • 启动任务

对刚进入无人机任务系统开发的人来说,这比单纯看协议字段手册更容易建立整体认知。

3. 任务上传在实战中要关注握手流程

不过从协议严谨性上讲,完整的 Mission 上传通常还涉及:

  • MISSION_COUNT
  • 飞控回 MISSION_REQUEST_INTMISSION_REQUEST
  • 地面端按请求逐条发送 MISSION_ITEM_INT
  • 飞控返回 MISSION_ACK

也就是说,在更标准的实现里,不是一口气把所有航点“盲发”过去,而是要跟飞控的请求节奏配合。

如果你后面准备做:

  • 多机任务调度
  • 高可靠任务执行
  • 长航线自主飞行

那建议在这个模板基础上继续补全 Mission 握手状态机。

4. 一个典型案例:低空巡检任务规划

我们在巡检类项目里,最常见的任务不是复杂避障,而是“固定航线、多点采集、回传状态”。

比如一条典型任务链路:

  1. 运维人员在后台配置巡检点
  2. 平台把点位转换成 [(lat, lon, alt)]
  3. 地面控制服务将航点上传到飞控
  4. 飞控按任务飞行
  5. 地面端持续接收位置、电池、姿态数据
  6. 任务结束后自动拉取飞行日志做复盘

你会发现,模板里的“航点上传 + 遥测接收 + 日志下载”,其实已经把这个闭环的骨架搭起来了。


六、日志下载:给飞行问题留下证据链

很多团队重视控制,却低估日志的重要性。实际上,日志才是排查问题、复盘任务、优化参数的基础数据源。

模板中的日志下载代码片段如下:

def download_logs():
    """下载飞控日志"""
    print("请求日志列表...")
    
    master.mav.log_request_list_send(
        master.target_system,
        master.target_component,
        0,  # 起始 ID
        10  # 数量
    )
    
    # 接收日志列表
    while True:
        msg = master.recv_match(type=['LOG_ENTRY'], blocking=True, timeout=5)
        if msg:
            print(f"日志{msg.id}: {msg.num_logs}条")
            break
    
    # 下载日志
    print("下载日志...")
    master.mav.log_request_data_send(
        master.target_system,
        master.target_component,
        0,  # 日志 ID
        0,  # 偏移
        0xffffffff  # 大小
    )
    
    # 保存日志
    with open('flight.log', 'wb') as f:
        while True:
            msg = master.recv_match(type=['LOG_DATA'],

虽然种子内容里这段代码没有完整展开,但核心流程已经很明确:

  1. 请求日志列表
  2. 获取日志条目
  3. 请求指定日志数据
  4. 按块接收 LOG_DATA
  5. 持久化到本地文件

1. 为什么日志下载不是“可选项”

在以下场景里,日志几乎是必需品:

  • 起飞失败,需要确认预检原因
  • 飞行中突然切模式,需要定位状态变化源头
  • 电池消耗异常,需要复盘电流曲线
  • 轨迹偏航,需要分析姿态与定位质量
  • 任务中断,需要还原执行过程

没有日志,很多问题只能靠猜。

2. 日志模块的工程建议

如果你准备把模板升级成正式工具,我建议补上这些能力:

  • 日志分块校验
  • 下载超时重试
  • 断点续传
  • 多日志批量拉取
  • 文件命名包含时间戳、机体编号、任务编号

比如:

logs/
  UAV001_2025-01-18_mission-34.bin
  UAV001_2025-01-18_mission-34.meta.json

这样后续做自动归档和问题追踪会轻松很多。


七、把模板代码升级为可落地工具,我会怎么做

如果让我基于这份模板,快速搭一个能给团队使用的 MAVLink 调试工具,我会按下面这个方向拆分模块:

1. 连接管理层

负责:

  • 串口/UDP/TCP 统一接入
  • 自动重连
  • 心跳监控
  • system/component 识别

2. 消息订阅层

负责:

  • 高频遥测消息接收
  • 单位换算
  • 字段标准化
  • 发布给 UI 或业务服务

例如统一输出:

{
    "lat": 30.123456,
    "lon": 120.567890,
    "alt_m": 49.8,
    "roll_rad": 0.02,
    "pitch_rad": -0.01,
    "yaw_rad": 1.57,
    "battery_v": 15.2,
    "battery_a": 8.3,
    "battery_remaining": 78
}

3. 控制命令层

负责:

  • ARM / DISARM
  • TAKEOFF / LAND
  • 模式切换
  • ACK 监听
  • 超时和失败回滚

4. Mission 服务层

负责:

  • 航点序列校验
  • 任务上传握手
  • 任务状态跟踪
  • 执行结果记录

5. 日志与诊断层

负责:

  • 飞控日志下载
  • 原始消息存档
  • 错误事件追踪
  • 调试报表生成

这个结构的好处是,模板代码不再只是“几个片段”,而是可以逐步生长成一个真正可维护的项目。


八、结语:从一份模板开始,把通信链路真正吃透

MAVLink 开发看起来像“协议细节活”,但它对无人机项目的影响远不止通信本身。你后面的任务规划、自动控制、数据平台、日志分析,几乎都要建立在这条链路之上。

我很喜欢 UAV-Stack-Knowledge-Base 里这份“通信模版代码”的一点是:它没有一开始就堆很多复杂框架,而是直接围绕开发者最需要的几个动作展开——连接、接收、控制、任务、日志。这对于刚上手 MAVLink 的人非常友好,对于有经验的工程师来说,也很适合拿来做二次封装和团队内部脚手架。

如果你最近正在做无人机开发、低空经济相关系统,或者想系统补齐协议开发这块能力,建议直接去看这个开源项目,把模板代码跑起来,再结合自己的飞控和业务场景迭代。

GitHub 项目:UAV-Stack-Knowledge-Base

我建议你从仓库里的 03-Protocols-Dev/04-通信模版代码.md 开始,先打通第一条链路。很多时候,真正让项目往前推进的,不是一份宏大的架构图,而是一段今天就能连上飞控的代码。