Add source-aware UART log replay
This commit is contained in:
@@ -8,3 +8,4 @@ build-*/
|
|||||||
*.autosave
|
*.autosave
|
||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
|
uart_logs/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -261,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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+282
-3
@@ -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,6 +778,7 @@ 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);
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 <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"));
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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