From 24b08ee5180f70c3b9f1b8701d471a3d56e6643f Mon Sep 17 00:00:00 2001 From: onur Date: Thu, 23 Jul 2026 23:19:52 +0300 Subject: [PATCH] Improve satellite view and fused location telemetry --- CMakeLists.txt | 8 +- README.md | 11 +- src/GlobeWidget.cpp | 202 ++++++++++++++++++++----------- src/GlobeWidget.h | 11 +- src/LocationParser.cpp | 20 ++++ src/MainWindow.cpp | 70 +++++++++-- src/MainWindow.h | 1 + src/RinexNavigation.cpp | 233 ++++++++++++++++++++---------------- src/RinexNavigation.h | 34 +++++- src/TelemetryPoint.h | 10 ++ tests/main_window_tests.cpp | 16 +++ tests/parser_tests.cpp | 58 ++++++++- 12 files changed, 486 insertions(+), 188 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ed9a55a..cb584d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(location_plotter VERSION 0.2.0 LANGUAGES CXX) +project(location_plotter VERSION 0.3.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -62,6 +62,9 @@ add_executable(location_parser_tests src/TelemetryPoint.h ) target_link_libraries(location_parser_tests PRIVATE Qt6::Core Qt6::Network ZLIB::ZLIB) +target_compile_options(location_parser_tests PRIVATE + $<$:-UNDEBUG> +) add_test(NAME location_parser_tests COMMAND location_parser_tests) add_executable(location_main_window_tests @@ -82,6 +85,9 @@ add_executable(location_main_window_tests src/TelemetryPoint.h ) target_link_libraries(location_main_window_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB) +target_compile_options(location_main_window_tests PRIVATE + $<$:-UNDEBUG> +) qt_add_resources(location_main_window_tests "test_app_resources" PREFIX "/" FILES resources/ankasys-logo.png resources/land-110m.json diff --git a/README.md b/README.md index 6e490b2..dcbf021 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,8 @@ ancak son gecerli RINEX verisi ve harita onbellegiyle cevrimdisi kullanim da des Port secici Linux'ta `/dev/ttyUSB*` ve `/dev/ttyACM*`; macOS'ta USB ile ilgili `/dev/cu.*` ve `/dev/tty.*` aygitlarini listeler. Hic aygit algilanmazsa Linux'ta `/dev/ttyUSB2`, macOS'ta `/dev/cu.usbserial` duzenlenebilir bir varsayilan yol olarak -gosterilir. Varsayilan baud rate `115200`'dur. +gosterilir. 50 Hz IMU-duzeltilmis konum akisini kayipsiz tasimak icin varsayilan baud +rate `460800`'dur. If the port cannot be opened, make sure the user has serial access: @@ -216,7 +217,13 @@ without logging out. receiver marker without resetting a globe orientation chosen by dragging. - RINEX download starts at every launch, repeats every 15 minutes, and falls back through the previous two UTC days if today's stream file is unavailable. Current and next-minute satellite - positions are propagated from the cached ephemeris, then smoothly interpolated at 30 FPS. + positions are propagated from the cached ephemeris, then smoothly interpolated at 10 FPS. +- The 3D Earth tab includes a `RINEX Guncelle` button for an immediate manual refresh. Its status + line shows the UTC time of the last successful download or cache load. +- Complete UART channel snapshots such as `tracked satellites=5 prns=[...]` replace the previous + connected-PRN set, so satellites that were lost no longer remain green. +- RINEX text is parsed once per download and retained as compact ephemeris records. The static + globe is cached between camera changes, reducing CPU and allocation load during orbit animation. - The globe uses an embedded Natural Earth coastline mask with terrain-like land colouring, shallow-water edges, atmospheric shading, and a cached Earth-fixed texture for smooth rotation. - The main content width and the right-side dashboard/UART height are both adjustable by diff --git a/src/GlobeWidget.cpp b/src/GlobeWidget.cpp index 35c4596..6b65e5d 100644 --- a/src/GlobeWidget.cpp +++ b/src/GlobeWidget.cpp @@ -15,6 +15,7 @@ #include #include +#include namespace { constexpr double Pi = 3.14159265358979323846; @@ -211,8 +212,10 @@ GlobeWidget::GlobeWidget(QWidget *parent) setMouseTracking(true); setCursor(Qt::OpenHandCursor); setToolTip(QStringLiteral("Dunyayi dondurmek icin surukleyin, yakinlastirmak icin tekerlegi kullanin")); - m_satelliteAnimationTimer.setInterval(33); - m_satelliteAnimationTimer.setTimerType(Qt::PreciseTimer); + // GNSS satellites move only a few screen pixels per minute. Ten frames per + // second is visually smooth here and avoids repainting a static globe at 30 FPS. + m_satelliteAnimationTimer.setInterval(100); + m_satelliteAnimationTimer.setTimerType(Qt::CoarseTimer); connect(&m_satelliteAnimationTimer, &QTimer::timeout, this, &GlobeWidget::updateSatelliteAnimation); } @@ -235,8 +238,29 @@ void GlobeWidget::setSatelliteMotion(const QVector ¤t, // Continue from the currently rendered points when a fresh ephemeris arrives, // avoiding even a small visual jump at the update boundary. - m_satelliteMotionStart = m_satellites.isEmpty() ? current : m_satellites; - m_satelliteMotionTarget = next; + const QVector &candidateStart = + m_satellites.isEmpty() ? current : m_satellites; + QHash starts; + starts.reserve(candidateStart.size()); + for (const SatellitePosition &satellite : candidateStart) { + starts.insert(satellite.prn, satellite); + } + + m_satelliteMotionStart.clear(); + m_satelliteMotionTarget.clear(); + m_satelliteMotionStart.reserve(next.size()); + m_satelliteMotionTarget.reserve(next.size()); + for (const SatellitePosition &target : next) { + const auto start = starts.constFind(target.prn); + if (start == starts.cend()) continue; + m_satelliteMotionStart.push_back(start.value()); + m_satelliteMotionTarget.push_back(target); + } + if (m_satelliteMotionStart.isEmpty()) { + setSatellites(current); + m_satelliteAnimationTimer.stop(); + return; + } m_satelliteMotionDurationMs = durationMs; m_satelliteMotionClock.restart(); m_satelliteAnimationTimer.start(); @@ -250,18 +274,11 @@ void GlobeWidget::updateSatelliteAnimation() const double progress = std::clamp( static_cast(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs, 0.0, 1.0); - QHash targets; - targets.reserve(m_satelliteMotionTarget.size()); - for (const SatellitePosition &satellite : m_satelliteMotionTarget) { - targets.insert(satellite.prn, satellite); - } - QVector interpolated; interpolated.reserve(m_satelliteMotionStart.size()); - for (const SatellitePosition &start : m_satelliteMotionStart) { - const auto targetIt = targets.constFind(start.prn); - if (targetIt == targets.cend()) continue; - const SatellitePosition &target = targetIt.value(); + for (qsizetype i = 0; i < m_satelliteMotionStart.size(); ++i) { + const SatellitePosition &start = m_satelliteMotionStart[i]; + const SatellitePosition &target = m_satelliteMotionTarget[i]; SatellitePosition point = start; point.x += (target.x - start.x) * progress; point.y += (target.y - start.y) * progress; @@ -336,6 +353,74 @@ void GlobeWidget::paintEvent(QPaintEvent *) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); + const qreal devicePixelRatio = devicePixelRatioF(); + if (m_staticLayer.isNull() || m_staticLayerSize != size() + || !qFuzzyCompare(m_staticLayerDevicePixelRatio, devicePixelRatio) + || m_staticLayerYaw != m_yaw || m_staticLayerPitch != m_pitch + || m_staticLayerZoom != m_zoom) { + rebuildStaticLayer(); + } + painter.drawPixmap(0, 0, m_staticLayer); + + if (m_hasReceiver) drawVisibilityCone(painter); + if (m_hasReceiver) drawReceiver(painter); + const int visibleCount = drawSatellites(painter); + + painter.setPen(QColor(229, 240, 249)); + QFont titleFont = painter.font(); + titleFont.setPixelSize(18); + titleFont.setBold(true); + painter.setFont(titleFont); + painter.drawText(QRect(20, 16, width() - 40, 28), Qt::AlignLeft | Qt::AlignVCenter, + QStringLiteral("GNSS YORUNGE GORUNUMU")); + QFont smallFont = painter.font(); + smallFont.setPixelSize(11); + smallFont.setBold(false); + painter.setFont(smallFont); + painter.setPen(m_statusError ? QColor(255, 177, 77) : QColor(132, 177, 204)); + painter.drawText(QRect(20, 43, width() - 40, 22), Qt::AlignLeft | Qt::AlignVCenter, m_status); + + const int legendY = height() - 25; + const int legendX = width() / 2 - 125; + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(42, 224, 137)); + painter.drawEllipse(QPointF(legendX, legendY - 3), 4, 4); + painter.setPen(QColor(101, 239, 176)); + painter.drawText(legendX + 9, legendY + 1, QStringLiteral("BULUNDU")); + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(255, 79, 95)); + painter.drawEllipse(QPointF(legendX + 78, legendY - 3), 4, 4); + painter.setPen(QColor(255, 132, 143)); + painter.drawText(legendX + 87, legendY + 1, QStringLiteral("BULUNAMADI")); + painter.setPen(QColor(138, 183, 207)); + painter.drawText(legendX + 181, legendY + 1, + QStringLiteral("%1/%2 PRN · ELEV≥%3°: %4") + .arg(m_observedPrns.size()).arg(m_satellites.size()) + .arg(m_elevationMaskDegrees, 0, 'f', 1).arg(visibleCount)); + + if (!m_hasReceiver) { + painter.setPen(QColor(219, 232, 241)); + QFont waitingFont = painter.font(); + waitingFont.setPixelSize(16); + waitingFont.setBold(true); + painter.setFont(waitingFont); + painter.drawText(rect().adjusted(30, 70, -30, -50), Qt::AlignCenter, + QStringLiteral("Ilk gecerli konum bekleniyor")); + } +} + +void GlobeWidget::rebuildStaticLayer() +{ + const qreal devicePixelRatio = devicePixelRatioF(); + const QSize pixelSize( + std::max(1, qRound(width() * devicePixelRatio)), + std::max(1, qRound(height() * devicePixelRatio))); + QPixmap layer(pixelSize); + layer.setDevicePixelRatio(devicePixelRatio); + layer.fill(Qt::transparent); + QPainter painter(&layer); + painter.setRenderHint(QPainter::Antialiasing, true); + QLinearGradient background(0, 0, width(), height()); background.setColorAt(0.0, QColor(3, 10, 21)); background.setColorAt(0.55, QColor(8, 24, 43)); @@ -374,63 +459,23 @@ void GlobeWidget::paintEvent(QPaintEvent *) painter.restore(); for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0); - if (m_hasReceiver) drawVisibilityCone(painter); - if (m_hasReceiver) drawReceiver(painter); - drawSatellites(painter); + painter.end(); - painter.setPen(QColor(229, 240, 249)); - QFont titleFont = painter.font(); - titleFont.setPixelSize(18); - titleFont.setBold(true); - painter.setFont(titleFont); - painter.drawText(QRect(20, 16, width() - 40, 28), Qt::AlignLeft | Qt::AlignVCenter, - QStringLiteral("GNSS YORUNGE GORUNUMU")); - QFont smallFont = painter.font(); - smallFont.setPixelSize(11); - smallFont.setBold(false); - painter.setFont(smallFont); - painter.setPen(m_statusError ? QColor(255, 177, 77) : QColor(132, 177, 204)); - painter.drawText(QRect(20, 43, width() - 40, 22), Qt::AlignLeft | Qt::AlignVCenter, m_status); - - const int legendY = height() - 25; - const int legendX = width() / 2 - 125; - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(42, 224, 137)); - painter.drawEllipse(QPointF(legendX, legendY - 3), 4, 4); - painter.setPen(QColor(101, 239, 176)); - painter.drawText(legendX + 9, legendY + 1, QStringLiteral("BULUNDU")); - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(255, 79, 95)); - painter.drawEllipse(QPointF(legendX + 78, legendY - 3), 4, 4); - painter.setPen(QColor(255, 132, 143)); - painter.drawText(legendX + 87, legendY + 1, QStringLiteral("BULUNAMADI")); - painter.setPen(QColor(138, 183, 207)); - int visibleCount = 0; - for (const auto &satellite : m_satellites) { - if (satelliteElevation(satellite) >= m_elevationMaskDegrees) ++visibleCount; - } - painter.drawText(legendX + 181, legendY + 1, - QStringLiteral("%1/%2 PRN · ELEV≥%3°: %4") - .arg(m_observedPrns.size()).arg(m_satellites.size()) - .arg(m_elevationMaskDegrees, 0, 'f', 1).arg(visibleCount)); - - if (!m_hasReceiver) { - painter.setPen(QColor(219, 232, 241)); - QFont waitingFont = painter.font(); - waitingFont.setPixelSize(16); - waitingFont.setBold(true); - painter.setFont(waitingFont); - painter.drawText(rect().adjusted(30, 70, -30, -50), Qt::AlignCenter, - QStringLiteral("Ilk gecerli konum bekleniyor")); - } + m_staticLayer = std::move(layer); + m_staticLayerSize = size(); + m_staticLayerDevicePixelRatio = devicePixelRatio; + m_staticLayerYaw = m_yaw; + m_staticLayerPitch = m_pitch; + m_staticLayerZoom = m_zoom; } void GlobeWidget::drawContinents(QPainter &painter) const { const double radius = std::min(width(), height()) * 0.255 * m_zoom; - const double pixelRatio = std::min(devicePixelRatioF(), 1.5); + const double pixelRatio = m_dragging ? 0.75 : std::min(devicePixelRatioF(), 1.5); const int textureSize = std::clamp( - static_cast(std::ceil(radius * 2.0 * pixelRatio)), 320, 900); + static_cast(std::ceil(radius * 2.0 * pixelRatio)), + m_dragging ? 240 : 320, m_dragging ? 480 : 900); const QPointF center(width() * 0.5, height() * 0.52); const QRectF target(center.x() - radius, center.y() - radius, radius * 2.0, radius * 2.0); @@ -715,33 +760,41 @@ void GlobeWidget::drawReceiver(QPainter &painter) const painter.drawText(p.point + QPointF(10, -8), QStringLiteral("ALICI")); } -void GlobeWidget::drawSatellites(QPainter &painter) const +int GlobeWidget::drawSatellites(QPainter &painter) const { - if (!m_hasReceiver) return; + if (!m_hasReceiver) return 0; const double orbitRadius = std::min(width(), height()) * 0.255 * m_zoom * 1.47; - struct Item { SatellitePosition satellite; ProjectedPoint projected; }; + struct Item { + SatellitePosition satellite; + ProjectedPoint projected; + double elevation = -90.0; + }; QVector items; + items.reserve(m_satellites.size()); for (const auto &satellite : m_satellites) { const double length = std::sqrt(satellite.x * satellite.x + satellite.y * satellite.y + satellite.z * satellite.z); if (length <= 0.0) continue; items.push_back({satellite, project(satellite.x / length, satellite.y / length, - satellite.z / length, orbitRadius)}); + satellite.z / length, orbitRadius), + satelliteElevation(satellite)}); } std::sort(items.begin(), items.end(), [](const Item &a, const Item &b) { return a.projected.depth < b.projected.depth; }); + const double earthRadius = std::min(width(), height()) * 0.255 * m_zoom; + const Vec3 receiver = globeVector(m_receiverLatitude, m_receiverLongitude); + const auto receiverPoint = project(receiver.x, receiver.y, receiver.z, + earthRadius * 1.02); + int visibleCount = 0; for (const Item &item : items) { const bool found = m_observedPrns.contains(item.satellite.prn); - const bool inElevationMask = satelliteElevation(item.satellite) >= m_elevationMaskDegrees; + const bool inElevationMask = item.elevation >= m_elevationMaskDegrees; + if (inElevationMask) ++visibleCount; QColor color = found ? QColor(42, 224, 137) : QColor(255, 79, 95); if (item.projected.depth < 0.0) color.setAlpha(75); const double markerRadius = found ? 6.5 : 5.0; if (inElevationMask) { - const double earthRadius = std::min(width(), height()) * 0.255 * m_zoom; - const Vec3 receiver = globeVector(m_receiverLatitude, m_receiverLongitude); - const auto receiverPoint = project(receiver.x, receiver.y, receiver.z, - earthRadius * 1.02); QColor sightLine(70, 224, 231, item.projected.depth < 0.0 ? 25 : 80); painter.setPen(QPen(sightLine, 0.8, Qt::DashLine)); painter.drawLine(receiverPoint.point, item.projected.point); @@ -757,6 +810,7 @@ void GlobeWidget::drawSatellites(QPainter &painter) const painter.drawText(item.projected.point + QPointF(7, -6), item.satellite.prn); } } + return visibleCount; } double GlobeWidget::satelliteElevation(const SatellitePosition &satellite) const @@ -802,7 +856,9 @@ void GlobeWidget::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { m_dragging = false; + m_staticLayer = QPixmap(); setCursor(Qt::OpenHandCursor); + update(); } } diff --git a/src/GlobeWidget.h b/src/GlobeWidget.h index 707594a..0164b18 100644 --- a/src/GlobeWidget.h +++ b/src/GlobeWidget.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ public: void setReceiverLocation(double latitude, double longitude); void clearReceiverLocation(); void setRinexStatus(const QString &status, bool error); + [[nodiscard]] QSet observedPrns() const { return m_observedPrns; } public slots: void setElevationMask(double degrees); @@ -47,9 +49,10 @@ private: void drawVisibilityCone(QPainter &painter) const; void drawGraticule(QPainter &painter) const; void drawReceiver(QPainter &painter) const; - void drawSatellites(QPainter &painter) const; + int drawSatellites(QPainter &painter) const; double satelliteElevation(const SatellitePosition &satellite) const; void updateSatelliteAnimation(); + void rebuildStaticLayer(); QVector m_satellites; QVector m_satelliteMotionStart; @@ -60,6 +63,12 @@ private: mutable QImage m_landTexture; mutable double m_landTextureYaw = 0.0; mutable double m_landTexturePitch = 0.0; + QPixmap m_staticLayer; + QSize m_staticLayerSize; + qreal m_staticLayerDevicePixelRatio = 0.0; + double m_staticLayerYaw = 0.0; + double m_staticLayerPitch = 0.0; + double m_staticLayerZoom = 0.0; QSet m_observedPrns; QString m_status = QStringLiteral("Konum bekleniyor"); bool m_statusError = false; diff --git a/src/LocationParser.cpp b/src/LocationParser.cpp index bb6621a..f60ff53 100644 --- a/src/LocationParser.cpp +++ b/src/LocationParser.cpp @@ -103,6 +103,26 @@ std::optional LocationParser::parseLine(const QString &line) telemetry.source = LocationSource::Gps; } } + static const QRegularExpression fusionModeRegex( + QStringLiteral(R"((?:^|\s)mode\s*=\s*(?gps[_-]?aided|dead[_-]?reckoning)(?=$|\s))"), + QRegularExpression::CaseInsensitiveOption); + const QRegularExpressionMatch fusionModeMatch = fusionModeRegex.match(trimmed); + if (fusionModeMatch.hasMatch()) { + telemetry.fusionMode = fusionModeMatch.captured(QStringLiteral("mode")).startsWith( + QStringLiteral("dead"), Qt::CaseInsensitive) + ? FusionMode::DeadReckoning : FusionMode::GpsAided; + } + static const QRegularExpression gpsAgeRegex( + QStringLiteral(R"((?:^|\s)gps[_-]?age[_-]?ms\s*=\s*(?\d+)(?=$|\s))"), + QRegularExpression::CaseInsensitiveOption); + const QRegularExpressionMatch gpsAgeMatch = gpsAgeRegex.match(trimmed); + if (gpsAgeMatch.hasMatch()) { + bool ok = false; + const qint64 ageMs = gpsAgeMatch.captured(QStringLiteral("age")).toLongLong(&ok); + if (ok) { + telemetry.gpsAgeMs = ageMs; + } + } bool hadCoordinateField = false; bool hadLatitudeField = false; bool hadLongitudeField = false; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 0abe336..8e861da 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -307,7 +307,7 @@ public: int columnCount(const QModelIndex &parent = QModelIndex()) const override { - return parent.isValid() ? 0 : 7; + return parent.isValid() ? 0 : 8; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override @@ -327,6 +327,14 @@ public: case 0: return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz")); case 1: + if (point.source == LocationSource::ImuCorrected + && point.fusionMode == FusionMode::DeadReckoning) { + return QStringLiteral("IMU corrected (DR)"); + } + if (point.source == LocationSource::ImuCorrected + && point.fusionMode == FusionMode::GpsAided) { + return QStringLiteral("IMU corrected (GPS)"); + } return locationSourceText(point.source); case 2: return coordinateText(point.latitude, point.latitudeText); @@ -338,6 +346,10 @@ public: return optionalIntText(point.satellites); case 6: return optionalDopText(point.dop.pdop); + case 7: + return point.gpsAgeMs.has_value() + ? QStringLiteral("%1 ms").arg(*point.gpsAgeMs) + : QStringLiteral("-"); default: return {}; } @@ -351,7 +363,8 @@ public: } static const QStringList headers = { QStringLiteral("Saat"), QStringLiteral("Kaynak"), QStringLiteral("Lat"), QStringLiteral("Long"), - QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP")}; + QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP"), + QStringLiteral("GPS yas")}; return section >= 0 && section < headers.size() ? headers[section] : QVariant(); } @@ -386,6 +399,14 @@ MainWindow::MainWindow(QWidget *parent) m_globe->setRinexStatus(status, error); statusBar()->showMessage(status, error ? 8000 : 4000); }); + connect(m_rinex, &RinexNavigation::loadingChanged, this, [this](bool loading) { + m_refreshRinexButton->setEnabled(!loading); + m_refreshRinexButton->setText( + loading ? QStringLiteral("RINEX indiriliyor...") + : QStringLiteral("RINEX Guncelle")); + }); + connect(m_refreshRinexButton, &QPushButton::clicked, + m_rinex, &RinexNavigation::refresh); m_rinex->refresh(); auto *rinexRefreshTimer = new QTimer(this); rinexRefreshTimer->setInterval(15 * 60 * 1000); @@ -497,15 +518,24 @@ void MainWindow::handleSerialLine(const QString &line) static const QRegularExpression solveStartRegex( QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"), QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression trackingSnapshotRegex( + QStringLiteral( + R"(\btracked\s+satellites\s*=\s*\d+\s+(?:prns?|svns?)\s*=\s*\[[^\]]*\])"), + QRegularExpression::CaseInsensitiveOption); const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch(); + const bool trackingSnapshot = trackingSnapshotRegex.match(line).hasMatch(); if (solveStarted) { m_observedPrns.clear(); } const QSet linePrns = LocationParser::parseGpsPrns(line); - if (!linePrns.isEmpty()) { + if (trackingSnapshot) { + // This line is a complete point-in-time channel snapshot. Replacing + // prevents satellites from older solves remaining green indefinitely. + m_observedPrns = linePrns; + } else if (!linePrns.isEmpty()) { m_observedPrns.unite(linePrns); } - if (solveStarted || !linePrns.isEmpty()) { + if (solveStarted || trackingSnapshot || !linePrns.isEmpty()) { m_globe->setObservedPrns(m_observedPrns); } @@ -809,7 +839,7 @@ void MainWindow::buildUi() for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) { m_baudCombo->addItem(QString::number(baud)); } - m_baudCombo->setCurrentText(QStringLiteral("115200")); + m_baudCombo->setCurrentText(QStringLiteral("460800")); m_maxPointsSpin = new QSpinBox(this); m_maxPointsSpin->setObjectName(QStringLiteral("maxPointsInput")); @@ -856,7 +886,25 @@ void MainWindow::buildUi() auto *viewTabs = new QTabWidget(this); viewTabs->setObjectName(QStringLiteral("mainViewTabs")); viewTabs->addTab(m_map, QStringLiteral("2B Harita")); - viewTabs->addTab(m_globe, QStringLiteral("3B Dunya · Uydular")); + + auto *globeTab = new QWidget(viewTabs); + auto *globeTabLayout = new QVBoxLayout(globeTab); + globeTabLayout->setContentsMargins(0, 0, 0, 0); + globeTabLayout->setSpacing(6); + auto *rinexToolbar = new QWidget(globeTab); + auto *rinexToolbarLayout = new QHBoxLayout(rinexToolbar); + rinexToolbarLayout->setContentsMargins(10, 6, 10, 0); + rinexToolbarLayout->addWidget(new QLabel( + QStringLiteral("GPS yayin efemerisi · otomatik: 15 dk"), rinexToolbar)); + rinexToolbarLayout->addStretch(1); + m_refreshRinexButton = new QPushButton(QStringLiteral("RINEX Guncelle"), rinexToolbar); + m_refreshRinexButton->setObjectName(QStringLiteral("refreshRinexButton")); + m_refreshRinexButton->setToolTip( + QStringLiteral("Guncel RINEX navigasyon dosyasini simdi indir")); + rinexToolbarLayout->addWidget(m_refreshRinexButton); + globeTabLayout->addWidget(rinexToolbar); + globeTabLayout->addWidget(m_globe, 1); + viewTabs->addTab(globeTab, QStringLiteral("3B Dunya · Uydular")); auto *sidePanel = new QWidget(this); sidePanel->setMinimumWidth(390); @@ -1105,6 +1153,8 @@ void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry) { TelemetryPoint point; point.source = telemetry.source; + point.fusionMode = telemetry.fusionMode; + point.gpsAgeMs = telemetry.gpsAgeMs; point.latitude = *telemetry.latitude; point.longitude = *telemetry.longitude; point.latitudeText = telemetry.latitudeText; @@ -1209,6 +1259,12 @@ void MainWindow::mergePendingTelemetry(const ParsedTelemetry &telemetry) commitPendingTelemetry(); } + if (telemetry.fusionMode != FusionMode::Unknown) { + m_pendingTelemetry.fusionMode = telemetry.fusionMode; + } + if (telemetry.gpsAgeMs.has_value()) { + m_pendingTelemetry.gpsAgeMs = telemetry.gpsAgeMs; + } if (telemetry.latitude.has_value()) { m_pendingTelemetry.source = telemetry.source; m_pendingTelemetry.latitude = telemetry.latitude; @@ -1290,5 +1346,5 @@ int MainWindow::selectedBaudRate() const { bool ok = false; const int baudRate = m_baudCombo->currentText().toInt(&ok); - return ok ? baudRate : 115200; + return ok ? baudRate : 460800; } diff --git a/src/MainWindow.h b/src/MainWindow.h index 3f6ee15..2d78d8d 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -74,6 +74,7 @@ private: QPushButton *m_connectButton = nullptr; QPushButton *m_clearButton = nullptr; QPushButton *m_openLogButton = nullptr; + QPushButton *m_refreshRinexButton = nullptr; QCheckBox *m_autoFollowCheck = nullptr; QCheckBox *m_showTelemetryCheck = nullptr; QCheckBox *m_showRecentPointsCheck = nullptr; diff --git a/src/RinexNavigation.cpp b/src/RinexNavigation.cpp index a823dcc..4cff428 100644 --- a/src/RinexNavigation.cpp +++ b/src/RinexNavigation.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include namespace { @@ -22,29 +24,6 @@ constexpr double HalfGpsWeek = GpsWeekSeconds / 2.0; constexpr double CurrentGpsUtcOffsetSeconds = 18.0; constexpr int PositionUpdateIntervalMs = 60000; -struct Ephemeris { - QString prn; - QDateTime toc; - double crs = 0.0; - double deltaN = 0.0; - double m0 = 0.0; - double cuc = 0.0; - double eccentricity = 0.0; - double cus = 0.0; - double sqrtA = 0.0; - double toe = 0.0; - double cic = 0.0; - double omega0 = 0.0; - double cis = 0.0; - double i0 = 0.0; - double crc = 0.0; - double omega = 0.0; - double omegaDot = 0.0; - double idot = 0.0; - int gpsWeek = 0; - bool healthy = true; -}; - double parseField(const QString &line, int field) { QString text = line.mid(4 + field * 19, 19).trimmed(); @@ -93,49 +72,6 @@ QDateTime gpsEpoch() return QDateTime(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC); } -SatellitePosition propagate(const Ephemeris &eph, const QDateTime &whenUtc) -{ - const double gpsSeconds = gpsEpoch().msecsTo(whenUtc) / 1000.0 - + CurrentGpsUtcOffsetSeconds; - const double absoluteToe = eph.gpsWeek * GpsWeekSeconds + eph.toe; - double tk = gpsSeconds - absoluteToe; - while (tk > HalfGpsWeek) tk -= GpsWeekSeconds; - while (tk < -HalfGpsWeek) tk += GpsWeekSeconds; - - const double a = eph.sqrtA * eph.sqrtA; - const double meanMotion = std::sqrt(GpsMu / (a * a * a)) + eph.deltaN; - const double meanAnomaly = eph.m0 + meanMotion * tk; - double eccentricAnomaly = meanAnomaly; - for (int iteration = 0; iteration < 12; ++iteration) { - eccentricAnomaly = meanAnomaly + eph.eccentricity * std::sin(eccentricAnomaly); - } - - const double trueAnomaly = std::atan2( - std::sqrt(1.0 - eph.eccentricity * eph.eccentricity) * std::sin(eccentricAnomaly), - std::cos(eccentricAnomaly) - eph.eccentricity); - const double phi = trueAnomaly + eph.omega; - const double twoPhi = 2.0 * phi; - const double u = phi + eph.cus * std::sin(twoPhi) + eph.cuc * std::cos(twoPhi); - const double radius = a * (1.0 - eph.eccentricity * std::cos(eccentricAnomaly)) - + eph.crs * std::sin(twoPhi) + eph.crc * std::cos(twoPhi); - const double inclination = eph.i0 + eph.idot * tk - + eph.cis * std::sin(twoPhi) + eph.cic * std::cos(twoPhi); - const double orbitalX = radius * std::cos(u); - const double orbitalY = radius * std::sin(u); - const double ascendingNode = eph.omega0 + (eph.omegaDot - EarthRotation) * tk - - EarthRotation * eph.toe; - - SatellitePosition result; - result.prn = eph.prn; - result.x = orbitalX * std::cos(ascendingNode) - - orbitalY * std::cos(inclination) * std::sin(ascendingNode); - result.y = orbitalX * std::sin(ascendingNode) - + orbitalY * std::cos(inclination) * std::cos(ascendingNode); - result.z = orbitalY * std::sin(inclination); - result.healthy = eph.healthy; - return result; -} - } // namespace RinexNavigation::RinexNavigation(QObject *parent) @@ -150,10 +86,10 @@ RinexNavigation::RinexNavigation(QObject *parent) void RinexNavigation::refresh() { if (m_loading) return; - if (m_latestRinex.isEmpty()) { + if (m_ephemerides.isEmpty()) { loadCache(); } - m_loading = true; + setLoading(true); emit statusChanged(QStringLiteral("Guncel RINEX efemerisi indiriliyor..."), false); requestDay(QDateTime::currentDateTimeUtc().date(), 0); } @@ -181,8 +117,13 @@ void RinexNavigation::requestDay(const QDate &date, int fallbackIndex) if (fallbackIndex < 2) { requestDay(date.addDays(-1), fallbackIndex + 1); } else { - m_loading = false; - if (!loadCache()) { + setLoading(false); + if (!m_ephemerides.isEmpty()) { + publishCurrentMotion(); + emit statusChanged( + QStringLiteral("RINEX indirilemedi; mevcut efemeris kullaniliyor"), + true); + } else if (!loadCache()) { emit statusChanged(QStringLiteral("RINEX indirilemedi; internet baglantisini kontrol edin"), true); } } @@ -197,14 +138,16 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate & { const QByteArray rinex = gunzip(compressed); QString error; - const QVector positions = parseGpsNavigation( - rinex, QDateTime::currentDateTimeUtc(), &error); + QVector ephemerides = parseEphemerides(rinex, &error); + const QDateTime now = QDateTime::currentDateTimeUtc(); + const QVector positions = propagateEphemerides( + selectEphemerides(ephemerides, now), now); if (positions.isEmpty()) { if (fallbackIndex < 2) { requestDay(date.addDays(-1), fallbackIndex + 1); return; } - m_loading = false; + setLoading(false); emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true); return; } @@ -212,11 +155,13 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate & QFile cache(cachePath()); QDir().mkpath(QFileInfo(cache).absolutePath()); if (cache.open(QIODevice::WriteOnly)) cache.write(rinex); - m_latestRinex = rinex; - m_loading = false; + m_ephemerides = std::move(ephemerides); + setLoading(false); publishCurrentMotion(); - emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 (UTC)") - .arg(positions.size()).arg(date.toString(QStringLiteral("dd.MM.yyyy"))), false); + emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 · %3 UTC") + .arg(positions.size()) + .arg(date.toString(QStringLiteral("dd.MM.yyyy"))) + .arg(now.time().toString(QStringLiteral("HH:mm:ss"))), false); } bool RinexNavigation::loadCache() @@ -224,12 +169,16 @@ bool RinexNavigation::loadCache() QFile cache(cachePath()); if (!cache.open(QIODevice::ReadOnly)) return false; QString error; - m_latestRinex = cache.readAll(); - const QVector positions = parseGpsNavigation( - m_latestRinex, QDateTime::currentDateTimeUtc(), &error); + QVector ephemerides = parseEphemerides(cache.readAll(), &error); + const QDateTime now = QDateTime::currentDateTimeUtc(); + const QVector positions = propagateEphemerides( + selectEphemerides(ephemerides, now), now); if (positions.isEmpty()) return false; + m_ephemerides = std::move(ephemerides); publishCurrentMotion(); - emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX").arg(positions.size()), true); + emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX · %2 UTC") + .arg(positions.size()) + .arg(now.time().toString(QStringLiteral("HH:mm:ss"))), true); return true; } @@ -241,28 +190,36 @@ QString RinexNavigation::cachePath() const void RinexNavigation::updateCurrentPositions() { - if (m_latestRinex.isEmpty()) return; + if (m_ephemerides.isEmpty()) return; publishCurrentMotion(); } void RinexNavigation::publishCurrentMotion() { - if (m_latestRinex.isEmpty()) return; + if (m_ephemerides.isEmpty()) return; const QDateTime now = QDateTime::currentDateTimeUtc(); - const QVector current = parseGpsNavigation(m_latestRinex, now); + const QVector selected = selectEphemerides(m_ephemerides, now); + const QVector current = propagateEphemerides(selected, now); if (current.isEmpty()) return; - const QVector next = parseGpsNavigation( - m_latestRinex, now.addMSecs(PositionUpdateIntervalMs)); + const QVector next = propagateEphemerides( + selected, now.addMSecs(PositionUpdateIntervalMs)); emit satellitesUpdated(current); if (!next.isEmpty()) { emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs); } } -QVector RinexNavigation::parseGpsNavigation( - const QByteArray &rinex, const QDateTime &whenUtc, QString *error) +void RinexNavigation::setLoading(bool loading) +{ + if (m_loading == loading) return; + m_loading = loading; + emit loadingChanged(loading); +} + +QVector RinexNavigation::parseEphemerides( + const QByteArray &rinex, QString *error) { const QString text = QString::fromLatin1(rinex); const QStringList lines = text.split('\n'); @@ -274,8 +231,8 @@ QVector RinexNavigation::parseGpsNavigation( } ++index; - QHash closest; - QHash closestAge; + QVector result; + result.reserve(512); while (index < lines.size()) { const QString first = lines[index]; if (first.size() < 3) { ++index; continue; } @@ -321,19 +278,95 @@ QVector RinexNavigation::parseGpsNavigation( eph.cus, eph.sqrtA, eph.toe, eph.cic, eph.omega0, eph.cis, eph.i0, eph.crc, eph.omega, eph.omegaDot, eph.idot}) && eph.sqrtA > 4000.0 && eph.gpsWeek > 0) { - const qint64 age = std::abs(toc.secsTo(whenUtc)); - if (!closestAge.contains(eph.prn) || age < closestAge[eph.prn]) { - closest[eph.prn] = eph; - closestAge[eph.prn] = age; - } + result.push_back(eph); } index += 8; } - - QVector result; - result.reserve(closest.size()); - for (const Ephemeris &eph : closest) result.push_back(propagate(eph, whenUtc)); - std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { return a.prn < b.prn; }); if (result.isEmpty() && error) *error = QStringLiteral("GPS efemerisi bulunamadi"); return result; } + +QVector RinexNavigation::selectEphemerides( + const QVector &ephemerides, const QDateTime &whenUtc) +{ + QHash closest; + QHash closestAge; + // GPS navigation epochs are labelled in GPS system time, not UTC. + const QDateTime whenGps = whenUtc.addSecs( + static_cast(CurrentGpsUtcOffsetSeconds)); + for (const Ephemeris &eph : ephemerides) { + const qint64 age = std::abs(eph.toc.msecsTo(whenGps)); + if (!closestAge.contains(eph.prn) || age < closestAge[eph.prn]) { + closest[eph.prn] = eph; + closestAge[eph.prn] = age; + } + } + + QVector result; + result.reserve(closest.size()); + for (const Ephemeris &eph : closest) result.push_back(eph); + std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { + return a.prn < b.prn; + }); + return result; +} + +QVector RinexNavigation::propagateEphemerides( + const QVector &ephemerides, const QDateTime &whenUtc) +{ + QVector result; + result.reserve(ephemerides.size()); + for (const Ephemeris &eph : ephemerides) result.push_back(propagate(eph, whenUtc)); + std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { return a.prn < b.prn; }); + return result; +} + +SatellitePosition RinexNavigation::propagate(const Ephemeris &eph, const QDateTime &whenUtc) +{ + const double gpsSeconds = gpsEpoch().msecsTo(whenUtc) / 1000.0 + + CurrentGpsUtcOffsetSeconds; + const double absoluteToe = eph.gpsWeek * GpsWeekSeconds + eph.toe; + double tk = gpsSeconds - absoluteToe; + while (tk > HalfGpsWeek) tk -= GpsWeekSeconds; + while (tk < -HalfGpsWeek) tk += GpsWeekSeconds; + + const double a = eph.sqrtA * eph.sqrtA; + const double meanMotion = std::sqrt(GpsMu / (a * a * a)) + eph.deltaN; + const double meanAnomaly = eph.m0 + meanMotion * tk; + double eccentricAnomaly = meanAnomaly; + for (int iteration = 0; iteration < 12; ++iteration) { + eccentricAnomaly = meanAnomaly + eph.eccentricity * std::sin(eccentricAnomaly); + } + + const double trueAnomaly = std::atan2( + std::sqrt(1.0 - eph.eccentricity * eph.eccentricity) * std::sin(eccentricAnomaly), + std::cos(eccentricAnomaly) - eph.eccentricity); + const double phi = trueAnomaly + eph.omega; + const double twoPhi = 2.0 * phi; + const double u = phi + eph.cus * std::sin(twoPhi) + eph.cuc * std::cos(twoPhi); + const double radius = a * (1.0 - eph.eccentricity * std::cos(eccentricAnomaly)) + + eph.crs * std::sin(twoPhi) + eph.crc * std::cos(twoPhi); + const double inclination = eph.i0 + eph.idot * tk + + eph.cis * std::sin(twoPhi) + eph.cic * std::cos(twoPhi); + const double orbitalX = radius * std::cos(u); + const double orbitalY = radius * std::sin(u); + const double ascendingNode = eph.omega0 + (eph.omegaDot - EarthRotation) * tk + - EarthRotation * eph.toe; + + SatellitePosition result; + result.prn = eph.prn; + result.x = orbitalX * std::cos(ascendingNode) + - orbitalY * std::cos(inclination) * std::sin(ascendingNode); + result.y = orbitalX * std::sin(ascendingNode) + + orbitalY * std::cos(inclination) * std::cos(ascendingNode); + result.z = orbitalY * std::sin(inclination); + result.healthy = eph.healthy; + return result; +} + +QVector RinexNavigation::parseGpsNavigation( + const QByteArray &rinex, const QDateTime &whenUtc, QString *error) +{ + const QVector ephemerides = parseEphemerides(rinex, error); + return propagateEphemerides(selectEphemerides(ephemerides, whenUtc), whenUtc); +} diff --git a/src/RinexNavigation.h b/src/RinexNavigation.h index 7d331d2..4b99f4d 100644 --- a/src/RinexNavigation.h +++ b/src/RinexNavigation.h @@ -26,17 +26,49 @@ signals: const QVector &next, int durationMs); void statusChanged(const QString &status, bool error); + void loadingChanged(bool loading); private: + struct Ephemeris { + QString prn; + QDateTime toc; + double crs = 0.0; + double deltaN = 0.0; + double m0 = 0.0; + double cuc = 0.0; + double eccentricity = 0.0; + double cus = 0.0; + double sqrtA = 0.0; + double toe = 0.0; + double cic = 0.0; + double omega0 = 0.0; + double cis = 0.0; + double i0 = 0.0; + double crc = 0.0; + double omega = 0.0; + double omegaDot = 0.0; + double idot = 0.0; + int gpsWeek = 0; + bool healthy = true; + }; + void requestDay(const QDate &date, int fallbackIndex); void handleDownload(const QByteArray &compressed, const QDate &date, int fallbackIndex); bool loadCache(); QString cachePath() const; void updateCurrentPositions(); void publishCurrentMotion(); + void setLoading(bool loading); + static QVector parseEphemerides( + const QByteArray &rinex, QString *error = nullptr); + static QVector selectEphemerides( + const QVector &ephemerides, const QDateTime &whenUtc); + static QVector propagateEphemerides( + const QVector &ephemerides, const QDateTime &whenUtc); + static SatellitePosition propagate(const Ephemeris &eph, const QDateTime &whenUtc); QNetworkAccessManager m_network; QTimer m_positionTimer; - QByteArray m_latestRinex; + QVector m_ephemerides; bool m_loading = false; }; diff --git a/src/TelemetryPoint.h b/src/TelemetryPoint.h index 437683f..fa50479 100644 --- a/src/TelemetryPoint.h +++ b/src/TelemetryPoint.h @@ -12,6 +12,12 @@ enum class LocationSource { Trajectory }; +enum class FusionMode { + Unknown, + GpsAided, + DeadReckoning +}; + inline QString locationSourceText(LocationSource source) { switch (source) { @@ -43,6 +49,8 @@ struct DopValues { struct TelemetryPoint { LocationSource source = LocationSource::Generic; + FusionMode fusionMode = FusionMode::Unknown; + std::optional gpsAgeMs; double latitude = 0.0; double longitude = 0.0; QString latitudeText; @@ -56,6 +64,8 @@ struct TelemetryPoint { struct ParsedTelemetry { LocationSource source = LocationSource::Generic; + FusionMode fusionMode = FusionMode::Unknown; + std::optional gpsAgeMs; std::optional latitude; std::optional longitude; QString latitudeText; diff --git a/tests/main_window_tests.cpp b/tests/main_window_tests.cpp index bb40f56..e96ed3f 100644 --- a/tests/main_window_tests.cpp +++ b/tests/main_window_tests.cpp @@ -1,4 +1,5 @@ #include "../src/MainWindow.h" +#include "../src/GlobeWidget.h" #include "../src/MapWidget.h" #include @@ -9,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +53,8 @@ int main(int argc, char **argv) auto *showRecentPoints = window.findChild(QStringLiteral("showRecentPointsCheck")); auto *showAverage = window.findChild(QStringLiteral("showAverageCheck")); auto *portSelector = window.findChild(QStringLiteral("portSelector")); + auto *refreshRinexButton = + window.findChild(QStringLiteral("refreshRinexButton")); auto *map = window.findChild(); auto *mainViewTabs = window.findChild(QStringLiteral("mainViewTabs")); auto *sideSplitter = window.findChild(QStringLiteral("sideSplitter")); @@ -69,6 +73,9 @@ int main(int argc, char **argv) assert(showRecentPoints != nullptr); assert(showAverage != nullptr); assert(portSelector != nullptr); + assert(refreshRinexButton != nullptr); + assert(refreshRinexButton->text() == QStringLiteral("RINEX Guncelle") + || refreshRinexButton->text() == QStringLiteral("RINEX indiriliyor...")); assert(portSelector->isEditable()); assert(portSelector->count() >= 1); assert(portSelector->currentText() == QStringLiteral("/dev/test-usb-uart")); @@ -82,6 +89,15 @@ int main(int argc, char **argv) assert(sideSplitter != nullptr); assert(sideSplitter->orientation() == Qt::Vertical); + sendLine(window, QStringLiteral("[TRK] tracked satellites=2 prns=[3,8]")); + assert(map != nullptr); + auto *globe = window.findChild(); + assert(globe != nullptr); + assert(globe->observedPrns() + == QSet({QStringLiteral("G03"), QStringLiteral("G08")})); + sendLine(window, QStringLiteral("[TRK] tracked satellites=1 prns=[14]")); + assert(globe->observedPrns() == QSet({QStringLiteral("G14")})); + assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection, Q_ARG(double, 40.25), Q_ARG(double, 29.5))); assert(referenceLatInput->text() == QStringLiteral("40.250000000000000")); diff --git a/tests/parser_tests.cpp b/tests/parser_tests.cpp index 1b701af..c9d1a1a 100644 --- a/tests/parser_tests.cpp +++ b/tests/parser_tests.cpp @@ -9,9 +9,39 @@ namespace { -bool closeTo(double left, double right) +bool closeTo(double left, double right, double tolerance = 0.000001) { - return std::fabs(left - right) < 0.000001; + return std::fabs(left - right) < tolerance; +} + +QString rinexField(double value) +{ + return QStringLiteral("%1").arg(value, 19, 'E', 12); +} + +QString rinexContinuation(std::initializer_list values) +{ + QString line(4, QLatin1Char(' ')); + for (double value : values) line += rinexField(value); + return line + QLatin1Char('\n'); +} + +QByteArray syntheticGpsNavigation(const QDateTime &gpsEpochLabel, int gpsWeek, double toe) +{ + QString rinex = QStringLiteral( + " 3.05 N: GNSS NAV DATA M: MIXED RINEX VERSION / TYPE\n" + " END OF HEADER\n"); + rinex += QStringLiteral("G01 %1").arg( + gpsEpochLabel.toString(QStringLiteral("yyyy MM dd HH mm ss"))); + rinex += rinexField(0.0) + rinexField(0.0) + rinexField(0.0) + QLatin1Char('\n'); + rinex += rinexContinuation({1.0, 0.0, 0.0, 0.0}); + rinex += rinexContinuation({0.0, 0.0, 0.0, 5153.7954775}); + rinex += rinexContinuation({toe, 0.0, 0.0, 0.0}); + rinex += rinexContinuation({0.0, 0.0, 0.0, 0.0}); + rinex += rinexContinuation({0.0, 0.0, static_cast(gpsWeek), 0.0}); + rinex += rinexContinuation({2.0, 0.0, 0.0, 1.0}); + rinex += rinexContinuation({0.0, 0.0, 0.0, 0.0}); + return rinex.toLatin1(); } } // namespace @@ -181,11 +211,14 @@ int main(int argc, char **argv) { const auto parsed = LocationParser::parseLine(QStringLiteral( - "[LOCATION] source=imu_corrected lat=40.7356 lon=31.5985 height=812.7 " + "[LOCATION] source=imu_corrected mode=dead_reckoning " + "lat=40.7356 lon=31.5985 height=812.7 gps_age_ms=3180 " "gdop=4.7 pdop=4.1 hdop=2.7 vdop=3.0 tdop=2.3 tracked=6 used=5")); assert(parsed.has_value()); assert(parsed->hasLocation()); assert(parsed->source == LocationSource::ImuCorrected); + assert(parsed->fusionMode == FusionMode::DeadReckoning); + assert(parsed->gpsAgeMs == 3180); assert(parsed->satellites == 6); assert(closeTo(*parsed->dop.hdop, 2.7)); } @@ -207,6 +240,25 @@ int main(int argc, char **argv) assert(parsed->satellites == 4); } + { + constexpr int gpsWeek = 2428; + constexpr double toe = 403200.0; + const QDateTime gpsEpoch(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC); + const QDateTime gpsLabel = gpsEpoch.addSecs( + static_cast(gpsWeek * 604800.0 + toe)); + const QDateTime matchingUtc = gpsLabel.addSecs(-18); + QString error; + const auto satellites = RinexNavigation::parseGpsNavigation( + syntheticGpsNavigation(gpsLabel, gpsWeek, toe), matchingUtc, &error); + assert(error.isEmpty()); + assert(satellites.size() == 1); + const double semiMajorAxis = 5153.7954775 * 5153.7954775; + const double node = -7.2921151467e-5 * toe; + assert(closeTo(satellites[0].x, semiMajorAxis * std::cos(node), 0.1)); + assert(closeTo(satellites[0].y, semiMajorAxis * std::sin(node), 0.1)); + assert(closeTo(satellites[0].z, 0.0, 0.1)); + } + const QString rinexPath = qEnvironmentVariable("LOCATION_PLOTTER_TEST_RINEX"); if (!rinexPath.isEmpty()) { QFile file(rinexPath);