基于MAVSDK与MQTT的飞控数据采集传输系统设计
简介:本资源是一套面向无人机飞控开发者的C语言跨平台实时数据采集与传输系统,适用于嵌入式开发者、飞控算法工程师及物联网通信学习者,解决无人机遥测数据低延迟获取、MAVLink协议解析与云端/地面站高效分发的核心问题。压缩包共38个文件,含8个CMake构建脚本(支撑Linux/Windows/macOS多平台编译)、4个C/C++源码文件(含mqtt_client.cpp与config.h等核心逻辑)、5个说明类文本(含README.md、说明文件.txt与附赠资源.docx),以及日志、缓存、构建中间产物等辅助文件,整体仅131KB,轻量易集成。已有187人学习下载,资源结构清晰:顶层为CMake工程框架,内含MAVSDK调用接口、MAVLink消息解析模块、Paho MQTT C客户端封装及跨平台适配层,配套文档详述架构设计、接口定义与编译流程,开发者可直接复用数据采集管道或快速对接自定义MQTT Broker。 做无人机远程监控的项目,最麻烦的往往不是飞机能不能飞起来,而是飞控产生的数据怎么稳定地送出去。我最近刚把一套基于MAVSDK的飞控数据采集与MQTT实时传输系统跑通,整个链路涉及MAVLink协议解析、MAVSDK上层接口调用、Paho MQTT C Client Library集成,以及跨平台编译环境配置,是个典型的边缘网关数据桥接工程。这里把完整的设计思路和实操过程整理出来,尤其是那些踩过的坑,给准备做无人机遥测上云、远程巡检或编队监控的朋友一个可以直接参考的底子。
这套系统的核心价值其实就一句话:把飞控上原本只适合点对点传输的MAVLink数据流,转换成适合云端海量设备接入的MQTT消息流。飞控链路距离短、协议固定,MQTT的Broker则天然支持大量设备接入、消息持久化和订阅分发,两者结合之后,地面站、Web端、手机端可以同时拿到飞机状态,不用再守着射频链路看数传。项目工程结构是C/C++混合的:采集端用MAVSDK的C++接口读飞控数据,传输端用Paho的纯C客户端库作为MQTT发布端,中间用cJSON做数据序列化,整体在Linux和ARM板子上都能编译运行。这篇博客就按从底层协议到上层应用的顺序,把整个系统的实现拆开讲清楚。
1. 项目整体思路与架构设计
1.1 这套系统到底解决什么问题
先说我为什么需要这个东西。当时做的是一个无人机远程巡检的验证项目,飞机飞在野外,人在办公室,飞控链路是标准数传模块,距离有限,数据只能被地面站接收。如果想让数据回到远程服务器做存储、告警和回放,就得在飞控和云端之间加一个“翻译官”和“快递员”。
这套系统里的角色划分是这样的:
- 飞控端:输出MAVLink协议数据,常见的如Pixhawk系列飞控,通过串口或UDP往外发遥测。
- 采集网关:就是一套带处理器的边缘设备,通常是一块树莓派或者ARM工控板,运行MAVSDK,负责连接飞控、接收MAVLink数据。
- 传输链路:MQTT协议,网关作为客户端,把解析出的姿态、GPS、电池等信息发布到Broker。
- 云端消费端:任何订阅了对应Topic的Web服务、数据库集群或小程序,从Broker拉取数据。
所以这里的“系统”本质是一个协议转换与数据桥接的服务程序。它不参与飞控决策,也不影响飞行,只是被动地读取和转发数据。这也是我后来在架构设计里非常注意的一点:任何对飞控数据的下行指令都必须单独设计,不能和遥测上报混在同一个Topic里,否则容易误触发,这是后话。
1.2 关键链路与技术选型的取舍
这个项目里几个核心组件的选型并不是拍脑袋定的,每一项背后都有对比和权衡。
先说MAVSDK。其实读MAVLink数据有两条路:一是自己按MAVLink帧格式逐字节解析,二是用MAVSDK这种封装好的库。MAVSDK底层虽然是C++实现的MAVLink协议栈,但它对外提供的是非常友好的系统抽象,比如Telemetry插件直接给姿态角、四元数、GPS状态,不需要自己处理消息ID和字节偏移。它的好处是跨平台、自动重连、接口语义清晰,适合做应用层开发。代价是库体量大、依赖多,在资源极度紧张的MCU上不合适,但跑在Linux边缘网关上是绰绰有余。
MQTT的选择就没什么悬念了。市面上IoT消息协议里,MQTT是最成熟的一套,QoS分级、遗嘱消息、保留消息、断线持久会话这些特性全是针对弱网设备设计的。和裸TCP长连接比,MQTT多了一层Broker做缓冲和分发,客户端不用管对端在不在线;和HTTP轮询比,MQTT是推送式的,实时性和流量消耗都更好。
Paho MQTT C Client Library选纯C版本而不用C++版本的考虑是:这个库在嵌入式领域用得极广,没有C++运行时依赖,编译出来体积小,而且API稳定。另一个重要原因是飞控网关通常不是高性能设备,纯C库在交叉编译时省心很多。
数据序列化我用了cJSON。这个库单文件、无依赖、嵌套操作方便,把飞控结构体转成JSON字符串再发给MQTT,云端各语言都能直接解析。
下图是系统数据流向,我画个文字版:
飞控(MAVLink) -> MAVSDK(C++) -> 业务逻辑(解析/过滤/序列化) -> Paho MQTT C Client(发布) -> MQTT Broker -> 云端订阅端简单来说,采集端MAVSDK负责“听懂”飞控的话,业务逻辑负责“整理”成云端友好的格式,Paho负责“快递”到Broker。
2. MAVLink协议解析与飞控数据读取
2.1 MAVLink帧结构与消息格式速览
理解MAVLink是后面所有工作的基础。MAVLink是一种轻量级的消息传输协议,常见的有v1和v2两个版本。现在新固件基本都默认开v2,所以我在工程里固定按v2解析,但兼容判断还是保留了,因为部分老设备仍是v1。
MAVLink v2帧的典型结构如下:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| STX | 1 | 帧起始标志,v2为0xFD |
| LEN | 1 | 有效载荷长度 |
| INC_FLAGS | 1 | 不兼容标志,如是否需要签名 |
| CMP_FLAGS | 1 | 兼容标志 |
| SEQ | 1 | 消息序号,用于丢包检测 |
| SYS_ID | 1 | 系统ID,标识飞控 |
| COMP_ID | 1 | 组件ID,标识飞控内部模块 |
| MSG_ID | 3 | 消息ID,标识消息类型 |
| PAYLOAD | 0~255 | 消息体 |
| CRC | 2 | 校验码 |
其中CRC不仅覆盖PAYLOAD,还要覆盖前面的部分字段,并且会附加一个消息种子值,这很关键。如果你手写解析,CRC很容易错,因为不同消息的CRC种子不同。在MAVLink的C头文件里有一个MAVLINK_MESSAGE_CRC表,专门存放每个消息ID对应的额外CRC种子,必须查表才能算对。
这个项目里我虽然是基于MAVSDK做的,底层帧解析被库消化了,但排查问题时必须懂得帧格式本身。比如有一次数据断断续续,一看日志里CRC失败率飙升,最后定位是串口波特率不匹配,而不是信号干扰。
2.2 用MAVSDK还是手写解析器
这是很多做无人机相关项目的人会纠结的问题。我自己的经验建议是:如果跑在Linux级别的主控上,无脑选MAVSDK;只有当你需要部署到STM32这类单片机,或者需要深度定制协议行为时,才值得手写。
对比一下两条路:
| 维度 | MAVSDK | 手写MAVLink解析 |
|---|---|---|
| 开发效率 | 高,Telemetry直接给姿态、GPS等结构化数据 | 低,需自己维护消息表、CRC表、结构体 |
| 依赖 | 较大,需要C++编译链 | 只需纯C代码 |
| 内存占用 | 较高 | 极低 |
| 协议升级适配 | 库升级即可 | 需要自己同步头文件 |
| 适合场景 | 边缘网关、地面站、仿真 | MCU、对资源和依赖苛刻的嵌入式场景 |
我用的MAVSDK版本是v2.x,它默认支持MAVLink v2,通信方式可以自动识别串口和UDP。MAVSDK里面有个关键设计是System和Plugin架构。一个System对象代表一个飞控,Telemetry、Action、Mission这些都是挂在System下的插件。采集数据只用到Telemetry,但系统初始化的时候可以同时初始化多个插件,方便以后扩展航线控制。
2.3 实际采集哪些飞控消息
飞控遥测消息很多,但不是全都要。我按项目需求筛选了五类核心数据:
- HEARTBEAT(MSG_ID 0):飞控心跳,用于判断连接状态和飞控类型。
- ATTITUDE(MSG_ID 30):姿态角,包含roll、pitch、yaw和对应角速度。
- GPS_RAW_INT(MSG_ID 24):GPS原始数据,经纬度、高度、卫星数、定位类型。
- BATTERY_STATUS(MSG_ID 147):电池电压、电流、剩余电量百分比。
- GLOBAL_POSITION_INT(MSG_ID 33):融合后的全局位置,比位置估计更稳定。
用MAVSDK的Telemetry插件订阅这些数据非常简单,核心代码大致是:
#include "mavsdk/mavsdk.h" #include "mavsdk/plugins/telemetry/telemetry.h" #include <iostream> using namespace mavsdk; int main() { Mavsdk mavsdk; ConnectionResult conn_result = mavsdk.add_any_connection("udp://:14550"); if (conn_result != ConnectionResult::Success) { std::cerr << "连接飞控失败: " << conn_result << std::endl; return -1; } // 等待飞控系统出现 auto system = mavsdk.first_system(); if (!system) { std::cerr << "未检测到飞控系统" << std::endl; return -1; } std::cout << "已连接到飞控 System ID: " << system->get_system_id() << std::endl; auto telemetry = std::make_shared<Telemetry>(system); // 订阅姿态数据 telemetry->subscribe_attitude_quaternion([](Telemetry::Quaternion quat) { std::cout << "四元素: w=" << quat.w << " x=" << quat.x << " y=" << quat.y << " z=" << quat.z << std::endl; }); // 订阅GPS原始数据 telemetry->subscribe_gps_raw_int([](Telemetry::GpsRawInt gps) { std::cout << "GPS: lat=" << gps.latitude_deg << " lon=" << gps.longitude_deg << " 卫星数=" << static_cast<int>(gps.satellites_visible) << std::endl; }); while (true) { std::this_thread::sleep_for(std::chrono::seconds(1)); } return 0; }这段代码跑起来之后,飞控的实时数据就能持续打印。实际业务里,我不会在回调里直接做发布操作,因为回调线程的调用频率很高,直接发MQTT容易造成消息风暴。我的做法是先把数据写入一个环形缓存区,再由独立线程按固定频率(比如5Hz)取出、打包、发送。
3. Paho MQTT C Client集成与消息传输
3.1 Paho库编译的两种方式
Paho MQTT C Client Library的项目名是paho.mqtt.c,编译方式比较灵活。我试过两种,都在工程里留了脚本。
第一种是直接用CMake构建并安装到系统目录:
git clone https://github.com/eclipse-paho/paho.mqtt.c.git cd paho.mqtt.c cmake -B build -DPAHO_WITH_SSL=FALSE -DPAHO_ENABLE_TESTING=FALSE cmake --build build sudo cmake --install build我编译时直接把SSL关了,因为我们当前网络环境里Broker和网关在同一内网,不需要TLS加密。如果以后要公网传输,可以考虑开启SSL,但编译时需要额外处理OpenSSL依赖,后面跨平台部分会说。
第二种是在自己的CMake工程里用FetchContent拉取源码:
include(FetchContent) FetchContent_Declare( paho_mqtt_c GIT_REPOSITORY https://github.com/eclipse-paho/paho.mqtt.c.git GIT_TAG v1.3.13 ) FetchContent_MakeAvailable(paho_mqtt_c)用FetchContent的好处是版本可控,团队里其他人拉下来就能编,不需要提前装系统库。
3.2 连接Broker和发布消息的关键配置
Paho C库提供两套API:同步的MQTTClient和异步的MQTTAsync。同步API用起来直观,适合发布频率不高的场景;异步API则适合需要高吞吐、非阻塞发送的场景。由于我的发布频率峰值能到20Hz,我一开始用同步API,结果发现发送偶尔会阻塞几百毫秒,如果Broker网络抖动会直接拖垮采集线程。后来改成异步API,才彻底解决阻塞问题。
连接Broker的核心初始化代码如下:
#include "MQTTClient.h" #define ADDRESS "tcp://127.0.0.1:1883" #define CLIENTID "drone_gateway_01" #define TOPIC "drones/001/telemetry" #define QOS 1 MQTTClient client; MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; int rc; rc = MQTTClient_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL); if (rc != MQTTCLIENT_SUCCESS) { printf("创建客户端失败: %d\n", rc); return -1; } conn_opts.keepAliveInterval = 20; conn_opts.cleansession = 1; conn_opts.connectTimeout = 10; conn_opts.retryInterval = 2; conn_opts.automaticReconnect = 1; conn_opts.minRetryInterval = 1; conn_opts.maxRetryInterval = 60; rc = MQTTClient_connect(client, &conn_opts); if (rc != MQTTCLIENT_SUCCESS) { printf("连接Broker失败: %d\n", rc); return -1; }几个参数重点说明一下:
keepAliveInterval:心跳间隔。我设置20秒,Broker超过约1.5倍时间没收到报文就认为客户端掉线。设太小会频繁产生网络包,设太大则断线发现不及时。cleansession = 1:每次连接都是全新会话,不保留离线消息。对于遥测数据这没问题,因为飞控数据本来就是“当前值”,错过几秒没意义。如果要下发指令,建议单独开一个订阅连接并设置cleansession = 0。automaticReconnect = 1:自动重连。这是弱网环境下最重要的开关,不用自己写重连逻辑。
发布消息我封装了一个函数:
void publish_json(const char* topic, const char* payload, int qos) { MQTTClient_message pubmsg = MQTTClient_message_initializer; pubmsg.payload = (void*)payload; pubmsg.payloadlen = (int)strlen(payload); pubmsg.qos = qos; pubmsg.retained = 0; MQTTClient_deliveryToken token; MQTTClient_publishMessage(client, topic, &pubmsg, &token); // 异步接口下,这里不需要等待,回调里会收到deliveryComplete }有个容易踩的坑:异步API中MQTTClient_publishMessage里的topic参数和msg.payload,在函数返回后,库内部并不拷贝topic字符串,所以不能用临时变量。必须保证这个字符串生命周期足够长,最好是静态或动态分配。我最初就是用了栈上的字符串缓冲区,导致丢消息,排查了半天。
3.3 Topic设计与QoS策略
MQTT的Topic设计直接影响后续数据消费的复杂度。我建议按设备ID和数据类型分层,这样云端订阅时可以灵活过滤。
我实际用的Topic结构是:
drones/{sysid}/telemetry # 所有遥测数据 drones/{sysid}/status # 连接状态、飞行模式变更 drones/{sysid}/event # 告警、超阈值事件遥测Topic里的payload是一个完整的JSON对象,包含多种字段:
{ "ts": 1712345678.123, "sysid": 1, "att": {"roll": 0.12, "pitch": -0.34, "yaw": 2.15}, "gps": {"lat": 39.9088, "lon": 116.3975, "alt": 350.2, "sats": 15}, "bat": {"voltage": 11.8, "current": 5.2, "remaining": 0.78} }QoS策略我这样选择:
- 遥测数据:QoS 1。因为数据实时性强,QoS 2会造成额外确认延迟,QoS 0则可能丢帧。QoS 1保证Broker至少收到一次,重复消息由云端用时间戳去重。
- 状态类消息:QoS 1,同时设置
retained = 1。这样新的云端客户端上线后能立刻知道当前飞控是处于“自动飞行”还是“悬停”状态,不用等待下一个状态变化。 - 告警事件:QoS 1或2,看业务重要性。我这里用了QoS 2,因为告警不能丢,也不能重复。
这里多说一句retained消息。当时我调试Web端时发现,只要页面刷新,飞控状态就变成“未知”,要等好几秒才有新数据刷新出来。后来就是给status Topic开了retained,页面一订阅就能拿到最新状态。这个技巧很小,但效果立竿见影。
4. 跨平台编译与工程组织
4.1 整体依赖梳理
这个工程的依赖关系大概是这样的:
| 依赖库 | 语言 | 用途 | 是否可选 |
|---|---|---|---|
| MAVSDK | C++14/17 | 飞控数据采集 | 必选 |
| Paho MQTT C | C99 | MQTT发布 | 必选 |
| cJSON | C99 | JSON序列化 | 必选 |
| OpenSSL | C | TLS支持,Paho/MAVSDK都依赖 | 可选但建议装 |
| Libevent/WebSockets | C | MAVSDK底层通信 | MAVSDK依赖的后端 |
| Asio | C++ | MAVSDK网络IO | MAVSDK依赖 |
MAVSDK的底层传输后端有串口、UDP、TCP,还支持WebSocket。在Linux和ARM上,UDP是最常用的,因为大多数数传模块就是走的UDP转发。编译时,MAVSDK会自己拉取这些第三方依赖,所以不需要手动处理太多。
4.2 CMake工程组织示例
整个工程我按模块化组织:
drone_gateway/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── mavlink_reader.h / .cpp │ ├── json_serializer.h / .cpp │ ├── mqtt_bridge.h / .cpp ├── third_party/ │ ├── cJSON.c / cJSON.h └── config/ └── config.json根目录的CMakeLists.txt关键部分是:
cmake_minimum_required(VERSION 3.16) project(drone_gateway LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_C_STANDARD 99) # MAVSDK add_subdirectory(third_party/mavsdk) # Paho MQTT C set(PAHO_WITH_SSL ON CACHE BOOL "" FORCE) set(PAHO_ENABLE_TESTING OFF CACHE BOOL "" FORCE) add_subdirectory(third_party/paho.mqtt.c) # cJSON 直接编译源码 add_library(cjson third_party/cJSON/cJSON.c) add_executable(drone_gateway src/main.cpp src/mavlink_reader.cpp src/json_serializer.cpp src/mqtt_bridge.cpp ) target_link_libraries(drone_gateway PRIVATE mavsdk mavsdk_telemetry paho-mqtt3as cjson pthread )这里有个细节:Paho库编译出来有两个版本,paho-mqtt3a是异步API,paho-mqtt3c是同步API,paho-mqtt3as是带SSL的异步库。链接时要选对后缀,否则编译报未定义引用。
4.3 不同平台编译实测记录
我实际在三个平台编译过这套工程:
平台一:x86_64 Ubuntu 20.04
最顺利,直接装依赖就行:
sudo apt install build-essential cmake libssl-dev mkdir build && cd build cmake .. make -j$(nproc)这里的坑是Ubuntu 20.04的默认GCC版本是9.x,MAVSDK编译要求C++17,而Paho库要求C99,基本都能满足。
平台二:树莓派4B (ARM64, Ubuntu Server)
编译流程和x86差不多,但有两个坑:
- 内存不够。树莓派4B如果只有2GB内存,
make -j4会直接OOM,我改成make -j2才编过。 - 默认编译器是32位还是64位要确认。建议直接用64位系统镜像,因为32位下MAVSDK的部分第三方依赖会有问题。
平台三:瑞芯微RK3588 ARM板(交叉编译)
交叉编译相对麻烦,我用的是aarch64编译链。核心是给CMake指定工具链文件:
# aarch64-toolchain.cmake set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) set(CMAKE_FIND_ROOT_PATH /opt/aarch64/sysroot) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)然后编译:
mkdir build_arm && cd build_arm cmake -DCMAKE_TOOLCHAIN_FILE=aarch64-toolchain.cmake .. make -j8交叉编译时最容易出问题的是OpenSSL。因为MAVSDK和Paho编译时如果开了SSL,都会去找目标板上的libssl。交叉编译环境下,这个库需要事先准备好目标板的rootfs,或者直接把SSL关掉。我为了省事,在ARM板上直接把PAHO_WITH_SSL关了,MAVSDK的底层WebSocket传输也不用,只用UDP,这样交叉编译就顺利很多。
4.4 Windows平台的一点点说明
严格说Windows上也能编过,但体验比较差。MAVSDK官方支持Windows,但依赖的串口和网络库在Windows上编译路径不同,还需要装vcpkg来管理第三方依赖。Paho库在Windows上用vcpkg安装最方便:
vcpkg install paho-mqtt-c但是需要注意,Windows版Paho编译默认是DLL方式,把它部署到无VC运行库的目标机器上,容易跑不起来。如果只是开发调试,建议直接用WSL或者Docker里的Linux编译,产物放到生产Linux环境。我自己很少在Windows上编这个工程,除非只是给客户演示demo。
5. 常见问题与排查技巧实录
5.1 飞控连接失败,报错“没有检测到飞控系统”
这个是最常见的。原因我梳理了几个:
- 串口设备不存在或权限不足。检查
ls /dev/ttyUSB*或/dev/serial*,给当前用户加dialout组权限,或者用sudo chmod 666 /dev/ttyUSB0临时解决。 - UDP端口不对。如果数传模块在电脑上开了Mission Planner,它正在占着14550端口,你再开这个口就会冲突。
- 波特率不对。Pixhawk的TELEM2口默认一般是57600,但有些飞控或改装模块是115200,一定要先确认固件里参数设置。
排查这个问题的通用方法:先用QGroundControl或Mission Planner连接飞控,确认数传链路正常;再用mavlink-inspector之类工具监听,看主控端能否收到心跳包。如果地面站能连上而你的程序连不上,问题基本出在连接地址和权限上。
5.2 数据全是NaN或者明显是0
这个情况通常是飞控本身的问题,不是采集程序的问题。常见原因:
- 飞机没解锁,IMU还在初始化状态,部分数据可能是NaN。
- GPS没有完成定位,
satellites_visible为0,经纬度为0。 - 传感器校准丢失,飞控内部EKF不健康。
排查方式:多看MAVSDK提供的健康状态接口,最直接的是Telemetry::health_all_ok()。我在程序里加了一行日志,非健康状态下会打warning,同时把采集频率降到1Hz,减少无意义的数据包。
5.3 MQTT消息时断时续,Broker日志里全是连接中断记录
这种现象通常不是程序逻辑错误,而是网络或配置问题。我碰到过几种:
- keepalive设置太短。如果Broker和网关之间有几秒延迟,
keepAliveInterval设成5秒会导致Broker误判掉线。我后来统一设20秒,重连稳定很多。 - Broker地址用了域名,但网关的DNS解析不稳定。建议直接换成IP,或提前做DNS缓存。
- 网关的4G模块休眠策略。树莓派等设备如果启用省电模式,网络会周期性断开,导致MQTT被动重连。要在系统层面关闭WiFi/4G模块的省电,或者加大keepalive间隔。
这里我强烈建议给Paho配置自动重连,但要注意自动重连的时间间隔。Paho的initialReconnectDelay和maxReconnectDelay可以控制退避策略,我设的是1秒起始、60秒封顶,实测在弱网下比较科学。
5.4 数据发布频率太高,Broker压力大
飞控Telemetry消息原始频率很高,比如IMU数据能到100Hz甚至更高,如果直接把每个回调都发到MQTT,Broker很容易被打爆。
我的降频策略是:
- 筛选关键消息。姿态、GPS这种变化频率中等的消息,5~10Hz就足够。
- 在采集端做时间窗口聚合。比如把1秒内的数据合并成一个JSON数组,一条MQTT消息里携带多帧数据。
- 用环形缓存加定时器,固定间隔发送,而不是回调一触发就发。
实测下来,把10Hz的遥测发布稳定在一个数据包约1KB以内,带宽占用很小,4G网络完全无压力。
5.5 程序长时间运行后内存持续增长
这是个经典坑。我排查过两处:
第一处是Paho的异步发布。使用MQTTClient_publishMessage时,如果消息率很高,而send缓冲满了,消息会堆积在内存里。虽然Paho内部有队列,但无限发布会导致内存增长。解决办法是增加频率控制,或者改用同步发布并接受偶尔的阻塞。
第二处是cJSON对象没有及时删除。cJSON的cJSON_CreateObject()分配的内存必须用cJSON_Delete()释放。我在封装序列化函数时,一开始忘记在发送完成后释放对象,跑半小时内存就涨了几十MB。
正确的发布流程是:
cJSON* root = cJSON_CreateObject(); // ...填充字段... char* payload = cJSON_PrintUnformatted(root); // 用payload发布MQTT mqtt_publish(payload); // 释放资源 free(payload); cJSON_Delete(root);不要小看free(payload)这一步,cJSON_PrintUnformatted内部也是malloc分配的内存,不释放一样泄漏。
5.6 遥测数据里时间戳不同步
云端如果想按时间线回放姿态数据,网关本地时间戳非常关键。我最初是用time(NULL)取的秒级时间,后来发现多帧数据在1秒内无法区分顺序,只能靠MQTT内的seq字段辅助。改成clock_gettime(CLOCK_REALTIME, &ts)取纳秒级时间戳之后,云端排序就正常了。
另外,如果网关和云端的时钟不同步,归档的数据会漂移。建议在网关加一个NTP同步,并在JSON里同时带设备本地时间和接收时间。这个细节对后续做数据分析和告警别影响很大。
6. 我最后想说的几个工程化建议
整个系统跑通之后,回头总结,几个点我认为对要复现这个项目的人很重要。
第一个建议,模块之间一定要做解耦。采集、序列化、发布拆成三个独立模块,用环形缓冲队列通信。这样以后换掉任何一层都不影响其他层。比如想从MAVSDK换成手写MAVLink解析,只需要改mavlink_reader模块,MQTT部分一行不动。
第二个建议,开始写代码前,先把Topic命名和JSON格式定义清楚。这两个是接口契约,一旦定下来,云端、网关、数据库多方都要遵守,后期改动成本很高。最好是写一个简单的接口文档,哪怕只有一页,也能避免很多沟通成本。
第三个建议,日志管理要提前做。这个系统跑在无人值守的网关上,出问题只能翻日志。我用了spdlog库,按天滚动,保留30天日志,同时把关键事件(起飞、连接断开、数据异常)单独输出到syslog。一开始嫌麻烦没做,后来一次现场排查花了半天时间才定位到问题,就再也不省这个事了。
第四个建议,做这个项目的边界要清晰:数据采集和上报只是整个无人机监控系统的一小块,但它是连接飞行器和云端的生命线。数据链路稳定比数据精度更优先,先保证心跳不断、消息不积压,再谈优化数据内容。所以我在程序启动时会有一个“自检模式”,先连续采集30秒,检查MAVLink丢包率、MQTT发布成功率,全部达标后才进入正常数据流,不达标就打印诊断信息,这样启动时能第一时间暴露问题。
这套基于MAVSDK和MQTT的飞控数据采集传输方案,从想法到稳定运行,前后折腾了大概两周,其中一半时间花在编译和部署上,另一半花在排查各种奇奇怪怪的断连问题上。如果你也在做类似的事情,希望这篇记录能帮你少走一些弯路。尤其在Paho异步发布、Topic规划、交叉编译这些地方,提前想清楚,后面会省很多事。
本文还有配套的精品资源,点击获取
