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 的显示也恢复一致。这类问题本质上不是地图画错了,而是任务项语义层给了错误坐标。后面再遇到类似现象时,优先往任务项重建逻辑上查,通常会更快定位到问题。

相关推荐
用户805533698032 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner2 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz7 天前
QML Hello World 入门示例
qt
xcyxiner10 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner11 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner11 天前
DicomViewer (添加模型类)3
qt
xcyxiner12 天前
DicomViewer (目录调整) 2
qt
xcyxiner12 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
桥田智能14 天前
桥田智能 QT-650S:面向白车身焊装的 800kg 重载快换解决方案
开发语言·qt·系统架构
森G14 天前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt