4、QGC任务数据结构:MissionItem类、MissionController类、任务列表管理

任务数据怎么组织,是QGC二次开发最基础的问题。我见过不少开发者,上来就改界面、调参数,结果任务上传后飞机乱飞——回头一看,原来是数据结构没搞明白。这一章,我们就把它彻底说清楚。

4.1 MissionItem类:一个航点长什么样?

说白了,MissionItem就是QGC里描述单个航点的“身份证”。它包含了这个点所有的属性:位置、动作、参数、是否被选中等等。

我习惯把MissionItem分成三组来看:

分组 核心字段 说明
位置信息 coordinate、altitude、altitudeMode 经纬度、海拔高度、高度模式(相对/绝对)
动作指令 command、param1~param7 MAVLink命令ID(如MAV_CMD_NAV_WAYPOINT)及对应参数
状态标记 isCurrent、isSelected、isFlyThrough 当前点、选中状态、是否飞越(不悬停)

关键点:command字段决定了这个航点的“行为”。比如MAV_CMD_NAV_WAYPOINT是普通航点,MAV_CMD_NAV_LOITER_UNLIM是无限悬停,MAV_CMD_DO_SET_SERVO是控制舵机。你想想看,如果command填错了,飞机到了那个点根本不会按你预想的动作执行。

来看一个典型的MissionItem创建代码:

// 创建一个普通航点
MissionItem* item = new MissionItem(this);
item->setCommand(MAV_CMD_NAV_WAYPOINT);
item->setCoordinate(QGeoCoordinate(39.9042, 116.4074, 50.0));
item->setAltitudeMode(MissionItem::AltitudeModeRelative);
item->setParam1(0.0);  // 悬停时间,0表示不悬停
item->setParam2(0.0);  // 接受半径
item->setParam3(0.0);  // 通过半径
item->setParam4(NAN);  // 偏航角,NAN表示不指定
item->setFlyThrough(true);  // 飞越模式,不减速

嗯,这里要注意param4。我刚开始做的时候,习惯给偏航角填0,结果飞机到了每个点都自动转回正北。后来才发现,NAN才是“不指定”的意思。这个坑,我踩过。

4.2 MissionController类:任务的大脑

MissionController是QGC任务系统的核心控制器。它不直接存数据,而是管理数据的生命周期和交互逻辑。我个人觉得,理解它最好的方式,是看它干了哪几件事:

  1. 加载任务:从文件(.plan/.mission)或Pixhawk读取任务列表
  2. 编辑任务:增删改航点,支持撤销/重做
  3. 校验任务:检查航点合法性(高度超限?命令无效?)
  4. 上传/下载:通过MAVLink与飞控同步任务
  5. 状态同步:实时更新当前执行到哪个航点

它的内部结构大致是这样的:

class MissionController : public QObject
{
    Q_OBJECT
public:
    // 任务列表操作
    void loadFromFile(const QString& filePath);
    void saveToFile(const QString& filePath);
    void removeAll();
    
    // 航点操作
    void appendMissionItem(MissionItem* item);
    void removeMissionItem(int index);
    void moveMissionItem(int from, int to);
    
    // 与飞控交互
    void sendMissionToVehicle();
    void loadMissionFromVehicle();
    
    // 信号
signals:
    void missionItemsChanged();
    void currentMissionItemChanged(int index);
    
private:
    QList<MissionItem*> _missionItems;
    Vehicle* _vehicle;
    int _currentItemIndex;
};

为什么说它是“大脑”?因为它把界面和飞控隔离开了。你拖拽航点时,界面只发信号给MissionController,它负责更新数据、触发校验、再通知界面刷新。这样分工清晰,后期改起来也方便。

我的习惯:做二次开发时,尽量通过MissionController的接口操作任务,不要直接修改_missionItems列表。否则撤销/重做功能会失效,而且容易引发界面不同步的bug。

4.3 任务列表管理:增删改查与排序

