2、QGC架构解析:整体架构、模块划分、核心类图、通信机制与插件系统
QGC 这个项目,我第一次看源码的时候,说实话有点懵。代码量不小,目录结构也复杂。但摸清楚它的骨架之后,你会发现它其实非常清晰。说白了,它就是一个典型的 Model-View-Controller (MVC) 架构,只不过在 Qt 的框架下,用了一些自己的习惯。
我个人习惯把 QGC 的架构拆成四个层面来看:通信层、核心逻辑层、UI 表现层,以及插件扩展层。这一章,我们就一层一层把它剥开。
2.1 整体架构与模块划分
QGC 的源码根目录下,有几个核心的文件夹,它们对应了不同的模块。我整理了一个表格,方便你对照着看:
| 模块目录 | 职责描述 | 核心类(举例) |
|---|---|---|
src/Comm |
通信层,负责与飞控建立连接,收发 MAVLink 数据包 | LinkManager, SerialLink, UDPLink |
src/Vehicle |
核心逻辑层,管理无人机状态、参数、任务等 | Vehicle, VehicleObject |
src/FirmwarePlugin |
固件插件层,适配不同固件(PX4, ArduPilot)的差异 | FirmwarePlugin, PX4FirmwarePlugin |
src/QmlControls |
UI 层,存放 QML 控件和页面 | FlyView.qml, PlanView.qml |
src/AnalyzeView |
日志与数据分析模块 | LogDownloadController |
嗯,这里要注意,src/Vehicle 是整个应用的“大脑”。所有的 MAVLink 消息最终都会汇聚到这里,然后被解析、分发。UI 层只负责展示和接收用户操作,它不直接处理飞控逻辑。
核心原则: QGC 的 UI(QML)和业务逻辑(C++)是严格分离的。C++ 层通过注册到 QML 上下文的 Q_PROPERTY 和 Q_INVOKABLE 方法,向 UI 层暴露数据接口。你永远不要在 QML 里直接写业务逻辑。
2.2 核心类图:从 Link 到 Vehicle
为了让你更直观地理解,我画了一张核心类的协作图。这张图展示了从数据流进来,到最终被 UI 消费的完整链路。
这张图里,Vehicle 类是个关键节点。它内部维护了一个巨大的状态机,处理着心跳、姿态、位置、电池等几十种消息。我刚开始做二次开发时,总想着自己从头解析 MAVLink 消息,后来发现直接继承 Vehicle 或者监听它的信号,效率高得多。
2.3 通信机制:MAVLink 的“毛细血管”
MAVLink 是 QGC 和飞控之间的“语言”。QGC 支持两种版本:MAVLink 1.0 和 MAVLink 2.0。我个人建议,新项目一律用 2.0,因为它支持签名认证,安全性好很多。
通信的起点是 LinkInterface。它是个抽象类,具体的实现有 SerialLink(串口)、UDPLink(UDP)、TCPLink(TCP)。你猜哪个最常用?
我的经验: 开发调试阶段,UDP 链路最方便。你可以用 mavproxy.py 在电脑上模拟一个飞控,QGC 通过 UDP 连上去,完全不需要硬件。我曾经用这个方式,一天之内调通了自定义的 MAVLink 协议扩展。
数据接收的流程大致是这样的:
- 轮询读取:
LinkInterface的后台线程不断从串口或 socket 读取字节流。 - 字节解码: 调用
MAVLinkProtocol::receiveBytes(),按照 MAVLink 帧格式(起始符、长度、序列号、系统ID、消息ID、负载、CRC)进行解码。 - 信号发射: 解码成功后,发射
messageReceived()信号。 - 消息路由:
Vehicle对象接收到信号,根据message.msgid调用对应的处理函数。
这里有个坑,我必须要提醒你:
我曾经踩过的坑: MAVLink 的 target_system 和 target_component 字段。如果你在发送消息时填错了这两个 ID,飞控会直接忽略你的指令。QGC 的 Vehicle 对象在创建时就会从心跳包中解析出正确的 systemId 和 componentId,你发消息时一定要用这两个值。
2.4 插件系统:如何优雅地适配不同固件
QGC 最巧妙的设计之一,就是它的 FirmwarePlugin 插件系统。为什么需要这个?因为 PX4 和 ArduPilot 虽然都用 MAVLink,但很多细节不一样。比如,解锁的指令、参数的定义、航点的格式,都有差异。
插件系统的核心是 FirmwarePlugin 这个基类。它定义了一系列虚函数,比如:
isVehicleDisarmed():判断飞机是否已上锁。sendArmCommand():发送解锁/上锁指令。startMission():开始执行任务。getParameterMetaData():获取参数的元数据(最小值、最大值、单位等)。
然后,PX4FirmwarePlugin 和 APMFirmwarePlugin 分别继承这个基类,实现各自的逻辑。QGC 在启动时,会根据飞控发来的 AUTOPILOT_VERSION 消息,自动选择对应的插件实例。
如果你想适配一个新的固件,比如你自己的定制固件,你只需要做两件事:
- 继承
FirmwarePlugin,实现所有纯虚函数。 - 在
FirmwarePluginManager中注册你的插件,并指定它对应的MAV_AUTOPILOT枚举值。
代码示例:注册自定义插件
// 在 FirmwarePluginManager.cpp 中
if (autopilotType == MAV_AUTOPILOT_GENERIC) {
// 假设你的固件类型是 MAV_AUTOPILOT_GENERIC
plugin = new MyCustomFirmwarePlugin(this);
}
嗯,这里要注意,插件系统不仅仅是适配指令。它还负责提供 UI 层面的“差异”。比如,PX4 的“安全开关”在 ArduPilot 上可能没有,那么 FirmwarePlugin 中有一个 hasSafetyButton() 方法,UI 层会根据这个方法的返回值来决定是否显示安全开关的按钮。
我个人觉得,这种设计非常优雅。它把“变”的部分隔离在插件里,“不变”的部分留在核心框架中。你想想看,如果没有这个插件系统,QGC 的代码里会充斥着大量的 if (firmware == PX4) {...} else if (firmware == APM) {...},那维护起来得多痛苦。