2 Commits

Author SHA1 Message Date
Onur Cayan c8d6314e48 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:06 +03:00
Onur Cayan f7fa2b3e4d Fix incomplete map tile updates
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-10 00:51:00 +03:00
14 changed files with 949 additions and 15 deletions
+1
View File
@@ -8,3 +8,4 @@ build-*/
*.autosave *.autosave
*.swp *.swp
*~ *~
uart_logs/
+38
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
@@ -67,6 +72,20 @@ target_compile_options(location_parser_tests PRIVATE
) )
add_test(NAME location_parser_tests COMMAND location_parser_tests) add_test(NAME location_parser_tests COMMAND location_parser_tests)
add_executable(location_map_widget_tests
tests/map_widget_tests.cpp
src/MapWidget.cpp
src/MapWidget.h
src/TelemetryPoint.h
)
target_link_libraries(location_map_widget_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network)
target_compile_options(location_map_widget_tests PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG>
)
add_test(NAME location_map_widget_tests COMMAND location_map_widget_tests)
set_tests_properties(location_map_widget_tests PROPERTIES
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-map-test-cache")
add_executable(location_main_window_tests add_executable(location_main_window_tests
tests/main_window_tests.cpp tests/main_window_tests.cpp
src/LocationParser.cpp src/LocationParser.cpp
@@ -79,12 +98,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 +123,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
+16
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:
@@ -263,5 +276,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.
+3
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);
+278 -3
View File
@@ -5,6 +5,7 @@
#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>
@@ -13,10 +14,13 @@
#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>
@@ -380,6 +384,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();
@@ -399,6 +404,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_portRefreshTimer, &QTimer::timeout, this, [this] { connect(m_portRefreshTimer, &QTimer::timeout, this, [this] {
@@ -439,14 +478,20 @@ MainWindow::MainWindow(QWidget *parent)
void MainWindow::closeEvent(QCloseEvent *event) void MainWindow::closeEvent(QCloseEvent *event)
{ {
m_replay->stop();
stopUartLogging();
saveSettings(); saveSettings();
QMainWindow::closeEvent(event); 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();
} }
@@ -454,11 +499,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) {
@@ -469,6 +633,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());
@@ -510,6 +676,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();
@@ -557,6 +812,10 @@ void MainWindow::buildMenus()
openAction->setObjectName(QStringLiteral("openLogAction")); openAction->setObjectName(QStringLiteral("openLogAction"));
openAction->setShortcut(QKeySequence::Open); openAction->setShortcut(QKeySequence::Open);
connect(openAction, &QAction::triggered, this, &MainWindow::openTelemetryLog); connect(openAction, &QAction::triggered, this, &MainWindow::openTelemetryLog);
QAction *replayAction = fileMenu->addAction(QStringLiteral("UART Log Replay..."));
replayAction->setObjectName(QStringLiteral("uartReplayAction"));
replayAction->setShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+O")));
connect(replayAction, &QAction::triggered, this, &MainWindow::openUartReplay);
fileMenu->addSeparator(); fileMenu->addSeparator();
QAction *quitAction = fileMenu->addAction(QStringLiteral("Cikis")); QAction *quitAction = fileMenu->addAction(QStringLiteral("Cikis"));
quitAction->setShortcut(QKeySequence::Quit); quitAction->setShortcut(QKeySequence::Quit);
@@ -648,6 +907,7 @@ void MainWindow::saveSettings() const
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);
@@ -732,6 +992,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);
@@ -998,6 +1259,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_rinexUpdateButton = new QPushButton(QStringLiteral("RINEX Guncelle"), this); m_rinexUpdateButton = new QPushButton(QStringLiteral("RINEX Guncelle"), this);
m_rinexUpdateButton->setObjectName(QStringLiteral("rinexUpdateButton")); m_rinexUpdateButton->setObjectName(QStringLiteral("rinexUpdateButton"));
m_rinexUpdateButton->setIcon(style()->standardIcon(QStyle::SP_BrowserReload)); m_rinexUpdateButton->setIcon(style()->standardIcon(QStyle::SP_BrowserReload));
@@ -1023,6 +1289,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_rinexUpdateButton); toolbarLayout->addWidget(m_rinexUpdateButton);
toolbarLayout->addWidget(m_clearButton); toolbarLayout->addWidget(m_clearButton);
toolbarLayout->addWidget(m_autoFollowCheck); toolbarLayout->addWidget(m_autoFollowCheck);
@@ -1259,6 +1526,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(m_rinexUpdateButton, &QPushButton::clicked, m_rinex, &RinexNavigation::refresh); connect(m_rinexUpdateButton, &QPushButton::clicked, m_rinex, &RinexNavigation::refresh);
connect(refreshPortsButton, &QToolButton::clicked, this, [this] { connect(refreshPortsButton, &QToolButton::clicked, this, [this] {
m_portsInitialized = false; m_portsInitialized = false;
@@ -1297,10 +1565,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)
+13
View File
@@ -19,12 +19,14 @@ class QSpinBox;
class QTableView; class QTableView;
class QTimer; class QTimer;
class QCloseEvent; 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
@@ -32,6 +34,8 @@ 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: protected:
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
@@ -47,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();
@@ -67,9 +75,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;
@@ -81,6 +91,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_rinexUpdateButton = nullptr; QPushButton *m_rinexUpdateButton = nullptr;
QPushButton *m_refreshRinexButton = nullptr; QPushButton *m_refreshRinexButton = nullptr;
QCheckBox *m_autoFollowCheck = nullptr; QCheckBox *m_autoFollowCheck = nullptr;
@@ -107,6 +118,8 @@ private:
QTimer *m_locationTimeoutTimer = nullptr; QTimer *m_locationTimeoutTimer = nullptr;
QTimer *m_uiRefreshTimer = nullptr; QTimer *m_uiRefreshTimer = nullptr;
QTimer *m_portRefreshTimer = nullptr; QTimer *m_portRefreshTimer = nullptr;
QFile *m_uartLogFile = nullptr;
QString m_uartLogPath;
QVector<TelemetryPoint> m_points; QVector<TelemetryPoint> m_points;
QStringList m_pendingLogLines; QStringList m_pendingLogLines;
+149 -12
View File
@@ -1,5 +1,6 @@
#include "MapWidget.h" #include "MapWidget.h"
#include <QDateTime>
#include <QDir> #include <QDir>
#include <QLabel> #include <QLabel>
#include <QLineF> #include <QLineF>
@@ -14,6 +15,7 @@
#include <QResizeEvent> #include <QResizeEvent>
#include <QStandardPaths> #include <QStandardPaths>
#include <QStringList> #include <QStringList>
#include <QTimer>
#include <QUrl> #include <QUrl>
#include <QWheelEvent> #include <QWheelEvent>
@@ -26,6 +28,8 @@ constexpr int TileSize = 256;
constexpr int MinZoom = 2; constexpr int MinZoom = 2;
constexpr int MaxZoom = 19; constexpr int MaxZoom = 19;
constexpr int MaxMemoryTileCount = 512; constexpr int MaxMemoryTileCount = 512;
constexpr int InitialTileRetryDelayMs = 250;
constexpr int MaximumTileRetryDelayMs = 8000;
constexpr double Pi = 3.14159265358979323846; constexpr double Pi = 3.14159265358979323846;
constexpr double WebMercatorRadiusMeters = 6378137.0; constexpr double WebMercatorRadiusMeters = 6378137.0;
constexpr double MaxMercatorLatitude = 85.05112878; constexpr double MaxMercatorLatitude = 85.05112878;
@@ -35,6 +39,14 @@ QString tileKey(int zoom, int tileX, int tileY)
return QStringLiteral("%1/%2/%3").arg(zoom).arg(tileX).arg(tileY); return QStringLiteral("%1/%2/%3").arg(zoom).arg(tileX).arg(tileY);
} }
QUrl tileUrl(QString urlTemplate, int zoom, int tileX, int tileY)
{
urlTemplate.replace(QStringLiteral("{z}"), QString::number(zoom));
urlTemplate.replace(QStringLiteral("{x}"), QString::number(tileX));
urlTemplate.replace(QStringLiteral("{y}"), QString::number(tileY));
return QUrl(urlTemplate);
}
double clampLatitude(double latitude) double clampLatitude(double latitude)
{ {
return std::clamp(latitude, -MaxMercatorLatitude, MaxMercatorLatitude); return std::clamp(latitude, -MaxMercatorLatitude, MaxMercatorLatitude);
@@ -58,6 +70,9 @@ double metersPerPixel(double latitude, int zoom)
MapWidget::MapWidget(QWidget *parent) MapWidget::MapWidget(QWidget *parent)
: QWidget(parent) : QWidget(parent)
, m_network(new QNetworkAccessManager(this)) , m_network(new QNetworkAccessManager(this))
, m_tileUrlTemplate(qEnvironmentVariable(
"LOCATION_PLOTTER_TILE_URL_TEMPLATE",
QStringLiteral("https://tile.openstreetmap.org/{z}/{x}/{y}.png")))
{ {
setMinimumSize(480, 320); setMinimumSize(480, 320);
setMouseTracking(true); setMouseTracking(true);
@@ -460,6 +475,15 @@ void MapWidget::drawTiles(QPainter &painter)
painter.setRenderHint(QPainter::SmoothPixmapTransform, false); painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
struct MissingTile {
int x;
int y;
double distanceFromCenter;
};
QVector<MissingTile> missingTiles;
QSet<QString> visibleTileKeys;
const QPointF viewportCenter = topLeft + QPointF(width() / 2.0, height() / 2.0);
for (int tileY = firstTileY; tileY <= lastTileY; ++tileY) { for (int tileY = firstTileY; tileY <= lastTileY; ++tileY) {
if (tileY < 0 || tileY >= tileCount) { if (tileY < 0 || tileY >= tileCount) {
continue; continue;
@@ -468,6 +492,7 @@ void MapWidget::drawTiles(QPainter &painter)
for (int tileX = firstTileX; tileX <= lastTileX; ++tileX) { for (int tileX = firstTileX; tileX <= lastTileX; ++tileX) {
const int wrappedTileX = positiveModulo(tileX, tileCount); const int wrappedTileX = positiveModulo(tileX, tileCount);
const QString key = tileKey(m_zoom, wrappedTileX, tileY); const QString key = tileKey(m_zoom, wrappedTileX, tileY);
visibleTileKeys.insert(key);
const QRectF target( const QRectF target(
tileX * TileSize - topLeft.x(), tileX * TileSize - topLeft.x(),
tileY * TileSize - topLeft.y(), tileY * TileSize - topLeft.y(),
@@ -476,15 +501,57 @@ void MapWidget::drawTiles(QPainter &painter)
const auto tileIt = m_tileCache.constFind(key); const auto tileIt = m_tileCache.constFind(key);
if (tileIt != m_tileCache.constEnd()) { if (tileIt != m_tileCache.constEnd()) {
painter.drawPixmap(target.toRect(), *tileIt); painter.drawPixmap(target, *tileIt, QRectF(tileIt->rect()));
} else { } else {
painter.fillRect(target, QColor(230, 234, 230)); if (!drawCachedTileOrParent(painter, target, wrappedTileX, tileY, m_zoom)) {
painter.setPen(QColor(198, 205, 200)); painter.fillRect(target, QColor(230, 234, 230));
painter.drawRect(target); painter.setPen(QColor(198, 205, 200));
requestTile(wrappedTileX, tileY, m_zoom); painter.drawRect(target);
}
const QPointF tileCenter((tileX + 0.5) * TileSize,
(tileY + 0.5) * TileSize);
const QPointF offset = tileCenter - viewportCenter;
missingTiles.push_back({wrappedTileX, tileY,
offset.x() * offset.x() + offset.y() * offset.y()});
} }
} }
} }
// A fast pan or auto-follow jump can otherwise leave old requests ahead of
// the tiles that are visible now. Cancel those requests and enqueue the
// centre tiles first so the viewport fills in promptly.
cancelInvisibleTileRequests(visibleTileKeys);
std::sort(missingTiles.begin(), missingTiles.end(),
[](const MissingTile &left, const MissingTile &right) {
return left.distanceFromCenter < right.distanceFromCenter;
});
for (const MissingTile &tile : missingTiles) {
requestTile(tile.x, tile.y, m_zoom);
}
}
bool MapWidget::drawCachedTileOrParent(QPainter &painter, const QRectF &target,
int tileX, int tileY, int zoom) const
{
for (int parentZoom = zoom - 1; parentZoom >= MinZoom; --parentZoom) {
const int scale = 1 << (zoom - parentZoom);
const int parentX = tileX / scale;
const int parentY = tileY / scale;
const auto parentIt = m_tileCache.constFind(tileKey(parentZoom, parentX, parentY));
if (parentIt == m_tileCache.constEnd()) {
continue;
}
const QPixmap &parent = *parentIt;
const double sourceWidth = static_cast<double>(parent.width()) / scale;
const double sourceHeight = static_cast<double>(parent.height()) / scale;
const QRectF source((tileX % scale) * sourceWidth,
(tileY % scale) * sourceHeight,
sourceWidth, sourceHeight);
painter.drawPixmap(target, parent, source);
return true;
}
return false;
} }
void MapWidget::drawTelemetry(QPainter &painter) void MapWidget::drawTelemetry(QPainter &painter)
@@ -726,20 +793,27 @@ void MapWidget::requestTile(int tileX, int tileY, int zoom)
return; return;
} }
const qint64 now = QDateTime::currentMSecsSinceEpoch();
if (m_tileRetryAfterMs.value(key, 0) > now) {
return;
}
m_pendingTiles.insert(key); m_pendingTiles.insert(key);
const QUrl url(QStringLiteral("https://tile.openstreetmap.org/%1/%2/%3.png") QNetworkRequest request(tileUrl(m_tileUrlTemplate, zoom, tileX, tileY));
.arg(zoom) request.setRawHeader("User-Agent", "RocketLocationPlotter/0.3 (Qt desktop application)");
.arg(tileX) request.setTransferTimeout(15000);
.arg(tileY));
QNetworkRequest request(url);
request.setRawHeader("User-Agent", "RocketLocationPlotter/0.2 (Qt desktop application)");
QNetworkReply *reply = m_network->get(request); QNetworkReply *reply = m_network->get(request);
reply->setProperty("tileKey", key); reply->setProperty("tileKey", key);
m_tileReplies.insert(key, reply);
connect(reply, &QNetworkReply::finished, this, [this, reply]() { connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QString key = reply->property("tileKey").toString(); const QString key = reply->property("tileKey").toString();
m_pendingTiles.remove(key); m_pendingTiles.remove(key);
if (m_tileReplies.value(key) == reply) {
m_tileReplies.remove(key);
}
bool loaded = false;
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
QPixmap pixmap; QPixmap pixmap;
if (pixmap.loadFromData(reply->readAll())) { if (pixmap.loadFromData(reply->readAll())) {
@@ -747,14 +821,77 @@ void MapWidget::requestTile(int tileX, int tileY, int zoom)
m_tileCache.erase(m_tileCache.begin()); m_tileCache.erase(m_tileCache.begin());
} }
m_tileCache.insert(key, pixmap); m_tileCache.insert(key, pixmap);
update(); m_tileFailureCounts.remove(key);
m_tileRetryAfterMs.remove(key);
loaded = true;
} }
} }
const bool cancelled = reply->property("tileCancelled").toBool();
if (!loaded && !cancelled) {
const int failures = m_tileFailureCounts.value(key, 0) + 1;
m_tileFailureCounts.insert(key, failures);
const int shift = std::min(failures - 1, 5);
const int retryDelay = std::min(MaximumTileRetryDelayMs,
InitialTileRetryDelayMs * (1 << shift));
m_tileRetryAfterMs.insert(key,
QDateTime::currentMSecsSinceEpoch() + retryDelay);
QTimer::singleShot(retryDelay, this, [this, key]() {
repaintWhenTileRetryIsReady(key);
});
}
if (loaded) {
update();
}
reply->deleteLater(); reply->deleteLater();
}); });
} }
void MapWidget::cancelInvisibleTileRequests(const QSet<QString> &visibleTileKeys)
{
QVector<QNetworkReply *> obsoleteReplies;
for (auto it = m_tileReplies.cbegin(); it != m_tileReplies.cend(); ++it) {
if (!visibleTileKeys.contains(it.key())) {
obsoleteReplies.push_back(it.value());
}
}
for (QNetworkReply *reply : obsoleteReplies) {
if (reply == nullptr || reply->isFinished()) {
continue;
}
reply->setProperty("tileCancelled", true);
reply->abort();
}
for (auto it = m_tileRetryAfterMs.begin(); it != m_tileRetryAfterMs.end();) {
if (visibleTileKeys.contains(it.key())) {
++it;
continue;
}
m_tileFailureCounts.remove(it.key());
it = m_tileRetryAfterMs.erase(it);
}
}
void MapWidget::repaintWhenTileRetryIsReady(const QString &key)
{
if (!m_tileRetryAfterMs.contains(key)) {
return;
}
const qint64 remaining = m_tileRetryAfterMs.value(key, 0)
- QDateTime::currentMSecsSinceEpoch();
if (remaining > 0) {
QTimer::singleShot(static_cast<int>(remaining), this, [this, key]() {
repaintWhenTileRetryIsReady(key);
});
return;
}
update();
}
void MapWidget::setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor) void MapWidget::setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor)
{ {
newZoom = std::clamp(newZoom, MinZoom, MaxZoom); newZoom = std::clamp(newZoom, MinZoom, MaxZoom);
+9
View File
@@ -11,6 +11,7 @@
#include <optional> #include <optional>
class QNetworkAccessManager; class QNetworkAccessManager;
class QNetworkReply;
class QLabel; class QLabel;
class MapWidget : public QWidget { class MapWidget : public QWidget {
@@ -64,7 +65,11 @@ private:
void drawCepOverlay(QPainter &painter); void drawCepOverlay(QPainter &painter);
void drawCepMarker(QPainter &painter); void drawCepMarker(QPainter &painter);
void drawEmptyState(QPainter &painter); void drawEmptyState(QPainter &painter);
bool drawCachedTileOrParent(QPainter &painter, const QRectF &target,
int tileX, int tileY, int zoom) const;
void requestTile(int tileX, int tileY, int zoom); void requestTile(int tileX, int tileY, int zoom);
void cancelInvisibleTileRequests(const QSet<QString> &visibleTileKeys);
void repaintWhenTileRetryIsReady(const QString &key);
void setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor); void setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor);
void disableAutoFollowFromUserAction(); void disableAutoFollowFromUserAction();
int preferredFocusIndex() const; int preferredFocusIndex() const;
@@ -74,6 +79,10 @@ private:
QLabel *m_attributionLabel = nullptr; QLabel *m_attributionLabel = nullptr;
QHash<QString, QPixmap> m_tileCache; QHash<QString, QPixmap> m_tileCache;
QSet<QString> m_pendingTiles; QSet<QString> m_pendingTiles;
QHash<QString, QNetworkReply *> m_tileReplies;
QHash<QString, int> m_tileFailureCounts;
QHash<QString, qint64> m_tileRetryAfterMs;
QString m_tileUrlTemplate;
double m_centerLat = 39.0; double m_centerLat = 39.0;
double m_centerLon = 35.0; double m_centerLon = 35.0;
+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;
};
+30
View File
@@ -7,15 +7,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>
#include <cmath> #include <cmath>
@@ -45,6 +49,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"));
+73
View File
@@ -0,0 +1,73 @@
#include "../src/MapWidget.h"
#include <QApplication>
#include <QElapsedTimer>
#include <QHostAddress>
#include <QSet>
#include <QTcpServer>
#include <QTcpSocket>
#include <QThread>
#include <cassert>
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QTcpServer tileServer;
assert(tileServer.listen(QHostAddress::LocalHost, 0));
int requestCount = 0;
QSet<QByteArray> requestedPaths;
QObject::connect(&tileServer, &QTcpServer::newConnection, [&]() {
while (tileServer.hasPendingConnections()) {
QTcpSocket *socket = tileServer.nextPendingConnection();
const auto respond = [&, socket]() {
if (!socket->canReadLine()) {
return;
}
const QByteArray requestLine = socket->readLine().trimmed();
const QList<QByteArray> fields = requestLine.split(' ');
if (fields.size() >= 2) {
++requestCount;
requestedPaths.insert(fields[1]);
}
socket->readAll();
socket->write("HTTP/1.1 503 Service Unavailable\r\n"
"Content-Length: 0\r\n"
"Connection: close\r\n\r\n");
socket->disconnectFromHost();
};
QObject::connect(socket, &QTcpSocket::readyRead, socket, respond);
QObject::connect(socket, &QTcpSocket::disconnected,
socket, &QObject::deleteLater);
respond();
}
});
const QByteArray tileUrl = QByteArray("http://127.0.0.1:")
+ QByteArray::number(tileServer.serverPort()) + "/{z}/{x}/{y}.png";
qputenv("LOCATION_PLOTTER_TILE_URL_TEMPLATE", tileUrl);
{
MapWidget map;
map.resize(640, 480);
map.show();
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < 1200) {
app.processEvents(QEventLoop::AllEvents, 50);
QThread::msleep(5);
}
// Every URL fails. Seeing any URL more than once proves that a failed
// tile schedules a new paint/request instead of remaining a blank
// placeholder forever.
assert(!requestedPaths.isEmpty());
assert(requestCount > requestedPaths.size());
}
qunsetenv("LOCATION_PLOTTER_TILE_URL_TEMPLATE");
return 0;
}
+8
View File
@@ -250,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"));
+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;
}