任务列表本质上是一个QList<MissionItem*>。但管理起来,有几个细节值得注意。

4.3.1 增删改操作

QGC提供了完整的操作接口。我建议你直接调用MissionController的方法:

// 添加航点
MissionItem* wp = new MissionItem();
wp->setCommand(MAV_CMD_NAV_WAYPOINT);
// ... 设置其他属性
controller->appendMissionItem(wp);

// 删除航点(索引从0开始)
controller->removeMissionItem(3);

// 移动航点(把第2个移到第5个位置)
controller->moveMissionItem(2, 5);

这里有个容易忽略的点:删除航点后,后续航点的索引会自动前移。如果你在循环中删除多个航点,记得从后往前删,或者用removeAll清空重建。

4.3.2 排序与重编号

任务列表的排序,QGC默认按添加顺序排列。但实际飞行中,航点序号(seq)必须连续且从0开始。上传前,MissionController会自动重编号。不过,如果你在代码里手动插入了航点,最好调用一下:

controller->recalcSequenceNumbers();

我曾经遇到过一个问题:在任务中间插入了一个“拍照”动作(MAV_CMD_DO_SET_CAM_TRIGG_DIST),结果上传后飞控报错“sequence mismatch”。查了半天,原来是插入后序号没更新。从那以后,我每次修改列表都会主动调一下这个函数。

4.3.3 任务校验

QGC在校验任务时,会检查以下几项:

  • 首航点必须是NAV命令(不能是DO命令)
  • 高度不能超过限制(默认500米,可在设置中修改)
  • 相邻航点距离不能太近(小于0.5米会警告)
  • 命令参数在有效范围内

避坑指南:我曾经在项目中遇到一个情况——用户导入了一个第三方生成的任务,所有航点都是DO_SET_SERVO命令,没有NAV命令。结果上传后,飞控直接拒绝执行。QGC的校验逻辑是:任务列表里至少有一个NAV_*命令,否则视为无效任务。这个检查在MissionController::validateMission()里,你可以重写或扩展它。

4.4 核心数据流:从界面到飞控

为了让你更直观地理解整个数据流转,我画了一张图:

QGC任务数据流 用户界面 拖拽/编辑/导入 MissionController 校验/排序/管理 MissionItem列表 QList<MissionItem*> 操作信号 读写数据 数据变化 刷新信号 飞控 (Pixhawk) MAVLink协议 上传/下载 数据流说明: 1. 用户在界面上拖拽/编辑航点,发出操作信号 2. MissionController接收信号,更新MissionItem列表 3. 列表变化后,Controller触发校验和重排序 4. 最后通过MAVLink上传到飞控,或从飞控下载到列表

从这张图你可以看到,MissionController处于中心位置,所有数据交互都经过它。这样做的好处是:如果你想换一种任务存储方式(比如存到数据库),只需要改MissionController的读写逻辑,界面和飞控代码完全不用动。

4.5 性能优化:列表操作别踩坑

任务列表操作看起来简单,但性能问题往往藏在不经意的地方。我总结了几条经验:

  • 批量操作时用beginResetModel()/endResetModel():如果你要一次性添加100个航点,逐个appendMissionItem会导致界面刷新100次。正确的做法是先清空列表,批量添加,最后一次性刷新。
  • 避免频繁的深拷贝:MissionItem内部包含QGeoCoordinate等对象,深拷贝开销不小。传递指针或使用QSharedPointer会更高效。
  • 上传前压缩数据:QGC默认使用MISSION_COUNT + MISSION_ITEM协议上传。如果任务超过100个航点,可以考虑使用MISSION_ITEM_INT协议(支持浮点数压缩),减少传输时间。

我的建议:如果你在做航线规划功能,尽量在后台线程中处理任务列表的生成和校验,避免阻塞UI线程。QGC的MissionController本身是单线程的,但你可以用QtConcurrent把耗时的校验逻辑放到后台。