QT/QGroundControl 实战:Mission Planner 航线在 QGC 中出现 Takeoff 落到 (0,0) 的排查与修复

摘要

这篇文章记录一个在 QGroundControl 二次开发中实际遇到的问题:任务航线先在 Mission Planner 中规划,再从飞机读取到 QGC 后显示,结果第一个 Takeoff 点落到了经纬度 (0,0),导致 Mission startTakeoff 之间出现一条非常长的连线。这个问题在中文启动时更容易暴露,而英文启动时看起来又是正常的。

最终排查下来,问题不在地图绘制层,也不在 Mission Planner 本身,而是在 QGC 下载任务后对 TakeoffMissionItem 的重建逻辑中:当飞机返回的 takeoff 坐标为 (0,0) 时,程序把它误当成了一个真实的独立起飞点,而不是"未单独指定坐标"的语义。本文按实操过程记录这次修复,重点放在代码落点、修改方式和验证结果上。

左侧 MP 绘制正常航线,右侧 QGC 加载 MP 绘制的航线

一、问题背景

这次问题的前提很明确,不是直接在 QGC 的 Plan 页面里手工画航线,而是这样的链路:

  1. Mission Planner 中规划任务航线
  2. 把任务上传到飞机
  3. 在 QGC 中从飞机读取任务
  4. 在 QGC 的 Plan ViewFly View 中显示出来

在这个过程中,出现了一个比较明显的异常现象:

  • Mission start 的位置是正常的
  • 第一个 Takeoff 点却跑到了 (0,0)
  • 地图上会出现一条从任务起点连到远处的长线

更有迷惑性的是,这个问题在不同语言启动下表现还不完全一样:

  • 中文启动时,更容易看到 Takeoff 落到 (0,0)
  • 英文启动时,Takeoff 又常常和 Mission start 重合

从表面看,很容易让人先怀疑翻译、QML 显示或者地图图层,但继续往下跟代码后会发现,真正的问题并不在这些位置。


二、任务链路与问题位置

先把这次问题对应的任务链路画清楚,后面看代码会更直观。


Mission Planner 规划航线
任务上传到飞机
QGC 从飞机下载任务
MissionController 重建任务项
TakeoffMissionItem 初始化 / load
takeoff 坐标是否为 (0,0)?
保留独立 Takeoff 坐标
回落到 Mission start / launch same-location
Plan View / Fly View 正常显示

这张图里真正需要修的地方,不是在地图显示层,而是在 TakeoffMissionItem 对下载任务的坐标语义判断上。


三、排查路径

这次排查我没有先从 QML 的 MapItem 去查,而是直接往任务项构造链路上找。

QGC 中和这个问题相关的几个核心类是:

  • MissionController
  • MissionSettingsItem
  • TakeoffMissionItem

从飞机下载任务后,MissionController 会把底层任务项重建成可视化任务项,其中 Takeoff 最终会落到 TakeoffMissionItem 这条路径上。

也就是说,只要 TakeoffMissionItem 在重建过程中保留了错误坐标,后面的 Plan ViewFly View 都会跟着一起显示错。

所以这次排查的重点很快就收敛到了:

text 复制代码
src/MissionManager/TakeoffMissionItem.h
src/MissionManager/TakeoffMissionItem.cc
src/MissionManager/MissionController.cc

五、根因定位

继续往 TakeoffMissionItem 里看后,问题就很清楚了。

从飞机读回来的某些任务里,第一个 takeoff 的经纬度本身就是 (0,0)。这个 (0,0) 在业务语义上并不表示"飞机真的要从几内亚湾起飞",而更像是:

  • 飞控没有给出独立的起飞坐标
  • 这个 takeoff 应该和 Mission start / planned home 视为同一位置
  • (0,0) 只是一个未单独指定坐标的占位值

但原有逻辑并没有把这种 (0,0) 识别成"未指定坐标",而是继续把它当成一个合法的独立坐标保留下来。

结果就是:

  • Mission start 正常
  • Takeoff(0,0)
  • QGC 地图自然会画出一条超长连线

所以这个问题的根因可以直接概括成一句话:

下载后的 TakeoffMissionItem(0,0) 的语义判断不正确。


六、修复思路

这次修复没有去动 QML,也没有去做语言分支,而是把修复点放在 TakeoffMissionItem 本身。

原因很简单,TakeoffMissionItem 这一层才真正负责起飞任务项的语义:

  • 什么时候是独立起飞点
  • 什么时候和 Mission start 重合
  • 什么时候应当视为"未单独指定坐标"

修复规则可以整理成下面这几条:

  • 当前命令是 MAV_CMD_NAV_TAKEOFFMAV_CMD_NAV_VTOL_TAKEOFF
  • 关联的 MissionSettingsItem 中 launch / planned-home 坐标有效
  • 下载回来的 takeoff 坐标是 (0,0),或者语义上应视为未指定

