3 Commits

Author SHA1 Message Date
onur 9ced0da0b2 Normalize zero-based firmware satellite indices
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
2026-07-24 12:06:09 +03:00
onur 73589204f5 Verify satellite propagation is timezone invariant 2026-07-24 11:58:24 +03:00
onur 6ffa76139c Show RINEX status time in local timezone 2026-07-24 11:50:48 +03:00
6 changed files with 65 additions and 18 deletions
+4 -3
View File
@@ -223,9 +223,10 @@ without logging out.
previous two UTC days if today's stream file is unavailable. Current and next-minute satellite 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 10 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 - 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. line shows the system-local time of the last successful download or cache load.
- Complete UART channel snapshots such as `tracked satellites=5 prns=[...]` replace the previous - Complete UART channel snapshots such as `tracked satellites=5 prn_indices=[...]` replace the
connected-PRN set, so satellites that were lost no longer remain green. previous connected-PRN set, so satellites that were lost no longer remain green. Firmware
channel indices are zero-based and are normalized for display (`0` is GPS PRN `G01`).
- RINEX text is parsed once per download and retained as compact ephemeris records. The static - 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. 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, - The globe uses an embedded Natural Earth coastline mask with terrain-like land colouring,
+22 -8
View File
@@ -234,10 +234,11 @@ QSet<QString> LocationParser::parseGpsPrns(const QString &line)
QSet<QString> result; QSet<QString> result;
const auto addPrn = [&result](QString value, bool zeroBased = false) { const auto addPrn = [&result](QString value, bool zeroBased = false) {
value = value.trimmed().toUpper(); value = value.trimmed().toUpper();
if (value.startsWith(QLatin1Char('G'))) value.remove(0, 1); const bool explicitGpsPrn = value.startsWith(QLatin1Char('G'));
if (explicitGpsPrn) value.remove(0, 1);
bool ok = false; bool ok = false;
int number = value.toInt(&ok); int number = value.toInt(&ok);
if (ok && zeroBased) ++number; if (ok && zeroBased && !explicitGpsPrn) ++number;
if (ok && number >= 1 && number <= 63) { if (ok && number >= 1 && number <= 63) {
result.insert(QStringLiteral("G%1").arg(number, 2, 10, QLatin1Char('0'))); result.insert(QStringLiteral("G%1").arg(number, 2, 10, QLatin1Char('0')));
} }
@@ -249,18 +250,31 @@ QSet<QString> LocationParser::parseGpsPrns(const QString &line)
auto gpsMatches = gpsToken.globalMatch(line); auto gpsMatches = gpsToken.globalMatch(line);
while (gpsMatches.hasNext()) addPrn(gpsMatches.next().captured(QStringLiteral("prn"))); while (gpsMatches.hasNext()) addPrn(gpsMatches.next().captured(QStringLiteral("prn")));
static const QRegularExpression labeledList( // Current firmware versions called their zero-based hardware channel values
QStringLiteral(R"((?<label>\bPRNS?\b|\bSVS?\b|\bSVNS?\b|\bSVIDS?\b)\s*(?:=|:|->)?\s*[\[(]?\s*(?<list>(?:G?\d{1,2})(?:\s*[,;/| ]\s*G?\d{1,2})*)\s*[\])]?)"), // "prns" in complete tracking snapshots. Keep accepting those messages while
// newer firmware uses the unambiguous "prn_indices" label.
static const QRegularExpression legacyTrackingIndices(
QStringLiteral(R"(\btracked\s+satellites\s*=\s*\d+\s+prns?\s*=)"),
QRegularExpression::CaseInsensitiveOption); QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression number(QStringLiteral(R"(G?(\d{1,2}))"), const bool legacyZeroBasedPrns = legacyTrackingIndices.match(line).hasMatch();
static const QRegularExpression labeledList(
QStringLiteral(R"((?<label>\bPRN[_ -]?INDICES\b|\bPRNS?\b|\bSVS?\b|\bSVNS?\b|\bSVIDS?\b)\s*(?:=|:|->)?\s*[\[(]?\s*(?<list>(?:G?\d{1,2})(?:\s*[,;/| ]\s*G?\d{1,2})*)\s*[\])]?)"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression number(QStringLiteral(R"(G?\d{1,2})"),
QRegularExpression::CaseInsensitiveOption); QRegularExpression::CaseInsensitiveOption);
auto listMatches = labeledList.globalMatch(line); auto listMatches = labeledList.globalMatch(line);
while (listMatches.hasNext()) { while (listMatches.hasNext()) {
const QRegularExpressionMatch listMatch = listMatches.next(); const QRegularExpressionMatch listMatch = listMatches.next();
const bool zeroBased = listMatch.captured(QStringLiteral("label")).startsWith( QString label = listMatch.captured(QStringLiteral("label")).toUpper();
QStringLiteral("SVN"), Qt::CaseInsensitive); label.remove(QLatin1Char('_'));
label.remove(QLatin1Char('-'));
label.remove(QLatin1Char(' '));
const bool zeroBased = label.startsWith(QStringLiteral("SVN"))
|| label == QStringLiteral("PRNINDICES")
|| (legacyZeroBasedPrns && label.startsWith(QStringLiteral("PRN")));
auto values = number.globalMatch(listMatch.captured(QStringLiteral("list"))); auto values = number.globalMatch(listMatch.captured(QStringLiteral("list")));
while (values.hasNext()) addPrn(values.next().captured(1), zeroBased); while (values.hasNext()) addPrn(values.next().captured(0), zeroBased);
} }
return result; return result;
} }
+1 -1
View File
@@ -520,7 +520,7 @@ void MainWindow::handleSerialLine(const QString &line)
QRegularExpression::CaseInsensitiveOption); QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression trackingSnapshotRegex( static const QRegularExpression trackingSnapshotRegex(
QStringLiteral( QStringLiteral(
R"(\btracked\s+satellites\s*=\s*\d+\s+(?:prns?|svns?)\s*=\s*\[[^\]]*\])"), R"(\btracked\s+satellites\s*=\s*\d+\s+(?:prn[_ -]?indices|prns?|svns?)\s*=\s*\[[^\]]*\])"),
QRegularExpression::CaseInsensitiveOption); QRegularExpression::CaseInsensitiveOption);
const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch(); const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch();
const bool trackingSnapshot = trackingSnapshotRegex.match(line).hasMatch(); const bool trackingSnapshot = trackingSnapshotRegex.match(line).hasMatch();
+6 -4
View File
@@ -158,10 +158,11 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &
m_ephemerides = std::move(ephemerides); m_ephemerides = std::move(ephemerides);
setLoading(false); setLoading(false);
publishCurrentMotion(); publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 · %3 UTC") emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 · %3")
.arg(positions.size()) .arg(positions.size())
.arg(date.toString(QStringLiteral("dd.MM.yyyy"))) .arg(date.toString(QStringLiteral("dd.MM.yyyy")))
.arg(now.time().toString(QStringLiteral("HH:mm:ss"))), false); .arg(now.toLocalTime().toString(
QStringLiteral("HH:mm:ss t"))), false);
} }
bool RinexNavigation::loadCache() bool RinexNavigation::loadCache()
@@ -176,9 +177,10 @@ bool RinexNavigation::loadCache()
if (positions.isEmpty()) return false; if (positions.isEmpty()) return false;
m_ephemerides = std::move(ephemerides); m_ephemerides = std::move(ephemerides);
publishCurrentMotion(); publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX · %2 UTC") emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX · %2")
.arg(positions.size()) .arg(positions.size())
.arg(now.time().toString(QStringLiteral("HH:mm:ss"))), true); .arg(now.toLocalTime().toString(
QStringLiteral("HH:mm:ss t"))), true);
return true; return true;
} }
+2 -2
View File
@@ -89,13 +89,13 @@ int main(int argc, char **argv)
assert(sideSplitter != nullptr); assert(sideSplitter != nullptr);
assert(sideSplitter->orientation() == Qt::Vertical); assert(sideSplitter->orientation() == Qt::Vertical);
sendLine(window, QStringLiteral("[TRK] tracked satellites=2 prns=[3,8]")); sendLine(window, QStringLiteral("[TRK] tracked satellites=2 prn_indices=[2,7]"));
assert(map != nullptr); assert(map != nullptr);
auto *globe = window.findChild<GlobeWidget *>(); auto *globe = window.findChild<GlobeWidget *>();
assert(globe != nullptr); assert(globe != nullptr);
assert(globe->observedPrns() assert(globe->observedPrns()
== QSet<QString>({QStringLiteral("G03"), QStringLiteral("G08")})); == QSet<QString>({QStringLiteral("G03"), QStringLiteral("G08")}));
sendLine(window, QStringLiteral("[TRK] tracked satellites=1 prns=[14]")); sendLine(window, QStringLiteral("[TRK] tracked satellites=1 prn_indices=[13]"));
assert(globe->observedPrns() == QSet<QString>({QStringLiteral("G14")})); assert(globe->observedPrns() == QSet<QString>({QStringLiteral("G14")}));
assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection, assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection,
+30
View File
@@ -186,6 +186,25 @@ int main(int argc, char **argv)
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G32")})); assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G32")}));
} }
{
const QSet<QString> prns = LocationParser::parseGpsPrns(
QStringLiteral("[TRK] tracked satellites=3 prn_indices=[0,7,31]"));
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G08"),
QStringLiteral("G32")}));
}
{
const QSet<QString> prns = LocationParser::parseGpsPrns(
QStringLiteral("[TRK] tracked satellites=2 prns=[0,7]"));
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G08")}));
}
{
const QSet<QString> prns = LocationParser::parseGpsPrns(
QStringLiteral("[TRK] tracked satellites=2 prn_indices=[0,G08]"));
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G08")}));
}
{ {
const auto parsed = LocationParser::parseLine(QStringLiteral( const auto parsed = LocationParser::parseLine(QStringLiteral(
"[OK] [NAV] DOP GDOP=7.0907891622 PDOP=5.6333862175 " "[OK] [NAV] DOP GDOP=7.0907891622 PDOP=5.6333862175 "
@@ -257,6 +276,17 @@ int main(int argc, char **argv)
assert(closeTo(satellites[0].x, semiMajorAxis * std::cos(node), 0.1)); 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].y, semiMajorAxis * std::sin(node), 0.1));
assert(closeTo(satellites[0].z, 0.0, 0.1)); assert(closeTo(satellites[0].z, 0.0, 0.1));
// A local wall-clock representation must not move the satellites.
// 11:48 at UTC+03 and 08:48 UTC represent the same physical instant.
const auto localTimeSatellites = RinexNavigation::parseGpsNavigation(
syntheticGpsNavigation(gpsLabel, gpsWeek, toe),
matchingUtc.toOffsetFromUtc(3 * 60 * 60), &error);
assert(error.isEmpty());
assert(localTimeSatellites.size() == satellites.size());
assert(closeTo(localTimeSatellites[0].x, satellites[0].x, 0.001));
assert(closeTo(localTimeSatellites[0].y, satellites[0].y, 0.001));
assert(closeTo(localTimeSatellites[0].z, satellites[0].z, 0.001));
} }
const QString rinexPath = qEnvironmentVariable("LOCATION_PLOTTER_TEST_RINEX"); const QString rinexPath = qEnvironmentVariable("LOCATION_PLOTTER_TEST_RINEX");