摘要
这篇文章记录一个在 QGroundControl 二次开发中实际遇到的问题:任务航线先在 Mission Planner 中规划,再从飞机读取到 QGC 后显示,结果第一个 Takeoff 点落到了经纬度 (0,0),导致 Mission start 和 Takeoff 之间出现一条非常长的连线。这个问题在中文启动时更容易暴露,而英文启动时看起来又是正常的。
最终排查下来,问题不在地图绘制层,也不在 Mission Planner 本身,而是在 QGC 下载任务后对 TakeoffMissionItem 的重建逻辑中:当飞机返回的 takeoff 坐标为 (0,0) 时,程序把它误当成了一个真实的独立起飞点,而不是"未单独指定坐标"的语义。本文按实操过程记录这次修复,重点放在代码落点、修改方式和验证结果上。

左侧 MP 绘制正常航线,右侧 QGC 加载 MP 绘制的航线
一、问题背景
这次问题的前提很明确,不是直接在 QGC 的 Plan 页面里手工画航线,而是这样的链路:
- 在
Mission Planner中规划任务航线 - 把任务上传到飞机
- 在 QGC 中从飞机读取任务
- 在 QGC 的
Plan View和Fly 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 中和这个问题相关的几个核心类是:
MissionControllerMissionSettingsItemTakeoffMissionItem
从飞机下载任务后,MissionController 会把底层任务项重建成可视化任务项,其中 Takeoff 最终会落到 TakeoffMissionItem 这条路径上。
也就是说,只要 TakeoffMissionItem 在重建过程中保留了错误坐标,后面的 Plan View 和 Fly 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_TAKEOFF或MAV_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. 统一接入 _init 和 load(...) 路径
只加一个辅助函数还不够,必须把这条归一化逻辑真正接入到任务项的初始化和加载路径里。
这次接入的位置包括:
_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) Takeoff与Mission start重合- 地图上不再出现那条超长连线
而且这个结果在两个界面上是一致的:
Plan View正常Fly View正常
语言维度上也恢复一致:
- 中文启动正常
- 英文启动正常
也就是说,这次修复并不是"让中文看起来像英文",而是把下载后 Takeoff 坐标的语义统一了。
十二、总结
这次问题的表面现象是:Mission Planner 规划的任务在 QGC 中显示时,第一个 Takeoff 跑到了 (0,0),地图上出现长线,而且中文启动时更容易看到这个问题。
最终定位下来,根因并不在 Mission Planner,也不在地图层,而是在 QGC 下载任务后重建 TakeoffMissionItem 的过程中,把 (0,0) 错误当成了真实独立起飞点。
修复的关键在于两点:
- 把
(0,0)统一解释为"未单独指定坐标" - 在
TakeoffMissionItem这一层统一做归一化处理
修完之后,Takeoff 和 Mission start 重新重合,Plan View 和 Fly View 的显示也恢复一致。这类问题本质上不是地图画错了,而是任务项语义层给了错误坐标。后面再遇到类似现象时,优先往任务项重建逻辑上查,通常会更快定位到问题。