满足这些条件时,就不要保留 (0,0),而是应该把 Takeoff 回落到 Mission start 所在位置。

换句话说,这次修复的核心不是"修一个错误坐标",而是统一 takeoff(0,0) 的语义解释


七、修改代码的地方

这次修复主要集中在两个文件里:

text 复制代码
src/MissionManager/TakeoffMissionItem.h
src/MissionManager/TakeoffMissionItem.cc

对应的修改点可以概括成下面这张关系图:
TakeoffMissionItem.h
新增 _coordinateIsUnspecified(...) 声明
新增 _normalizeTakeoffCoordinate() 声明
TakeoffMissionItem.cc
实现 _coordinateIsUnspecified(...)
实现 _normalizeTakeoffCoordinate()
在 _init(...) 中调用归一化
在 load(QTextStream&) 中调用归一化
在 load(QJsonObject, ...) 中调用归一化
补充 MissionControllerLog 调试日志

从职责上看:

  • TakeoffMissionItem.h 负责声明新的辅助函数
  • TakeoffMissionItem.cc 负责实现坐标归一化逻辑,并接入初始化与加载路径

这样改完之后,不管任务项是从哪条路径构建出来的,都会先经过同一套 (0,0) 处理规则。


八、代码修改

1. 增加坐标是否为未指定的统一判断

首先在 TakeoffMissionItem 里增加一个辅助函数,用来统一判断当前坐标是否应视为"未指定"。

文件:

text 复制代码
src/MissionManager/TakeoffMissionItem.h

新增声明:

cpp 复制代码
bool _coordinateIsUnspecified(const QGeoCoordinate& coordinate) const;
void _normalizeTakeoffCoordinate(void);

对应实现放在:

text 复制代码
src/MissionManager/TakeoffMissionItem.cc

代码如下:

cpp 复制代码
bool TakeoffMissionItem::_coordinateIsUnspecified(const QGeoCoordinate& coordinate) const
{
    return !coordinate.isValid() ||
           (qFuzzyIsNull(coordinate.latitude()) && qFuzzyIsNull(coordinate.longitude()));
}

这里做的事情很直接:

  • 坐标无效,视为未指定
  • 经纬度同时为 0,也视为未指定

这样后面就不需要在多个地方重复写 (0,0) 判断了。

2. 增加 Takeoff 坐标归一化逻辑

接着增加一个专门的归一化函数,把下载得到的 (0,0) takeoff 统一折叠回 launch 坐标。

代码如下:

cpp 复制代码
void TakeoffMissionItem::_normalizeTakeoffCoordinate(void)
{
    const QGeoCoordinate launchCoordinate = _settingsItem->coordinate();
    const QGeoCoordinate takeoffCoordinate = coordinate();

    if (launchCoordinate.isValid() && _coordinateIsUnspecified(takeoffCoordinate)) {
        qCDebug(MissionControllerLog)
            << "Normalize downloaded takeoff coordinate"
            << "takeoff:" << takeoffCoordinate
            << "launch:"  << launchCoordinate
            << "fallback:" << true;

        setLaunchTakeoffAtSameLocation(true);
        SimpleMissionItem::setCoordinate(launchCoordinate);
    }
}

这段代码完成的事情是:

  • 先读出当前 Takeoff 坐标
  • 再读出 MissionSettingsItem 中的 launch 坐标
  • 如果 launch 有效,而 takeoff 又是"未指定"坐标
  • 就把 Takeoff 强制回落到 launch same-location 语义

修完这一步以后,地图层拿到的就不再是 (0,0),而是和 Mission start 重合的真实坐标。

3. 统一接入 _initload(...) 路径

只加一个辅助函数还不够,必须把这条归一化逻辑真正接入到任务项的初始化和加载路径里。

这次接入的位置包括:

  • _init(bool forLoad)
  • load(QTextStream&)
  • load(const QJsonObject&, int, QString&)

实际改法如下:

cpp 复制代码
void TakeoffMissionItem::_init(bool forLoad)
{
    ...

    _normalizeTakeoffCoordinate();
    _initLaunchTakeoffAtSameLocation();

    ...
}

以及:

cpp 复制代码
bool TakeoffMissionItem::load(QTextStream& loadStream)
{
    ...

    _normalizeTakeoffCoordinate();
    _initLaunchTakeoffAtSameLocation();

    ...
}
cpp 复制代码
bool TakeoffMissionItem::load(const QJsonObject& json, int sequenceNumber, QString& errorString)
{
    ...

    _normalizeTakeoffCoordinate();
    _initLaunchTakeoffAtSameLocation();

    ...
}

这样做的意义是非常明确的:

  • 不管当前任务项是走初始化路径,还是走加载路径
  • 不管后面是显示在 Plan View 还是 Fly View
  • 都先做一次同样的坐标归一化

