QGC 二次开发:内置 NTRIP Client,实现 CORS 差分数据接入与 GPS_RTCM_DATA 转发
前言
本次在 QGroundControl 二次开发版本中,新增了一个模块 NTRIP Client,在设置页面中填写 CORS/NTRIP 账号,连接 caster 后拼接 GGA,并把收到的 RTCM3 差分数据通过 MAVLink GPS_RTCM_DATA(233) 发给飞控。
本文按一次真实改造记录写,偏工程实操,不展开 RTK 原理。适合已经能编译 QGC 或 QGC fork 的同学参考。
一、最终效果
在 设置 -> RTK 页面中新增 NTRIP Client 页签,保留原来的 Account 账号配置页,不混用、不覆盖。
页面上提供以下配置项:
Enable:启用软件 NTRIP 客户端Server:NTRIP caster 地址Port:端口Mount Point:源节点Username/Password:账号和密码Use Simulated GGA:使用内置模拟坐标生成 GGASend GGA:是否定时发送 GGAGGA Interval(s):发送间隔Connection Status:连接状态RTCM Messages/RTCM Bytes:RTCM 接收统计
实测状态如下时,说明软件链路已经跑通:
text
Connection Status: NTRIP connected
Connected: Yes
RTCM Messages: 80
RTCM Bytes: 9931
这里要注意一点:NTRIP connected 只代表地面站成功登录 CORS/NTRIP 服务;RTCM Messages 和 RTCM Bytes 持续增长,才说明已经收到 RTCM3 差分数据,并进入后续转发路径。
二、整体流程
流程图如下:
否
是
否
是
用户在 RTK / NTRIP Client 填写配置
Enable 是否开启
NTRIP disabled
检查 Server / Port / MountPoint / Username / Password
QTcpSocket 连接 NTRIP caster
发送 HTTP Basic Auth 登录请求
发送 GGA
caster 是否接受登录
显示失败状态并重连
NTRIP connected
接收 RTCM3 字节流
按 RTCM3 preamble 0xD3 解析完整消息
RTCM Messages / Bytes 计数增加
RTCMMavlink::RTCMDataUpdate
MAVLink GPS_RTCM_DATA 233
发送到 active vehicle 的 primary link
核心链路可以压缩成一句话:
QML 配置 Fact -> NTRIPManager 连接 caster -> GGA 触发差分流 -> RTCM3 解析 -> RTCMMavlink 封装 GPS_RTCM_DATA -> 飞控链路发送。
三、文件改造清单
这次主要涉及以下文件:
text
src/GPS/NTRIPManager.h
src/GPS/NTRIPManager.cc
src/GPS/RTCM/RTCMMavlink.h
src/GPS/RTCM/RTCMMavlink.cc
src/Settings/NTRIP.SettingsGroup.json
src/Settings/NTRIPSettings.h
src/Settings/NTRIPSettings.cc
src/QGCToolbox.h / .cc
src/QmlControls/QGroundControlQmlGlobal.h / .cc
src/Settings/SettingsManager.h / .cc
xsrc/XUI/Setting/XRTKBlueTooth.qml
qgroundcontrol.pro
qgroundcontrol.qrc
translations/qgc_source_zh_CN.ts
README.md
其中 NTRIPManager 是新功能主体,RTCMMavlink 可以复用 QGC 原有 RTCM 转 MAVLink 的思路。
四、增加 NTRIP 设置项
QGC 的设置系统通常通过 SettingsGroup + Fact + json metadata 实现。这里新增 NTRIP.SettingsGroup.json:
json
{
"version": 1,
"fileType": "FactMetaData",
"QGC.MetaData.Facts": [
{
"name": "enableNTRIP",
"shortDesc": "Enable NTRIP client",
"type": "bool",
"default": false
},
{
"name": "server",
"shortDesc": "Caster server",
"type": "string",
"default": "140.143.212.42"
},
{
"name": "port",
"shortDesc": "Caster port",
"type": "Uint32",
"default": 8003,
"min": 1,
"max": 65535
},
{
"name": "mountPoint",
"shortDesc": "Mount point",
"type": "string",
"default": "RTCM32_GREC"
},
{
"name": "username",
"shortDesc": "Username",
"type": "string",
"default": "your_username"
},
{
"name": "password",
"shortDesc": "Password",
"type": "string",
"default": "your_password"
},
{
"name": "useSimulatedGGA",
"shortDesc": "Use simulated GGA",
"type": "bool",
"default": false
},
{
"name": "sendGGA",
"shortDesc": "Send GGA after connect",
"type": "bool",
"default": true
},
{
"name": "ggaInterval",
"shortDesc": "GGA interval",
"type": "Uint32",
"default": 5,
"min": 1,
"units": "s"
}
]
}
说明:真实项目中可以把账号做成默认值,但如果代码会提交到公共仓库,密码一定不要明文写入。这里博客中统一使用占位符。
对应 C++ 设置类非常简单:
cpp
#pragma once
#include "SettingsGroup.h"
class NTRIPSettings : public SettingsGroup
{
Q_OBJECT
public:
NTRIPSettings(QObject* parent = nullptr);
DEFINE_SETTING_NAME_GROUP()
DEFINE_SETTINGFACT(enableNTRIP)
DEFINE_SETTINGFACT(server)
DEFINE_SETTINGFACT(port)
DEFINE_SETTINGFACT(mountPoint)
DEFINE_SETTINGFACT(username)
DEFINE_SETTINGFACT(password)
DEFINE_SETTINGFACT(useSimulatedGGA)
DEFINE_SETTINGFACT(sendGGA)
DEFINE_SETTINGFACT(ggaInterval)
};
NTRIPSettings.cc 中注册 QML 类型和 Fact:
cpp
#include "NTRIPSettings.h"
#include <QQmlEngine>
#include <QtQml>
DECLARE_SETTINGGROUP(NTRIP, "NTRIP")
{
qmlRegisterUncreatableType<NTRIPSettings>(
"QGroundControl.SettingsManager", 1, 0,
"NTRIPSettings", "Reference only");
}
DECLARE_SETTINGSFACT(NTRIPSettings, enableNTRIP)
DECLARE_SETTINGSFACT(NTRIPSettings, server)
DECLARE_SETTINGSFACT(NTRIPSettings, port)
DECLARE_SETTINGSFACT(NTRIPSettings, mountPoint)
DECLARE_SETTINGSFACT(NTRIPSettings, username)
DECLARE_SETTINGSFACT(NTRIPSettings, password)
DECLARE_SETTINGSFACT(NTRIPSettings, useSimulatedGGA)
DECLARE_SETTINGSFACT(NTRIPSettings, sendGGA)
DECLARE_SETTINGSFACT(NTRIPSettings, ggaInterval)
五、NTRIPManager 的核心设计
NTRIPManager 放在 src/GPS/ 下,作为 QGCTool 挂入 QGCToolbox。这样 QML 侧可以通过 QGroundControl.ntripManager 获取状态。
头文件中暴露几个 QML 可观察属性:
cpp
class NTRIPManager : public QGCTool
{
Q_OBJECT
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
Q_PROPERTY(int rtcmMessageCount READ rtcmMessageCount NOTIFY rtcmMessageCountChanged)
Q_PROPERTY(int rtcmByteCount READ rtcmByteCount NOTIFY rtcmByteCountChanged)
public:
bool connected() const { return _connected; }
QString status() const { return _status; }
int rtcmMessageCount() const { return _rtcmMessageCount; }
int rtcmByteCount() const { return _rtcmByteCount; }
private:
QTcpSocket _socket;
QTimer _connectTimer;
QTimer _ggaTimer;
QTimer _reconnectTimer;
RTCMMavlink _rtcmMavlink;
RTCMParsing _rtcmParsing;
QByteArray _socketBuffer;
QGeoCoordinate _lastCoordinate;
bool _connected = false;
bool _headerParsed = false;
bool _collectingRtcm = false;
int _rtcmMessageCount = 0;
int _rtcmByteCount = 0;
};
这里的 rtcmMessageCount 和 rtcmByteCount 非常实用。现场调试时,有时候飞控是仿真的,RTK 状态不会变化,这时只看"是否 Fixed"会误判。只要这两个计数增长,就说明地面站已经收到差分流并解析成功。
六、连接 caster 并发送登录请求
连接逻辑使用 QTcpSocket。当设置变化、启用开关变化或 active vehicle 变化时,重新评估连接状态。
cpp
void NTRIPManager::_evaluateConnection()
{
if (!_ntripSettings || !_ntripSettings->enableNTRIP()->rawValue().toBool()) {
_manualDisconnect = true;
_disconnectFromCaster(false);
_setStatus(tr("NTRIP disabled"));
return;
}
if (!_settingsValid()) {
_manualDisconnect = true;
_disconnectFromCaster(false);
_setStatus(tr("NTRIP settings incomplete"));
return;
}
if (_socket.state() == QAbstractSocket::UnconnectedState) {
_manualDisconnect = false;
_headerParsed = false;
_socketBuffer.clear();
_rtcmParsing.reset();
_setStatus(tr("Connecting to NTRIP caster..."));
_socket.connectToHost(
_ntripSettings->server()->rawValue().toString(),
static_cast<quint16>(_ntripSettings->port()->rawValue().toUInt()));
_connectTimer.start(15000);
}
}
TCP 连上后发送 HTTP Basic Auth 请求:
cpp
void NTRIPManager::_sendLoginRequest()
{
const QString mountPoint = _ntripSettings->mountPoint()->rawValue().toString().trimmed();
const QByteArray credentials = QString("%1:%2")
.arg(_ntripSettings->username()->rawValue().toString())
.arg(_ntripSettings->password()->rawValue().toString())
.toUtf8()
.toBase64();
QByteArray request;
request += QString("GET /%1 HTTP/1.1\r\n").arg(mountPoint).toLatin1();
request += QString("Host: %1:%2\r\n")
.arg(_ntripSettings->server()->rawValue().toString().trimmed())
.arg(_ntripSettings->port()->rawValue().toUInt())
.toLatin1();
request += "Ntrip-Version: Ntrip/2.0\r\n";
request += "User-Agent: NTRIP usv-pilot/1.0\r\n";
request += "Accept: */*\r\n";
request += "Connection: keep-alive\r\n";
request += "Authorization: Basic " + credentials + "\r\n\r\n";
_socket.write(request);
_socket.flush();
}
实测中有些 caster 登录成功后不一定返回标准完整 HTTP 头,有的返回 ICY 200 OK,有的甚至会直接开始下发 RTCM3 字节流,所以接收解析要做兼容。
七、GGA 的来源:真实位置与仿真位置
需求中要求 GGA 的经纬高来源于 Vehicle.cc 中 _handleGlobalPositionInt 处理得到的位置。QGC 中 active vehicle 的坐标会触发 coordinateChanged,这里直接监听 active vehicle 坐标即可。
cpp
void NTRIPManager::_activeVehicleChanged(Vehicle* vehicle)
{
if (_activeVehicle) {
disconnect(_activeVehicle, &Vehicle::coordinateChanged,
this, &NTRIPManager::_vehicleCoordinateChanged);
}
_activeVehicle = vehicle;
if (_activeVehicle) {
connect(_activeVehicle, &Vehicle::coordinateChanged,
this, &NTRIPManager::_vehicleCoordinateChanged);
_vehicleCoordinateChanged(_activeVehicle->coordinate());
} else {
_lastCoordinate = QGeoCoordinate();
}
_evaluateConnection();
}
void NTRIPManager::_vehicleCoordinateChanged(QGeoCoordinate coordinate)
{
_lastCoordinate = coordinate;
}
室内测试时可能没有真实 GPS,也可能飞控是仿真的。为避免每次都跑到户外,增加一个 Use Simulated GGA:
cpp
QString NTRIPManager::_buildGGASentence() const
{
static const QGeoCoordinate simulatedCoordinate(31.230416, 121.473701, 10.0);
const QGeoCoordinate coordinate =
_ntripSettings && _ntripSettings->useSimulatedGGA()->rawValue().toBool()
? simulatedCoordinate
: _lastCoordinate;
if (!coordinate.isValid() || qIsNaN(coordinate.latitude()) || qIsNaN(coordinate.longitude())) {
return QString();
}
QString latitudeHemisphere;
QString longitudeHemisphere;
const QString latitude = _formatCoordinate(coordinate.latitude(), true, latitudeHemisphere);
const QString longitude = _formatCoordinate(coordinate.longitude(), false, longitudeHemisphere);
const QString timeString = QDateTime::currentDateTimeUtc().toString("hhmmss.zzz");
const double altitudeMeters = qIsNaN(coordinate.altitude()) ? 0.0 : coordinate.altitude();
const QString body = QString("GPGGA,%1,%2,%3,%4,%5,1,12,1.0,%6,M,0.0,M,,")
.arg(timeString)
.arg(latitude)
.arg(latitudeHemisphere)
.arg(longitude)
.arg(longitudeHemisphere)
.arg(altitudeMeters, 0, 'f', 2);
quint8 checksum = 0;
for (QChar character: body) {
checksum ^= static_cast<quint8>(character.toLatin1());
}
return QString("$%1*%2\r\n")
.arg(body)
.arg(checksum, 2, 16, QLatin1Char('0'))
.toUpper();
}
坐标格式转换如下:
cpp
QString NTRIPManager::_formatCoordinate(double value, bool latitude, QString& hemisphere) const
{
hemisphere = value >= 0 ? (latitude ? "N" : "E") : (latitude ? "S" : "W");
const double absoluteValue = qAbs(value);
const int degrees = static_cast<int>(absoluteValue);
const double minutes = (absoluteValue - degrees) * 60.0;
return latitude
? QString("%1%2").arg(degrees, 2, 10, QLatin1Char('0')).arg(minutes, 7, 'f', 4, QLatin1Char('0'))
: QString("%1%2").arg(degrees, 3, 10, QLatin1Char('0')).arg(minutes, 7, 'f', 4, QLatin1Char('0'));
}
八、兼容不同 caster 响应
最开始调试时,界面一直停在:
text
NTRIP TCP connected, waiting for caster response...
Connected: No
这说明 TCP 已经通了,但登录响应没有被当前代码识别。后来发现需要兼容几种情况:
- 标准 HTTP 响应,以
\r\n\r\n结束头部。 - NTRIP v1 常见的
ICY 200 OK。 - caster 直接下发 RTCM3,首字节就是
0xD3。
核心处理如下:
cpp
void NTRIPManager::_socketReadyRead()
{
_connectTimer.stop();
_socketBuffer.append(_socket.readAll());
if (!_headerParsed) {
if (!_socketBuffer.isEmpty() &&
static_cast<uint8_t>(_socketBuffer.at(0)) == RTCM3_PREAMBLE) {
_headerParsed = true;
_setConnected(true);
_setStatus(tr("NTRIP connected"));
if (_ntripSettings->sendGGA()->rawValue().toBool() && !_ggaTimer.isActive()) {
_ggaTimer.start();
}
}
int headerEndIndex = _socketBuffer.indexOf("\r\n\r\n");
int delimiterLength = 4;
if (headerEndIndex < 0) {
headerEndIndex = _socketBuffer.indexOf("\n\n");
delimiterLength = 2;
}
if (!_headerParsed && headerEndIndex < 0 && _socketBuffer.startsWith("ICY 200")) {
int lineEndIndex = _socketBuffer.indexOf('\n');
if (lineEndIndex >= 0) {
const QByteArray headerBytes = _socketBuffer.left(lineEndIndex).trimmed();
_socketBuffer.remove(0, lineEndIndex + 1);
_headerParsed = true;
_handleCasterHeader(headerBytes);
if (!_connected) {
return;
}
}
}
if (!_headerParsed && headerEndIndex < 0) {
return;
}
if (!_headerParsed) {
const QByteArray headerBytes = _socketBuffer.left(headerEndIndex);
_socketBuffer.remove(0, headerEndIndex + delimiterLength);
_headerParsed = true;
_handleCasterHeader(headerBytes);
}
if (!_connected) {
return;
}
}
if (!_socketBuffer.isEmpty()) {
const QByteArray rtcmBytes = _socketBuffer;
_socketBuffer.clear();
_processRtcmData(rtcmBytes);
}
}
这段是实际调试中比较关键的地方。很多时候不是账号错了,而是 caster 返回格式和你预期的不完全一样。
九、解析 RTCM3 并统计
RTCM3 数据以 0xD3 为 preamble。这里复用 QGC 自带的 RTCMParsing,解析出完整消息后再交给 MAVLink 转发模块。
cpp
void NTRIPManager::_processRtcmData(const QByteArray& data)
{
for (char byteValue: data) {
const uint8_t byte = static_cast<uint8_t>(byteValue);
if (!_collectingRtcm) {
if (byte != RTCM3_PREAMBLE) {
continue;
}
_rtcmParsing.reset();
_collectingRtcm = true;
}
if (_rtcmParsing.addByte(byte)) {
QByteArray rtcmMessage(
reinterpret_cast<const char*>(_rtcmParsing.message()),
_rtcmParsing.messageLength());
_rtcmMessageCount++;
_rtcmByteCount += rtcmMessage.size();
emit rtcmMessageCountChanged();
emit rtcmByteCountChanged();
_rtcmMavlink.RTCMDataUpdate(rtcmMessage);
_rtcmParsing.reset();
_collectingRtcm = false;
continue;
}
}
}
加计数器的好处非常直接:现场看到 RTCM Messages、RTCM Bytes 增长,就知道 NTRIP 数据已经进入地面站软件链路,不需要盲猜。
十、通过 GPS_RTCM_DATA 转发给飞控
RTCMMavlink 负责把 RTCM 消息打包成 MAVLink GPS_RTCM_DATA(233)。
cpp
void RTCMMavlink::RTCMDataUpdate(QByteArray message)
{
const int maxMessageLength = MAVLINK_MSG_GPS_RTCM_DATA_FIELD_DATA_LEN;
mavlink_gps_rtcm_data_t mavlinkRtcmData;
memset(&mavlinkRtcmData, 0, sizeof(mavlink_gps_rtcm_data_t));
if (message.size() < maxMessageLength) {
mavlinkRtcmData.len = message.size();
mavlinkRtcmData.flags = (_sequenceId & 0x1F) << 3;
memcpy(&mavlinkRtcmData.data, message.data(), message.size());
sendMessageToVehicle(mavlinkRtcmData);
} else {
uint8_t fragmentId = 0;
int start = 0;
while (start < message.size()) {
int length = std::min(message.size() - start, maxMessageLength);
mavlinkRtcmData.flags = 1;
mavlinkRtcmData.flags |= fragmentId++ << 1;
mavlinkRtcmData.flags |= (_sequenceId & 0x1F) << 3;
mavlinkRtcmData.len = length;
memcpy(&mavlinkRtcmData.data, message.data() + start, length);
sendMessageToVehicle(mavlinkRtcmData);
start += length;
}
}
++_sequenceId;
}
真正发送时,遍历当前所有 vehicle,找到 primary link:
cpp
void RTCMMavlink::sendMessageToVehicle(const mavlink_gps_rtcm_data_t& msg)
{
QmlObjectListModel& vehicles = *_toolbox.multiVehicleManager()->vehicles();
MAVLinkProtocol* mavlinkProtocol = _toolbox.mavlinkProtocol();
for (int i = 0; i < vehicles.count(); i++) {
Vehicle* vehicle = qobject_cast<Vehicle*>(vehicles[i]);
WeakLinkInterfacePtr weakLink = vehicle->vehicleLinkManager()->primaryLink();
if (!weakLink.expired()) {
mavlink_message_t message;
SharedLinkInterfacePtr sharedLink = weakLink.lock();
mavlink_msg_gps_rtcm_data_encode_chan(
mavlinkProtocol->getSystemId(),
mavlinkProtocol->getComponentId(),
sharedLink->mavlinkChannel(),
&message,
&msg);
vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), message);
}
}
}
这里有一个很重要的判断:如果 Android 上没有连接飞控,地面站仍然可以连接 CORS 并接收 RTCM,但没有 active vehicle/link 时,数据没有目标可发。
十一、QML 页面实现
页面放在 xsrc/XUI/Setting/XRTKBlueTooth.qml 中。这里要特别注意:不要改坏原来的 Account 页面。本次只新增 NTRIP Client 页签。
核心 QML 结构如下:
qml
property var ntripSettings: QGroundControl.settingsManager.ntripSettings
property var ntripManager: QGroundControl.ntripManager
TabButton {
text: qsTr("NTRIP Client")
}
GroupBox {
title: qsTr("Software NTRIP")
GridLayout {
columns: 2
XLabel { text: qsTr("Use Simulated GGA"); color: "white" }
FactCheckBox { fact: ntripSettings.useSimulatedGGA }
XLabel { text: qsTr("Enable"); color: "white" }
FactCheckBox { fact: ntripSettings.enableNTRIP }
XLabel { text: qsTr("Server"); color: "white" }
FactTextField { fact: ntripSettings.server; Layout.fillWidth: true }
XLabel { text: qsTr("Port"); color: "white" }
FactTextField { fact: ntripSettings.port; Layout.fillWidth: true }
XLabel { text: qsTr("Mount Point"); color: "white" }
FactTextField { fact: ntripSettings.mountPoint; Layout.fillWidth: true }
XLabel { text: qsTr("Username"); color: "white" }
FactTextField { fact: ntripSettings.username; Layout.fillWidth: true }
XLabel { text: qsTr("Password"); color: "white" }
FactTextField { fact: ntripSettings.password; Layout.fillWidth: true }
}
XLabel {
text: qsTr("Connection Status") + ": " +
(ntripManager ? ntripManager.status : qsTr("Unavailable"))
}
XLabel {
text: qsTr("RTCM Messages") + ": " +
(ntripManager ? ntripManager.rtcmMessageCount : 0)
}
XLabel {
text: qsTr("RTCM Bytes") + ": " +
(ntripManager ? ntripManager.rtcmByteCount : 0)
}
}
这次还顺手修了一个 UI 问题:深色背景上文字不能继续用黑色,否则 Android 屏幕上几乎看不清。
十二、Android 编译踩坑
Windows 下测试通过后,Android 编译时报了这个错误:
text
undefined reference to `NTRIPManager::NTRIPManager(QGCApplication*, QGCToolbox*)'
这类错误是链接阶段找不到实现。最后定位到 qgroundcontrol.pro:
qmake
!MobileBuild {
SOURCES += \
src/GPS/NTRIPManager.cc \
src/GPS/RTCM/RTCMMavlink.cc \
src/GPS/Drivers/src/rtcm.cpp
}
Android 属于 MobileBuild,所以这些 .cc 被排除了。但 QGCToolbox.cc 已经引用了 NTRIPManager,于是桌面能过,Android 链接失败。
解决方法是把 NTRIP 必需的最小文件移到通用构建列表,不能放在 !MobileBuild 里:
qmake
HEADERS += \
src/GPS/Drivers/src/rtcm.h \
src/GPS/NTRIPManager.h \
src/GPS/RTCM/RTCMMavlink.h
SOURCES += \
src/GPS/Drivers/src/rtcm.cpp \
src/GPS/NTRIPManager.cc \
src/GPS/RTCM/RTCMMavlink.cc
这个坑很典型:QGC 项目文件中有很多平台条件,新增 C++ 类时不要只看 Windows 是否能编译,要确认 Android/iOS 是否也被条件块排除了。
十三、实操测试步骤
1. Windows 室内测试
先用 Windows 验证 NTRIP 账号和 caster 链路,建议勾选:
text
Use Simulated GGA: true
Send GGA: true
Enable: true
配置示例:
text
Server: 140.143.212.42
Port: 8003
Mount Point: RTCM32_GREC
Username: your_username
Password: your_password
如果成功,界面会显示:
text
Connection Status: NTRIP connected
Connected: Yes
RTCM Messages: 持续增加
RTCM Bytes: 持续增加
2. Android 真机测试
Android 重点检查:
- 遥控器或手机能访问公网 CORS 服务器。
- App 已连接飞控,有 active vehicle。
RTCM Messages和RTCM Bytes持续增长。- 如果是仿真飞控,不要期待 RTK Fixed,这只能证明软件链路通了。
- 如果是真实 RTK 模块,还要确认飞控和 GPS 模块支持 RTCM 注入。
3. 判断问题在哪一段
可以按下面这张表排查:
| 现象 | 说明 | 优先检查 |
|---|---|---|
| 一直 Connecting | TCP 没连上 | 网络、IP、端口、防火墙 |
| TCP connected, waiting | TCP 通了,但响应未识别或 caster 未下发 | 账号、密码、节点、响应格式、GGA |
| NTRIP connected,但 RTCM 为 0 | 登录成功但无差分流 | GGA、节点权限、账号套餐 |
| RTCM 增长,但飞控无变化 | 地面站链路通,飞控/GPS 未体现 | active vehicle、GPS_RTCM_DATA、RTK 模块能力 |
十四、几个经验点
NTRIP connected不等于 RTK 生效,只代表 caster 登录成功。RTCM Messages / RTCM Bytes是判断软件链路的关键指标。- 室内测试一定要有
Use Simulated GGA,否则没有位置时 GGA 拼不出来。 - Android 编译要特别注意
!MobileBuild条件块。 - 真实提精度的前提是飞控和 GPS/RTK 硬件支持 RTCM 注入,普通 GPS 不会因为收到 RTCM 就变成 RTK。
- CORS 账号密码不要随意写进公开仓库,产品内部构建也要明确凭据管理边界。
十五、总结
这次改造本质上是在 QGC 内部补齐了一条软件差分链路:
text
CORS/NTRIP caster -> QGC NTRIP Client -> RTCM3 parser -> GPS_RTCM_DATA(233) -> Flight Controller
从工程角度看,最关键的不是单个 socket 连接,而是把设置系统、QML 页面、QGCToolbox 生命周期、GGA 构造、RTCM3 解析、MAVLink 转发和 Android 构建条件全部串起来。
最终实测 Windows 和 Android 都可以连接 NTRIP caster,RTCM Messages 与 RTCM Bytes 正常增长,说明 CORS 差分数据已经进入地面站并走到 MAVLink 转发路径。后续如果要验证真实精度提升,就需要接真实支持 RTK 的 GPS 模块和飞控参数一起测。