Add source-aware UART log replay
This commit is contained in:
@@ -8,3 +8,4 @@ build-*/
|
||||
*.autosave
|
||||
*.swp
|
||||
*~
|
||||
uart_logs/
|
||||
|
||||
@@ -22,6 +22,8 @@ add_executable(location_plotter
|
||||
src/MapWidget.h
|
||||
src/SerialReader.cpp
|
||||
src/SerialReader.h
|
||||
src/UartReplay.cpp
|
||||
src/UartReplay.h
|
||||
src/RinexNavigation.cpp
|
||||
src/RinexNavigation.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_compile_definitions(location_plotter PRIVATE
|
||||
LOCATION_PLOTTER_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(location_plotter PROPERTIES
|
||||
@@ -79,12 +84,17 @@ add_executable(location_main_window_tests
|
||||
src/GlobeWidget.h
|
||||
src/SerialReader.cpp
|
||||
src/SerialReader.h
|
||||
src/UartReplay.cpp
|
||||
src/UartReplay.h
|
||||
src/RinexNavigation.cpp
|
||||
src/RinexNavigation.h
|
||||
src/SatelliteData.h
|
||||
src/TelemetryPoint.h
|
||||
)
|
||||
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
|
||||
$<$<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
|
||||
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
|
||||
BUNDLE DESTINATION .
|
||||
RUNTIME DESTINATION bin
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
GPS deposundan 48 kombinasyonun replay loglarini üretin:
|
||||
@@ -261,5 +274,8 @@ without logging out.
|
||||
- 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
|
||||
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
|
||||
points and analysis values are cleared.
|
||||
|
||||
@@ -88,6 +88,9 @@ std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
|
||||
|
||||
ParsedTelemetry telemetry;
|
||||
telemetry.rawLine = trimmed;
|
||||
if (trimmed.startsWith(QStringLiteral("[FUSION]"), Qt::CaseInsensitive)) {
|
||||
telemetry.source = LocationSource::ImuCorrected;
|
||||
}
|
||||
static const QRegularExpression sourceRegex(
|
||||
QStringLiteral(R"((?:^|\s)source\s*=\s*(?<source>gps|imu[_-]?corrected|trajectory|ground[_-]?truth)(?=$|\s))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
+282
-3
@@ -5,28 +5,35 @@
|
||||
#include "MapWidget.h"
|
||||
#include "RinexNavigation.h"
|
||||
#include "SerialReader.h"
|
||||
#include "UartReplay.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QAbstractTableModel>
|
||||
#include <QCheckBox>
|
||||
#include <QCloseEvent>
|
||||
#include <QComboBox>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QScrollArea>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSpinBox>
|
||||
#include <QSplitter>
|
||||
#include <QSettings>
|
||||
#include <QStatusBar>
|
||||
#include <QStyle>
|
||||
#include <QTabWidget>
|
||||
@@ -375,6 +382,7 @@ private:
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, m_serial(new SerialReader(this))
|
||||
, m_replay(new UartReplay(this))
|
||||
, m_rinex(new RinexNavigation(this))
|
||||
{
|
||||
buildUi();
|
||||
@@ -390,6 +398,40 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
connect(m_serial, &SerialReader::lineReceived, this, &MainWindow::handleSerialLine);
|
||||
connect(m_serial, &SerialReader::statusChanged, this, &MainWindow::handleSerialStatus);
|
||||
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_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates);
|
||||
connect(m_rinex, &RinexNavigation::satelliteMotionUpdated,
|
||||
@@ -417,10 +459,21 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
setWindowTitle(QStringLiteral("Canli Konum Haritasi"));
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
m_replay->stop();
|
||||
stopUartLogging();
|
||||
QMainWindow::closeEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::toggleSerial()
|
||||
{
|
||||
if (m_replay->isRunning()) {
|
||||
stopUartReplay();
|
||||
}
|
||||
if (m_serial->isOpen()) {
|
||||
m_serial->closePort();
|
||||
stopUartLogging();
|
||||
if (m_locationTimeoutTimer != nullptr) {
|
||||
m_locationTimeoutTimer->stop();
|
||||
}
|
||||
@@ -428,11 +481,130 @@ void MainWindow::toggleSerial()
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (m_locationTimeoutTimer != nullptr) {
|
||||
@@ -443,6 +615,8 @@ void MainWindow::clearPoints()
|
||||
|
||||
bool MainWindow::loadTelemetryLog(const QString &filePath)
|
||||
{
|
||||
m_replay->stop();
|
||||
updateConnectionControls();
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
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()
|
||||
{
|
||||
const QString previousPort = m_portCombo->currentText().trimmed();
|
||||
@@ -515,6 +778,7 @@ void MainWindow::refreshSerialPorts()
|
||||
|
||||
void MainWindow::handleSerialLine(const QString &line)
|
||||
{
|
||||
appendUartLogLine(line);
|
||||
static const QRegularExpression solveStartRegex(
|
||||
QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
@@ -591,6 +855,7 @@ void MainWindow::handleSerialStatus(const QString &status)
|
||||
|
||||
void MainWindow::handleSerialError(const QString &message)
|
||||
{
|
||||
stopUartLogging();
|
||||
flushPendingLogLines();
|
||||
m_statusLabel->setText(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->setObjectName(QStringLiteral("openLogButton"));
|
||||
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->setChecked(true);
|
||||
@@ -874,6 +1144,7 @@ void MainWindow::buildUi()
|
||||
toolbarLayout->addWidget(m_maxPointsSpin);
|
||||
toolbarLayout->addWidget(m_connectButton);
|
||||
toolbarLayout->addWidget(m_openLogButton);
|
||||
toolbarLayout->addWidget(m_replayButton);
|
||||
toolbarLayout->addWidget(m_clearButton);
|
||||
toolbarLayout->addWidget(m_autoFollowCheck);
|
||||
toolbarLayout->addWidget(zoomOutButton);
|
||||
@@ -1109,6 +1380,7 @@ void MainWindow::buildUi()
|
||||
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial);
|
||||
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
|
||||
connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog);
|
||||
connect(m_replayButton, &QPushButton::clicked, this, &MainWindow::openUartReplay);
|
||||
connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts);
|
||||
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&MainWindow::trimToMaxPoints);
|
||||
@@ -1143,10 +1415,17 @@ void MainWindow::buildUi()
|
||||
void MainWindow::updateConnectionControls()
|
||||
{
|
||||
const bool open = m_serial->isOpen();
|
||||
const bool replaying = m_replay->isRunning();
|
||||
m_connectButton->setText(open ? QStringLiteral("Durdur") : QStringLiteral("Dinle"));
|
||||
m_connectButton->setIcon(style()->standardIcon(open ? QStyle::SP_MediaStop : QStyle::SP_MediaPlay));
|
||||
m_portCombo->setEnabled(!open);
|
||||
m_baudCombo->setEnabled(!open);
|
||||
m_connectButton->setEnabled(!replaying);
|
||||
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)
|
||||
|
||||
@@ -18,12 +18,15 @@ class QPushButton;
|
||||
class QSpinBox;
|
||||
class QTableView;
|
||||
class QTimer;
|
||||
class QCloseEvent;
|
||||
class QFile;
|
||||
|
||||
class MapWidget;
|
||||
class GlobeWidget;
|
||||
class RinexNavigation;
|
||||
class SerialReader;
|
||||
class TelemetryTableModel;
|
||||
class UartReplay;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
@@ -31,6 +34,11 @@ class MainWindow : public QMainWindow {
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
bool loadTelemetryLog(const QString &filePath);
|
||||
bool startUartReplay(const QString &filePath, int gpsIntervalMs = 200,
|
||||
int sensorIntervalMs = 50);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void toggleSerial();
|
||||
@@ -43,7 +51,11 @@ private slots:
|
||||
void updateReferenceLocation();
|
||||
void flushUiUpdates();
|
||||
void openTelemetryLog();
|
||||
void openUartReplay();
|
||||
void stopUartReplay();
|
||||
void refreshSerialPorts();
|
||||
bool startUartLogging(const QString &port, int baudRate);
|
||||
void stopUartLogging();
|
||||
|
||||
private:
|
||||
void buildUi();
|
||||
@@ -60,9 +72,11 @@ private:
|
||||
void scheduleTelemetryRefresh();
|
||||
void refreshTelemetryViews();
|
||||
void flushPendingLogLines();
|
||||
void appendUartLogLine(const QString &line);
|
||||
int selectedBaudRate() const;
|
||||
|
||||
SerialReader *m_serial = nullptr;
|
||||
UartReplay *m_replay = nullptr;
|
||||
MapWidget *m_map = nullptr;
|
||||
GlobeWidget *m_globe = nullptr;
|
||||
RinexNavigation *m_rinex = nullptr;
|
||||
@@ -74,6 +88,7 @@ private:
|
||||
QPushButton *m_connectButton = nullptr;
|
||||
QPushButton *m_clearButton = nullptr;
|
||||
QPushButton *m_openLogButton = nullptr;
|
||||
QPushButton *m_replayButton = nullptr;
|
||||
QPushButton *m_refreshRinexButton = nullptr;
|
||||
QCheckBox *m_autoFollowCheck = nullptr;
|
||||
QCheckBox *m_showTelemetryCheck = nullptr;
|
||||
@@ -98,6 +113,8 @@ private:
|
||||
QPlainTextEdit *m_log = nullptr;
|
||||
QTimer *m_locationTimeoutTimer = nullptr;
|
||||
QTimer *m_uiRefreshTimer = nullptr;
|
||||
QFile *m_uartLogFile = nullptr;
|
||||
QString m_uartLogPath;
|
||||
|
||||
QVector<TelemetryPoint> m_points;
|
||||
QStringList m_pendingLogLines;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -6,15 +6,19 @@
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMetaObject>
|
||||
#include <QSpinBox>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QTableView>
|
||||
#include <QTabWidget>
|
||||
#include <QSplitter>
|
||||
#include <QTemporaryFile>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
@@ -43,6 +47,32 @@ int main(int argc, char **argv)
|
||||
MainWindow window;
|
||||
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 *referenceLatInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLatInput"));
|
||||
auto *referenceLonInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLonInput"));
|
||||
|
||||
@@ -250,6 +250,14 @@ int main(int argc, char **argv)
|
||||
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(
|
||||
"[LOCATION] source=trajectory lat=40.7358 lon=31.5987 height=810 tracked=4 used=4"));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user