这样可以最大程度避免"某条路径修了,另一条路径没修"的情况。

4. 补充日志,方便后续排查

这类任务重建问题后续很容易重复出现,所以这次顺手加了一层调试日志。

示例:

cpp 复制代码
qCDebug(MissionControllerLog)
    << "Takeoff coordinate state"
    << "takeoff:" << takeoffCoordinate
    << "launch:" << launchCoordinate
    << "sameLocation:" << _launchTakeoffAtSameLocation;

后面再遇到类似问题时,就可以直接从日志里判断:

  • 飞机返回的原始 takeoff 坐标是多少
  • launch 坐标是多少
  • 是否触发了 (0,0) fallback
  • 最终是不是走了 launchTakeoffAtSameLocation

5. 修改点说明

这次几个实际修改点的作用可以单独总结一下:

(1)TakeoffMissionItem.h

这里新增了两个私有函数声明:

cpp 复制代码
bool _coordinateIsUnspecified(const QGeoCoordinate& coordinate) const;
void _normalizeTakeoffCoordinate(void);

作用是把"坐标是否应视为未指定"和"takeoff 坐标归一化"这两个职责独立出来,后面维护时逻辑更集中。

(2)TakeoffMissionItem.cc::_coordinateIsUnspecified(...)

这里统一定义 (0,0) 的语义。

只要:

  • 坐标无效
  • 或者经纬度同时为 0

就认为当前 takeoff 坐标不应继续当成真实独立坐标使用。

(3)TakeoffMissionItem.cc::_normalizeTakeoffCoordinate()

这里是真正完成修复的核心函数。

它会在 launch / planned-home 坐标有效的前提下,把下载得到的 (0,0) takeoff 折叠回 launch 坐标,并设置 launchTakeoffAtSameLocation

(4)TakeoffMissionItem.cc::_init(...)

这个位置负责对象初始化阶段的处理。把归一化放在这里,可以保证构造后的 TakeoffMissionItem 一开始就处于正确状态。

(5)TakeoffMissionItem.cc::load(QTextStream&)

这个路径对应文本流加载任务项时的处理。把归一化接进来后,可以避免某些加载路径绕过修复逻辑。

(6)TakeoffMissionItem.cc::load(const QJsonObject&, int, QString&)

这个路径对应 JSON 加载任务项时的处理。接入这里后,整个 TakeoffMissionItem 的多条重建路径就统一了。


九、修复后的效果

这次修复完成后,重新从飞机下载同一条任务,可以看到结果已经恢复正常:

  • Mission start 位置正常
  • 第一个 Takeoff 不再落到 (0,0)
  • TakeoffMission start 重合
  • 地图上不再出现那条超长连线

而且这个结果在两个界面上是一致的:

  • Plan View 正常
  • Fly View 正常

语言维度上也恢复一致:

  • 中文启动正常
  • 英文启动正常

也就是说,这次修复并不是"让中文看起来像英文",而是把下载后 Takeoff 坐标的语义统一了。


十二、总结

这次问题的表面现象是:Mission Planner 规划的任务在 QGC 中显示时,第一个 Takeoff 跑到了 (0,0),地图上出现长线,而且中文启动时更容易看到这个问题。

最终定位下来,根因并不在 Mission Planner,也不在地图层,而是在 QGC 下载任务后重建 TakeoffMissionItem 的过程中,把 (0,0) 错误当成了真实独立起飞点。

修复的关键在于两点:

  • (0,0) 统一解释为"未单独指定坐标"
  • TakeoffMissionItem 这一层统一做归一化处理

修完之后,TakeoffMission start 重新重合,Plan ViewFly View 的显示也恢复一致。这类问题本质上不是地图画错了,而是任务项语义层给了错误坐标。后面再遇到类似现象时,优先往任务项重建逻辑上查,通常会更快定位到问题。

相关推荐
blog_wanghao3 小时前
基于Qt的串口调试助手
开发语言·qt
Sss_Ass10 小时前
在Qt Creator创建并编写第一个程序
开发语言·qt
jf加菲猫12 小时前
第12章 数据可视化
开发语言·c++·qt·ui
咸鱼翻身小阿橙13 小时前
QT总结-P2
开发语言·qt
雪的季节14 小时前
QT QPointer 解析
开发语言·qt
老歌老听老掉牙1 天前
PyQt5+Qt Designer实战:可视化设计智能参数配置界面,告别手动布局时代!
python·qt
A.A呐1 天前
【QT第六章】界面优化
开发语言·qt
sycmancia1 天前
Qt——布局管理器(一)
前端·qt
AlanW1 天前
QT 信号槽内部实现原理深度解析
qt
A.A呐1 天前
【QT第五章】系统相关
开发语言·qt