QGC 二次开发(RTK):内置 NTRIP Client,实现 CORS 差分数据接入与 GPS_RTCM_DATA 转发

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:使用内置模拟坐标生成 GGA
  • Send GGA:是否定时发送 GGA
  • GGA 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 MessagesRTCM 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;
};

这里的 rtcmMessageCountrtcmByteCount 非常实用。现场调试时,有时候飞控是仿真的,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 已经通了,但登录响应没有被当前代码识别。后来发现需要兼容几种情况:

  1. 标准 HTTP 响应,以 \r\n\r\n 结束头部。
  2. NTRIP v1 常见的 ICY 200 OK
  3. 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 MessagesRTCM 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 重点检查:

  1. 遥控器或手机能访问公网 CORS 服务器。
  2. App 已连接飞控,有 active vehicle。
  3. RTCM MessagesRTCM Bytes 持续增长。
  4. 如果是仿真飞控,不要期待 RTK Fixed,这只能证明软件链路通了。
  5. 如果是真实 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 模块能力

十四、几个经验点

  1. NTRIP connected 不等于 RTK 生效,只代表 caster 登录成功。
  2. RTCM Messages / RTCM Bytes 是判断软件链路的关键指标。
  3. 室内测试一定要有 Use Simulated GGA,否则没有位置时 GGA 拼不出来。
  4. Android 编译要特别注意 !MobileBuild 条件块。
  5. 真实提精度的前提是飞控和 GPS/RTK 硬件支持 RTCM 注入,普通 GPS 不会因为收到 RTCM 就变成 RTK。
  6. 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 MessagesRTCM Bytes 正常增长,说明 CORS 差分数据已经进入地面站并走到 MAVLink 转发路径。后续如果要验证真实精度提升,就需要接真实支持 RTK 的 GPS 模块和飞控参数一起测。

相关推荐
Kapaseker1 小时前
客官,你误会 Compose Strong Skipping 了
android·kotlin
张风捷特烈1 小时前
状态管理大乱斗#04 | Riverpod 源码评析 (上) - 核心架构
android·前端·flutter
不要绝望总会慢慢变强1 小时前
无人机智能体的实现的一些思考
人工智能·深度学习·ai·无人机
大鹏说大话2 小时前
构造函数属性提升的利与弊:如何优雅地编写价值对象(Value Object)
android
moonsims2 小时前
无人机视觉传感器布局标准图(UAV Vision Sensor Layout Architecture)
无人机
大黄说说2 小时前
匹配表达式 vs. Switch语句:现代PHP中的条件逻辑重构
android·ide·android studio
fred_kang2 小时前
MySQL锁表诊断与解决方案
android·mysql·mariadb
未来之窗软件服务2 小时前
CICD 信发系统自动打包安卓签名apk—无相无界(7)—东方仙盟
android·仙盟创梦ide·东方仙盟·东方仙盟无相无界
Mr -老鬼2 小时前
零基础玩转 EasyClick+ESP32 OTG有线HID|零权限超高稳定手机操控
android·智能手机