4 Commits

Author SHA1 Message Date
Onur Cayan 498757b502 Add source-aware UART log replay
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-08-12 13:54:11 +03:00
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
12 changed files with 777 additions and 21 deletions
+1
View File
@@ -8,3 +8,4 @@ build-*/
*.autosave *.autosave
*.swp *.swp
*~ *~
uart_logs/
+24
View File
@@ -22,6 +22,8 @@ add_executable(location_plotter
src/MapWidget.h src/MapWidget.h
src/SerialReader.cpp src/SerialReader.cpp
src/SerialReader.h src/SerialReader.h
src/UartReplay.cpp
src/UartReplay.h
src/RinexNavigation.cpp src/RinexNavigation.cpp
src/RinexNavigation.h src/RinexNavigation.h
src/SatelliteData.h src/SatelliteData.h
@@ -30,6 +32,9 @@ add_executable(location_plotter
) )
target_link_libraries(location_plotter PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB) target_link_libraries(location_plotter PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
target_compile_definitions(location_plotter PRIVATE
LOCATION_PLOTTER_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
)
if(APPLE) if(APPLE)
set_target_properties(location_plotter PROPERTIES set_target_properties(location_plotter PROPERTIES
@@ -79,12 +84,17 @@ add_executable(location_main_window_tests
src/GlobeWidget.h src/GlobeWidget.h
src/SerialReader.cpp src/SerialReader.cpp
src/SerialReader.h src/SerialReader.h
src/UartReplay.cpp
src/UartReplay.h
src/RinexNavigation.cpp src/RinexNavigation.cpp
src/RinexNavigation.h src/RinexNavigation.h
src/SatelliteData.h src/SatelliteData.h
src/TelemetryPoint.h src/TelemetryPoint.h
) )
target_link_libraries(location_main_window_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB) target_link_libraries(location_main_window_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
target_compile_definitions(location_main_window_tests PRIVATE
LOCATION_PLOTTER_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
)
target_compile_options(location_main_window_tests PRIVATE target_compile_options(location_main_window_tests PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG> $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG>
) )
@@ -99,6 +109,20 @@ add_test(NAME location_main_window_tests COMMAND location_main_window_tests)
set_tests_properties(location_main_window_tests PROPERTIES set_tests_properties(location_main_window_tests PROPERTIES
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-test-cache") ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-test-cache")
add_executable(location_uart_replay_tests
tests/uart_replay_tests.cpp
src/UartReplay.cpp
src/UartReplay.h
src/LocationParser.cpp
src/LocationParser.h
src/TelemetryPoint.h
)
target_link_libraries(location_uart_replay_tests PRIVATE Qt6::Core)
target_compile_options(location_uart_replay_tests PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG>
)
add_test(NAME location_uart_replay_tests COMMAND location_uart_replay_tests)
install(TARGETS location_plotter install(TARGETS location_plotter
BUNDLE DESTINATION . BUNDLE DESTINATION .
RUNTIME DESTINATION bin RUNTIME DESTINATION bin
+20 -3
View File
@@ -66,6 +66,19 @@ corrected series; legacy input without a source keeps the previous behavior.
Synthetic `source=trajectory` points form a third independent green ground-truth Synthetic `source=trajectory` points form a third independent green ground-truth
path. The map legend identifies all three series. path. The map legend identifies all three series.
## UART log replay
Use the `UART Replay` button (or `Dosya > UART Log Replay...`) to feed a recorded
UART `.log`/`.txt` file back through the same parser used by the live serial port.
Choose the raw GPS update interval (default `200 ms`) and sensor/fused update
interval (default `50 ms`) in the replay dialog. Press `Replay Durdur` to stop.
Recorder timestamps are optional and do not control replay speed. The replayer
removes a leading timestamp when present, identifies `source=gps` as raw GPS and
`source=imu_corrected` or `[FUSION]` as fused/sensor output, then synthesizes the
two update timelines while retaining the original UART line order. Other UART
diagnostic lines are forwarded with the surrounding location update.
## Sentetik ESKF senaryolarini açma ## Sentetik ESKF senaryolarini açma
GPS deposundan 48 kombinasyonun replay loglarini üretin: GPS deposundan 48 kombinasyonun replay loglarini üretin:
@@ -223,9 +236,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,
@@ -260,5 +274,8 @@ without logging out.
- UART log and telemetry views are refreshed in batches so a high-rate serial stream does not - UART log and telemetry views are refreshed in batches so a high-rate serial stream does not
rebuild the complete interface for every received line. The locations table uses a virtual rebuild the complete interface for every received line. The locations table uses a virtual
model and remains usable with a high `Max` value. model and remains usable with a high `Max` value.
- Her basarili UART baglantisi, dosya adinda ve her satirda yerel zaman damgasi bulunan ayri bir
log olusturur. Dosyalar kaynak kodun ana klasorundeki `uart_logs` dizinine yazilir;
`LOCATION_PLOTTER_UART_LOG_DIR` ile farkli bir klasor secilebilir.
- If no new plotted location arrives for 10 seconds while listening, current location - If no new plotted location arrives for 10 seconds while listening, current location
points and analysis values are cleared. points and analysis values are cleared.
+25 -8
View File
@@ -88,6 +88,9 @@ std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
ParsedTelemetry telemetry; ParsedTelemetry telemetry;
telemetry.rawLine = trimmed; telemetry.rawLine = trimmed;
if (trimmed.startsWith(QStringLiteral("[FUSION]"), Qt::CaseInsensitive)) {
telemetry.source = LocationSource::ImuCorrected;
}
static const QRegularExpression sourceRegex( static const QRegularExpression sourceRegex(
QStringLiteral(R"((?:^|\s)source\s*=\s*(?<source>gps|imu[_-]?corrected|trajectory|ground[_-]?truth)(?=$|\s))"), QStringLiteral(R"((?:^|\s)source\s*=\s*(?<source>gps|imu[_-]?corrected|trajectory|ground[_-]?truth)(?=$|\s))"),
QRegularExpression::CaseInsensitiveOption); QRegularExpression::CaseInsensitiveOption);
@@ -234,10 +237,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 +253,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;
} }
+283 -4
View File
@@ -5,28 +5,35 @@
#include "MapWidget.h" #include "MapWidget.h"
#include "RinexNavigation.h" #include "RinexNavigation.h"
#include "SerialReader.h" #include "SerialReader.h"
#include "UartReplay.h"
#include <QAbstractItemView> #include <QAbstractItemView>
#include <QAbstractTableModel> #include <QAbstractTableModel>
#include <QCheckBox> #include <QCheckBox>
#include <QCloseEvent>
#include <QComboBox> #include <QComboBox>
#include <QDateTime> #include <QDateTime>
#include <QDir> #include <QDir>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDoubleSpinBox> #include <QDoubleSpinBox>
#include <QFormLayout> #include <QFormLayout>
#include <QFile> #include <QFile>
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo>
#include <QGridLayout> #include <QGridLayout>
#include <QGroupBox> #include <QGroupBox>
#include <QHeaderView> #include <QHeaderView>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMessageBox>
#include <QPlainTextEdit> #include <QPlainTextEdit>
#include <QScrollArea> #include <QScrollArea>
#include <QPushButton> #include <QPushButton>
#include <QRegularExpression> #include <QRegularExpression>
#include <QSpinBox> #include <QSpinBox>
#include <QSplitter> #include <QSplitter>
#include <QSettings>
#include <QStatusBar> #include <QStatusBar>
#include <QStyle> #include <QStyle>
#include <QTabWidget> #include <QTabWidget>
@@ -375,6 +382,7 @@ private:
MainWindow::MainWindow(QWidget *parent) MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent) : QMainWindow(parent)
, m_serial(new SerialReader(this)) , m_serial(new SerialReader(this))
, m_replay(new UartReplay(this))
, m_rinex(new RinexNavigation(this)) , m_rinex(new RinexNavigation(this))
{ {
buildUi(); buildUi();
@@ -390,6 +398,40 @@ MainWindow::MainWindow(QWidget *parent)
connect(m_serial, &SerialReader::lineReceived, this, &MainWindow::handleSerialLine); connect(m_serial, &SerialReader::lineReceived, this, &MainWindow::handleSerialLine);
connect(m_serial, &SerialReader::statusChanged, this, &MainWindow::handleSerialStatus); connect(m_serial, &SerialReader::statusChanged, this, &MainWindow::handleSerialStatus);
connect(m_serial, &SerialReader::errorOccurred, this, &MainWindow::handleSerialError); connect(m_serial, &SerialReader::errorOccurred, this, &MainWindow::handleSerialError);
connect(m_replay, &UartReplay::lineReady, this, &MainWindow::handleSerialLine);
connect(m_replay, &UartReplay::replayStarted, this,
[this](int lineCount, int locationCount) {
const QString message = QStringLiteral(
"UART replay basladi: %1 konum, %2 satir")
.arg(locationCount).arg(lineCount);
m_statusLabel->setText(message);
m_log->appendPlainText(QStringLiteral("[STATUS] ") + message);
updateConnectionControls();
});
connect(m_replay, &UartReplay::progressChanged, this,
[this](int processed, int total) {
if (processed == total || processed % 100 == 0) {
m_statusLabel->setText(QStringLiteral("UART replay: %1 / %2 satir")
.arg(processed).arg(total));
}
});
connect(m_replay, &UartReplay::replayFinished, this, [this] {
commitPendingTelemetry();
flushUiUpdates();
m_map->fitToPoints();
const QString message = QStringLiteral("UART replay tamamlandi: %1 konum")
.arg(m_points.size());
m_statusLabel->setText(message);
m_log->appendPlainText(QStringLiteral("[STATUS] ") + message);
statusBar()->showMessage(message, 8000);
updateConnectionControls();
});
connect(m_replay, &UartReplay::errorOccurred, this, [this](const QString &message) {
m_statusLabel->setText(message);
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
statusBar()->showMessage(message, 8000);
updateConnectionControls();
});
connect(m_locationTimeoutTimer, &QTimer::timeout, this, &MainWindow::handleLocationTimeout); connect(m_locationTimeoutTimer, &QTimer::timeout, this, &MainWindow::handleLocationTimeout);
connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates); connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates);
connect(m_rinex, &RinexNavigation::satelliteMotionUpdated, connect(m_rinex, &RinexNavigation::satelliteMotionUpdated,
@@ -417,10 +459,21 @@ MainWindow::MainWindow(QWidget *parent)
setWindowTitle(QStringLiteral("Canli Konum Haritasi")); setWindowTitle(QStringLiteral("Canli Konum Haritasi"));
} }
void MainWindow::closeEvent(QCloseEvent *event)
{
m_replay->stop();
stopUartLogging();
QMainWindow::closeEvent(event);
}
void MainWindow::toggleSerial() void MainWindow::toggleSerial()
{ {
if (m_replay->isRunning()) {
stopUartReplay();
}
if (m_serial->isOpen()) { if (m_serial->isOpen()) {
m_serial->closePort(); m_serial->closePort();
stopUartLogging();
if (m_locationTimeoutTimer != nullptr) { if (m_locationTimeoutTimer != nullptr) {
m_locationTimeoutTimer->stop(); m_locationTimeoutTimer->stop();
} }
@@ -428,11 +481,130 @@ void MainWindow::toggleSerial()
return; return;
} }
if (m_serial->openPort(m_portCombo->currentText().trimmed(), selectedBaudRate())) { const QString port = m_portCombo->currentText().trimmed();
const int baudRate = selectedBaudRate();
if (m_serial->openPort(port, baudRate)) {
startUartLogging(port, baudRate);
updateConnectionControls(); updateConnectionControls();
} }
} }
bool MainWindow::startUartReplay(const QString &filePath, int gpsIntervalMs,
int sensorIntervalMs)
{
if (m_serial->isOpen()) {
m_serial->closePort();
stopUartLogging();
}
m_replay->stop();
resetTelemetryState();
if (!m_replay->start(filePath, gpsIntervalMs, sensorIntervalMs)) {
updateConnectionControls();
return false;
}
m_maxPointsSpin->setValue(std::clamp(m_replay->locationCount(), 1, 10000));
statusBar()->showMessage(
QStringLiteral("UART replay: GPS %1 ms, sensor/fused %2 ms")
.arg(gpsIntervalMs).arg(sensorIntervalMs), 8000);
updateConnectionControls();
return true;
}
void MainWindow::stopUartReplay()
{
if (!m_replay->isRunning()) {
return;
}
m_replay->stop();
commitPendingTelemetry();
flushUiUpdates();
const QString message = QStringLiteral("UART replay durduruldu: %1 konum")
.arg(m_points.size());
m_statusLabel->setText(message);
m_log->appendPlainText(QStringLiteral("[STATUS] ") + message);
statusBar()->showMessage(message, 5000);
updateConnectionControls();
}
bool MainWindow::startUartLogging(const QString &port, int baudRate)
{
stopUartLogging();
QString logDirectory = QString::fromLocal8Bit(qgetenv("LOCATION_PLOTTER_UART_LOG_DIR"));
if (logDirectory.isEmpty()) {
logDirectory = QDir(QStringLiteral(LOCATION_PLOTTER_SOURCE_DIR))
.filePath(QStringLiteral("uart_logs"));
} else if (QDir::isRelativePath(logDirectory)) {
logDirectory = QDir::current().absoluteFilePath(logDirectory);
}
if (!QDir().mkpath(logDirectory)) {
const QString message = QStringLiteral("UART log klasoru olusturulamadi: %1")
.arg(logDirectory);
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
statusBar()->showMessage(message, 8000);
return false;
}
const QString fileName = QStringLiteral("uart_%1.log").arg(
QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss_zzz")));
m_uartLogPath = QDir(logDirectory).filePath(fileName);
m_uartLogFile = new QFile(m_uartLogPath, this);
if (!m_uartLogFile->open(QIODevice::WriteOnly | QIODevice::Text)) {
const QString message = QStringLiteral("UART log dosyasi acilamadi: %1")
.arg(m_uartLogFile->errorString());
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
statusBar()->showMessage(message, 8000);
delete m_uartLogFile;
m_uartLogFile = nullptr;
m_uartLogPath.clear();
return false;
}
const QString header = QStringLiteral("[%1] [SESSION START] port=%2 baud=%3\n")
.arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")),
port)
.arg(baudRate);
m_uartLogFile->write(header.toUtf8());
m_uartLogFile->flush();
statusBar()->showMessage(QStringLiteral("UART log kaydi: %1").arg(m_uartLogPath), 8000);
return true;
}
void MainWindow::stopUartLogging()
{
if (m_uartLogFile == nullptr) {
return;
}
if (m_uartLogFile->isOpen()) {
const QString footer = QStringLiteral("[%1] [SESSION END]\n").arg(
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")));
m_uartLogFile->write(footer.toUtf8());
m_uartLogFile->flush();
m_uartLogFile->close();
}
delete m_uartLogFile;
m_uartLogFile = nullptr;
m_uartLogPath.clear();
}
void MainWindow::appendUartLogLine(const QString &line)
{
if (m_uartLogFile == nullptr || !m_uartLogFile->isOpen()) {
return;
}
QString cleanLine = line;
cleanLine.replace(QLatin1Char('\r'), QStringLiteral("\\r"));
cleanLine.replace(QLatin1Char('\n'), QStringLiteral("\\n"));
const QString entry = QStringLiteral("[%1] %2\n")
.arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")),
cleanLine);
m_uartLogFile->write(entry.toUtf8());
m_uartLogFile->flush();
}
void MainWindow::clearPoints() void MainWindow::clearPoints()
{ {
if (m_locationTimeoutTimer != nullptr) { if (m_locationTimeoutTimer != nullptr) {
@@ -443,6 +615,8 @@ void MainWindow::clearPoints()
bool MainWindow::loadTelemetryLog(const QString &filePath) bool MainWindow::loadTelemetryLog(const QString &filePath)
{ {
m_replay->stop();
updateConnectionControls();
QFile file(filePath); QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
const QString message = QStringLiteral("Log acilamadi: %1").arg(file.errorString()); const QString message = QStringLiteral("Log acilamadi: %1").arg(file.errorString());
@@ -485,6 +659,95 @@ void MainWindow::openTelemetryLog()
} }
} }
void MainWindow::openUartReplay()
{
if (m_replay->isRunning()) {
stopUartReplay();
return;
}
QSettings settings;
QDialog dialog(this);
dialog.setWindowTitle(QStringLiteral("UART Log Replay"));
dialog.setMinimumWidth(620);
auto *layout = new QVBoxLayout(&dialog);
auto *form = new QFormLayout();
auto *fileRow = new QWidget(&dialog);
auto *fileLayout = new QHBoxLayout(fileRow);
fileLayout->setContentsMargins(0, 0, 0, 0);
auto *pathEdit = new QLineEdit(fileRow);
pathEdit->setObjectName(QStringLiteral("uartReplayPathInput"));
pathEdit->setText(settings.value(QStringLiteral("replay/file")).toString());
pathEdit->setPlaceholderText(QStringLiteral("UART .log veya .txt dosyasi"));
auto *browseButton = new QPushButton(QStringLiteral("Sec..."), fileRow);
fileLayout->addWidget(pathEdit, 1);
fileLayout->addWidget(browseButton);
form->addRow(QStringLiteral("Kayitli UART verisi"), fileRow);
auto *gpsInterval = new QSpinBox(&dialog);
gpsInterval->setObjectName(QStringLiteral("uartReplayGpsIntervalInput"));
gpsInterval->setRange(1, 60000);
gpsInterval->setSuffix(QStringLiteral(" ms"));
gpsInterval->setValue(settings.value(QStringLiteral("replay/gpsIntervalMs"), 200).toInt());
form->addRow(QStringLiteral("Raw GPS guncelleme"), gpsInterval);
auto *sensorInterval = new QSpinBox(&dialog);
sensorInterval->setObjectName(QStringLiteral("uartReplaySensorIntervalInput"));
sensorInterval->setRange(1, 60000);
sensorInterval->setSuffix(QStringLiteral(" ms"));
sensorInterval->setValue(settings.value(
QStringLiteral("replay/sensorIntervalMs"), 50).toInt());
form->addRow(QStringLiteral("Sensor / fused guncelleme"), sensorInterval);
layout->addLayout(form);
auto *hint = new QLabel(QStringLiteral(
"Log zaman damgasi gerektirmez. source=gps kayitlari GPS hizinda; "
"source=imu_corrected ve [FUSION] kayitlari sensor hizinda yeniden uretilir."),
&dialog);
hint->setWordWrap(true);
layout->addWidget(hint);
auto *buttons = new QDialogButtonBox(
QDialogButtonBox::Cancel | QDialogButtonBox::Ok, &dialog);
buttons->button(QDialogButtonBox::Ok)->setText(QStringLiteral("Replay Baslat"));
layout->addWidget(buttons);
connect(browseButton, &QPushButton::clicked, &dialog, [this, pathEdit] {
QString initialPath = pathEdit->text().trimmed();
if (initialPath.isEmpty()) {
initialPath = QDir(QStringLiteral(LOCATION_PLOTTER_SOURCE_DIR))
.filePath(QStringLiteral("uart_logs"));
}
const QString filePath = QFileDialog::getOpenFileName(
this, QStringLiteral("UART replay logunu sec"), initialPath,
QStringLiteral("UART logs (*.log *.txt);;All files (*)"));
if (!filePath.isEmpty()) {
pathEdit->setText(filePath);
}
});
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
connect(buttons, &QDialogButtonBox::accepted, &dialog, [&dialog, pathEdit] {
const QFileInfo selectedFile(pathEdit->text().trimmed());
if (!selectedFile.exists() || !selectedFile.isFile()) {
QMessageBox::warning(&dialog, QStringLiteral("UART Replay"),
QStringLiteral("Gecerli bir UART log dosyasi secin."));
return;
}
dialog.accept();
});
if (dialog.exec() != QDialog::Accepted) {
return;
}
const QString filePath = QFileInfo(pathEdit->text().trimmed()).absoluteFilePath();
settings.setValue(QStringLiteral("replay/file"), filePath);
settings.setValue(QStringLiteral("replay/gpsIntervalMs"), gpsInterval->value());
settings.setValue(QStringLiteral("replay/sensorIntervalMs"), sensorInterval->value());
startUartReplay(filePath, gpsInterval->value(), sensorInterval->value());
}
void MainWindow::refreshSerialPorts() void MainWindow::refreshSerialPorts()
{ {
const QString previousPort = m_portCombo->currentText().trimmed(); const QString previousPort = m_portCombo->currentText().trimmed();
@@ -515,12 +778,13 @@ void MainWindow::refreshSerialPorts()
void MainWindow::handleSerialLine(const QString &line) void MainWindow::handleSerialLine(const QString &line)
{ {
appendUartLogLine(line);
static const QRegularExpression solveStartRegex( static const QRegularExpression solveStartRegex(
QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"), QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"),
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();
@@ -591,6 +855,7 @@ void MainWindow::handleSerialStatus(const QString &status)
void MainWindow::handleSerialError(const QString &message) void MainWindow::handleSerialError(const QString &message)
{ {
stopUartLogging();
flushPendingLogLines(); flushPendingLogLines();
m_statusLabel->setText(message); m_statusLabel->setText(message);
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message); m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
@@ -854,6 +1119,11 @@ void MainWindow::buildUi()
m_openLogButton = new QPushButton(QStringLiteral("Senaryo Logu Ac"), this); m_openLogButton = new QPushButton(QStringLiteral("Senaryo Logu Ac"), this);
m_openLogButton->setObjectName(QStringLiteral("openLogButton")); m_openLogButton->setObjectName(QStringLiteral("openLogButton"));
m_openLogButton->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton)); m_openLogButton->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton));
m_replayButton = new QPushButton(QStringLiteral("UART Replay"), this);
m_replayButton->setObjectName(QStringLiteral("uartReplayButton"));
m_replayButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
m_replayButton->setToolTip(QStringLiteral(
"Kayitli UART verisini GPS ve sensor guncelleme hizlariyla yeniden oynat"));
m_autoFollowCheck = new QCheckBox(QStringLiteral("Kamerayi takip et"), this); m_autoFollowCheck = new QCheckBox(QStringLiteral("Kamerayi takip et"), this);
m_autoFollowCheck->setChecked(true); m_autoFollowCheck->setChecked(true);
@@ -874,6 +1144,7 @@ void MainWindow::buildUi()
toolbarLayout->addWidget(m_maxPointsSpin); toolbarLayout->addWidget(m_maxPointsSpin);
toolbarLayout->addWidget(m_connectButton); toolbarLayout->addWidget(m_connectButton);
toolbarLayout->addWidget(m_openLogButton); toolbarLayout->addWidget(m_openLogButton);
toolbarLayout->addWidget(m_replayButton);
toolbarLayout->addWidget(m_clearButton); toolbarLayout->addWidget(m_clearButton);
toolbarLayout->addWidget(m_autoFollowCheck); toolbarLayout->addWidget(m_autoFollowCheck);
toolbarLayout->addWidget(zoomOutButton); toolbarLayout->addWidget(zoomOutButton);
@@ -1109,6 +1380,7 @@ void MainWindow::buildUi()
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial); connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial);
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints); connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog); connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog);
connect(m_replayButton, &QPushButton::clicked, this, &MainWindow::openUartReplay);
connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts); connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts);
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this, connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
&MainWindow::trimToMaxPoints); &MainWindow::trimToMaxPoints);
@@ -1143,10 +1415,17 @@ void MainWindow::buildUi()
void MainWindow::updateConnectionControls() void MainWindow::updateConnectionControls()
{ {
const bool open = m_serial->isOpen(); const bool open = m_serial->isOpen();
const bool replaying = m_replay->isRunning();
m_connectButton->setText(open ? QStringLiteral("Durdur") : QStringLiteral("Dinle")); m_connectButton->setText(open ? QStringLiteral("Durdur") : QStringLiteral("Dinle"));
m_connectButton->setIcon(style()->standardIcon(open ? QStyle::SP_MediaStop : QStyle::SP_MediaPlay)); m_connectButton->setIcon(style()->standardIcon(open ? QStyle::SP_MediaStop : QStyle::SP_MediaPlay));
m_portCombo->setEnabled(!open); m_connectButton->setEnabled(!replaying);
m_baudCombo->setEnabled(!open); m_portCombo->setEnabled(!open && !replaying);
m_baudCombo->setEnabled(!open && !replaying);
m_openLogButton->setEnabled(!replaying);
m_replayButton->setText(replaying ? QStringLiteral("Replay Durdur")
: QStringLiteral("UART Replay"));
m_replayButton->setIcon(style()->standardIcon(
replaying ? QStyle::SP_MediaStop : QStyle::SP_MediaPlay));
} }
void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry) void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
+17
View File
@@ -18,12 +18,15 @@ class QPushButton;
class QSpinBox; class QSpinBox;
class QTableView; class QTableView;
class QTimer; class QTimer;
class QCloseEvent;
class QFile;
class MapWidget; class MapWidget;
class GlobeWidget; class GlobeWidget;
class RinexNavigation; class RinexNavigation;
class SerialReader; class SerialReader;
class TelemetryTableModel; class TelemetryTableModel;
class UartReplay;
class MainWindow : public QMainWindow { class MainWindow : public QMainWindow {
Q_OBJECT Q_OBJECT
@@ -31,6 +34,11 @@ class MainWindow : public QMainWindow {
public: public:
explicit MainWindow(QWidget *parent = nullptr); explicit MainWindow(QWidget *parent = nullptr);
bool loadTelemetryLog(const QString &filePath); bool loadTelemetryLog(const QString &filePath);
bool startUartReplay(const QString &filePath, int gpsIntervalMs = 200,
int sensorIntervalMs = 50);
protected:
void closeEvent(QCloseEvent *event) override;
private slots: private slots:
void toggleSerial(); void toggleSerial();
@@ -43,7 +51,11 @@ private slots:
void updateReferenceLocation(); void updateReferenceLocation();
void flushUiUpdates(); void flushUiUpdates();
void openTelemetryLog(); void openTelemetryLog();
void openUartReplay();
void stopUartReplay();
void refreshSerialPorts(); void refreshSerialPorts();
bool startUartLogging(const QString &port, int baudRate);
void stopUartLogging();
private: private:
void buildUi(); void buildUi();
@@ -60,9 +72,11 @@ private:
void scheduleTelemetryRefresh(); void scheduleTelemetryRefresh();
void refreshTelemetryViews(); void refreshTelemetryViews();
void flushPendingLogLines(); void flushPendingLogLines();
void appendUartLogLine(const QString &line);
int selectedBaudRate() const; int selectedBaudRate() const;
SerialReader *m_serial = nullptr; SerialReader *m_serial = nullptr;
UartReplay *m_replay = nullptr;
MapWidget *m_map = nullptr; MapWidget *m_map = nullptr;
GlobeWidget *m_globe = nullptr; GlobeWidget *m_globe = nullptr;
RinexNavigation *m_rinex = nullptr; RinexNavigation *m_rinex = nullptr;
@@ -74,6 +88,7 @@ private:
QPushButton *m_connectButton = nullptr; QPushButton *m_connectButton = nullptr;
QPushButton *m_clearButton = nullptr; QPushButton *m_clearButton = nullptr;
QPushButton *m_openLogButton = nullptr; QPushButton *m_openLogButton = nullptr;
QPushButton *m_replayButton = nullptr;
QPushButton *m_refreshRinexButton = nullptr; QPushButton *m_refreshRinexButton = nullptr;
QCheckBox *m_autoFollowCheck = nullptr; QCheckBox *m_autoFollowCheck = nullptr;
QCheckBox *m_showTelemetryCheck = nullptr; QCheckBox *m_showTelemetryCheck = nullptr;
@@ -98,6 +113,8 @@ private:
QPlainTextEdit *m_log = nullptr; QPlainTextEdit *m_log = nullptr;
QTimer *m_locationTimeoutTimer = nullptr; QTimer *m_locationTimeoutTimer = nullptr;
QTimer *m_uiRefreshTimer = nullptr; QTimer *m_uiRefreshTimer = nullptr;
QFile *m_uartLogFile = nullptr;
QString m_uartLogPath;
QVector<TelemetryPoint> m_points; QVector<TelemetryPoint> m_points;
QStringList m_pendingLogLines; QStringList m_pendingLogLines;
+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;
} }
+207
View File
@@ -0,0 +1,207 @@
#include "UartReplay.h"
#include "LocationParser.h"
#include <QFile>
#include <QRegularExpression>
#include <QTimer>
#include <algorithm>
#include <limits>
UartReplay::UartReplay(QObject *parent)
: QObject(parent)
, m_timer(new QTimer(this))
{
m_timer->setSingleShot(true);
connect(m_timer, &QTimer::timeout, this, &UartReplay::emitNext);
}
bool UartReplay::isRunning() const
{
return m_running;
}
int UartReplay::locationCount() const
{
return m_locationCount;
}
bool UartReplay::start(const QString &filePath, int gpsIntervalMs, int sensorIntervalMs)
{
stop();
if (gpsIntervalMs < 1 || sensorIntervalMs < 1) {
emit errorOccurred(QStringLiteral("Replay araliklari en az 1 ms olmalidir"));
return false;
}
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
emit errorOccurred(QStringLiteral("UART replay logu acilamadi: %1")
.arg(file.errorString()));
return false;
}
qint64 nextGpsMs = 0;
qint64 nextSensorMs = 0;
qint64 lastDueMs = 0;
bool pendingLatitude = false;
bool pendingLongitude = false;
LocationSource pendingSource = LocationSource::Generic;
const QList<QByteArray> rawLines = file.readAll().split('\n');
m_lines.reserve(rawLines.size());
for (const QByteArray &rawLine : rawLines) {
const QString line = removeRecorderTimestamp(QString::fromUtf8(rawLine));
if (line.isEmpty() || line.startsWith(QLatin1Char('#'))
|| line.startsWith(QStringLiteral("[SESSION START]"))
|| line.startsWith(QStringLiteral("[SESSION END]"))) {
continue;
}
Stream stream = classifyLine(line);
const std::optional<ParsedTelemetry> telemetry = LocationParser::parseLine(line);
if (telemetry.has_value() && telemetry->hasLocation()) {
pendingLatitude = false;
pendingLongitude = false;
pendingSource = LocationSource::Generic;
} else if (telemetry.has_value()) {
if (telemetry->latitude.has_value()) {
if (pendingLatitude) {
pendingLongitude = false;
}
pendingLatitude = true;
pendingSource = telemetry->source;
}
if (telemetry->longitude.has_value()) {
pendingLongitude = true;
if (telemetry->source != LocationSource::Generic) {
pendingSource = telemetry->source;
}
}
if (pendingLatitude && pendingLongitude) {
stream = pendingSource == LocationSource::ImuCorrected
|| pendingSource == LocationSource::Trajectory
? Stream::Sensor : Stream::Gps;
pendingLatitude = false;
pendingLongitude = false;
pendingSource = LocationSource::Generic;
}
}
qint64 dueMs = lastDueMs;
if (stream == Stream::Gps) {
dueMs = std::max(lastDueMs, nextGpsMs);
nextGpsMs = dueMs + gpsIntervalMs;
++m_locationCount;
} else if (stream == Stream::Sensor) {
dueMs = std::max(lastDueMs, nextSensorMs);
nextSensorMs = dueMs + sensorIntervalMs;
++m_locationCount;
}
ReplayLine replayLine;
replayLine.text = line;
replayLine.delayBeforeMs = static_cast<int>(std::min<qint64>(
dueMs - lastDueMs, std::numeric_limits<int>::max()));
m_lines.push_back(std::move(replayLine));
lastDueMs = dueMs;
}
if (m_locationCount == 0) {
m_lines.clear();
emit errorOccurred(QStringLiteral(
"UART replay logunda GPS veya fused konum kaydi bulunamadi"));
return false;
}
m_running = true;
emit replayStarted(m_lines.size(), m_locationCount);
m_timer->start(0);
return true;
}
void UartReplay::stop()
{
if (!m_running && m_lines.isEmpty()) {
return;
}
m_timer->stop();
m_running = false;
m_lines.clear();
m_index = 0;
m_locationCount = 0;
}
void UartReplay::emitNext()
{
if (!m_running) {
return;
}
constexpr int MaxImmediateLines = 250;
int emitted = 0;
while (m_index < m_lines.size() && emitted < MaxImmediateLines) {
const ReplayLine &line = m_lines.at(m_index);
if (line.delayBeforeMs > 0) {
if (emitted > 0) {
emit progressChanged(m_index, m_lines.size());
}
const int delay = line.delayBeforeMs;
m_lines[m_index].delayBeforeMs = 0;
m_timer->start(delay);
return;
}
emit lineReady(line.text);
++m_index;
++emitted;
}
emit progressChanged(m_index, m_lines.size());
if (m_index >= m_lines.size()) {
finish();
} else {
m_timer->start(0);
}
}
QString UartReplay::removeRecorderTimestamp(const QString &line)
{
QString result = line.trimmed();
static const QRegularExpression timestampPrefix(
QStringLiteral(R"(^\[\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[\.,]\d+)?\]\s*)"));
result.remove(timestampPrefix);
return result.trimmed();
}
UartReplay::Stream UartReplay::classifyLine(const QString &line)
{
const std::optional<ParsedTelemetry> telemetry = LocationParser::parseLine(line);
if (!telemetry.has_value() || !telemetry->hasLocation()) {
return Stream::Diagnostic;
}
switch (telemetry->source) {
case LocationSource::Gps:
return Stream::Gps;
case LocationSource::ImuCorrected:
case LocationSource::Trajectory:
return Stream::Sensor;
case LocationSource::Generic:
default:
// Older firmware used a [FUSION] record without an explicit source.
return line.startsWith(QStringLiteral("[FUSION]"), Qt::CaseInsensitive)
? Stream::Sensor : Stream::Gps;
}
}
void UartReplay::finish()
{
m_timer->stop();
m_running = false;
m_lines.clear();
m_index = 0;
m_locationCount = 0;
emit replayFinished();
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <QObject>
#include <QString>
#include <QVector>
class QTimer;
class UartReplay final : public QObject {
Q_OBJECT
public:
explicit UartReplay(QObject *parent = nullptr);
[[nodiscard]] bool isRunning() const;
[[nodiscard]] int locationCount() const;
public slots:
bool start(const QString &filePath, int gpsIntervalMs = 200,
int sensorIntervalMs = 50);
void stop();
signals:
void lineReady(const QString &line);
void replayStarted(int lineCount, int locationCount);
void progressChanged(int linesProcessed, int lineCount);
void replayFinished();
void errorOccurred(const QString &message);
private slots:
void emitNext();
private:
enum class Stream {
Diagnostic,
Gps,
Sensor
};
struct ReplayLine {
QString text;
int delayBeforeMs = 0;
};
static QString removeRecorderTimestamp(const QString &line);
static Stream classifyLine(const QString &line);
void finish();
QTimer *m_timer = nullptr;
QVector<ReplayLine> m_lines;
int m_index = 0;
int m_locationCount = 0;
bool m_running = false;
};
+32 -2
View File
@@ -6,15 +6,19 @@
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QDoubleSpinBox> #include <QDoubleSpinBox>
#include <QDir>
#include <QFile>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMetaObject> #include <QMetaObject>
#include <QSpinBox> #include <QSpinBox>
#include <QPushButton> #include <QPushButton>
#include <QRegularExpression>
#include <QTableView> #include <QTableView>
#include <QTabWidget> #include <QTabWidget>
#include <QSplitter> #include <QSplitter>
#include <QTemporaryFile> #include <QTemporaryFile>
#include <QTemporaryDir>
#include <cassert> #include <cassert>
@@ -43,6 +47,32 @@ int main(int argc, char **argv)
MainWindow window; MainWindow window;
qunsetenv("LOCATION_PLOTTER_PORT"); qunsetenv("LOCATION_PLOTTER_PORT");
{
QTemporaryDir uartLogDirectory;
assert(uartLogDirectory.isValid());
qputenv("LOCATION_PLOTTER_UART_LOG_DIR", uartLogDirectory.path().toUtf8());
bool loggingStarted = false;
assert(QMetaObject::invokeMethod(
&window, "startUartLogging", Qt::DirectConnection,
Q_RETURN_ARG(bool, loggingStarted),
Q_ARG(QString, QStringLiteral("/dev/test-uart")), Q_ARG(int, 460800)));
assert(loggingStarted);
sendLine(window, QStringLiteral("raw uart test line"));
assert(QMetaObject::invokeMethod(&window, "stopUartLogging", Qt::DirectConnection));
qunsetenv("LOCATION_PLOTTER_UART_LOG_DIR");
const QStringList logFiles = QDir(uartLogDirectory.path()).entryList(
{QStringLiteral("uart_*.log")}, QDir::Files);
assert(logFiles.size() == 1);
QFile logFile(QDir(uartLogDirectory.path()).filePath(logFiles.constFirst()));
assert(logFile.open(QIODevice::ReadOnly | QIODevice::Text));
const QString contents = QString::fromUtf8(logFile.readAll());
assert(contents.contains(QStringLiteral("[SESSION START] port=/dev/test-uart baud=460800")));
assert(contents.contains(QRegularExpression(
QStringLiteral(R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\] raw uart test line)"))));
assert(contents.contains(QStringLiteral("[SESSION END]")));
}
auto *table = window.findChild<QTableView *>(); auto *table = window.findChild<QTableView *>();
auto *referenceLatInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLatInput")); auto *referenceLatInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLatInput"));
auto *referenceLonInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLonInput")); auto *referenceLonInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLonInput"));
@@ -89,13 +119,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,
+38
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 "
@@ -231,6 +250,14 @@ int main(int argc, char **argv)
assert(parsed->satellites == 5); assert(parsed->satellites == 5);
} }
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[FUSION] state=moving lat=40.7357 lon=31.5986 height=811"));
assert(parsed.has_value());
assert(parsed->hasLocation());
assert(parsed->source == LocationSource::ImuCorrected);
}
{ {
const auto parsed = LocationParser::parseLine(QStringLiteral( const auto parsed = LocationParser::parseLine(QStringLiteral(
"[LOCATION] source=trajectory lat=40.7358 lon=31.5987 height=810 tracked=4 used=4")); "[LOCATION] source=trajectory lat=40.7358 lon=31.5987 height=810 tracked=4 used=4"));
@@ -257,6 +284,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");
+70
View File
@@ -0,0 +1,70 @@
#include "../src/UartReplay.h"
#include <QCoreApplication>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QTemporaryFile>
#include <QTimer>
#include <cassert>
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QTemporaryFile log;
assert(log.open());
log.write(
"[2026-08-12 10:00:00.000] [SESSION START] port=/dev/test baud=460800\n"
"[2026-08-12 10:00:00.001] [LOCATION] source=gps lat=40.0 lon=29.0\n"
"diagnostic line without timestamp\n"
"[LOCATION] source=imu_corrected lat=40.1 lon=29.1\n"
"[FUSION] lat=40.2 lon=29.2 height=10\n"
"[LOCATION] source=gps lat=40.3 lon=29.3\n"
"[2026-08-12 10:00:00.999] [SESSION END]\n");
log.flush();
UartReplay replay;
QStringList received;
QVector<qint64> receivedAt;
QElapsedTimer elapsed;
QEventLoop loop;
QObject::connect(&replay, &UartReplay::lineReady, &loop, [&](const QString &line) {
received.push_back(line);
receivedAt.push_back(elapsed.elapsed());
});
QObject::connect(&replay, &UartReplay::replayFinished, &loop, &QEventLoop::quit);
QTimer::singleShot(2000, &loop, &QEventLoop::quit);
elapsed.start();
assert(replay.start(log.fileName(), 20, 10));
assert(replay.locationCount() == 4);
loop.exec();
assert(!replay.isRunning());
assert(received.size() == 5);
assert(received.first().startsWith(QStringLiteral("[LOCATION] source=gps")));
assert(received.at(1) == QStringLiteral("diagnostic line without timestamp"));
assert(received.at(3).startsWith(QStringLiteral("[FUSION]")));
assert(receivedAt.last() >= 15);
QTemporaryFile splitLog;
assert(splitLog.open());
splitLog.write("latitude: 40.25\nlongitude: 29.50\n");
splitLog.flush();
received.clear();
receivedAt.clear();
elapsed.restart();
assert(replay.start(splitLog.fileName(), 1, 1));
assert(replay.locationCount() == 1);
loop.exec();
assert(received.size() == 2);
QTemporaryFile invalidLog;
assert(invalidLog.open());
invalidLog.write("only diagnostics\n");
invalidLog.flush();
assert(!replay.start(invalidLog.fileName(), 200, 50));
return 0;
}