Files
location_plotter/src/MainWindow.cpp
T
onur 049f6301e8
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
Add macOS USB UART support
2026-07-18 12:03:38 +03:00

1295 lines
47 KiB
C++

#include "MainWindow.h"
#include "LocationParser.h"
#include "GlobeWidget.h"
#include "MapWidget.h"
#include "RinexNavigation.h"
#include "SerialReader.h"
#include <QAbstractItemView>
#include <QAbstractTableModel>
#include <QCheckBox>
#include <QComboBox>
#include <QDateTime>
#include <QDir>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QFile>
#include <QFileDialog>
#include <QGridLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QScrollArea>
#include <QPushButton>
#include <QRegularExpression>
#include <QSpinBox>
#include <QSplitter>
#include <QStatusBar>
#include <QStyle>
#include <QTabWidget>
#include <QTableView>
#include <QToolButton>
#include <QTimer>
#include <QVBoxLayout>
#include <QPixmap>
#include <algorithm>
#include <cmath>
namespace {
constexpr int LocationTimeoutMs = 10000;
constexpr int UiRefreshIntervalMs = 100;
constexpr int MaxPendingLogLines = 500;
constexpr double Pi = 3.14159265358979323846;
constexpr double EarthRadiusMeters = 6371008.8;
struct LocationAnalysis {
bool valid = false;
double averageLat = 0.0;
double averageLon = 0.0;
double lastDeviationMeters = 0.0;
double meanDeviationMeters = 0.0;
double rmsScatterMeters = 0.0;
double maxDeviationMeters = 0.0;
};
double degreesToRadians(double degrees)
{
return degrees * Pi / 180.0;
}
double radiansToDegrees(double radians)
{
return radians * 180.0 / Pi;
}
double distanceMeters(double lat1, double lon1, double lat2, double lon2)
{
const double phi1 = degreesToRadians(lat1);
const double phi2 = degreesToRadians(lat2);
const double dPhi = degreesToRadians(lat2 - lat1);
const double dLambda = degreesToRadians(lon2 - lon1);
const double sinHalfPhi = std::sin(dPhi / 2.0);
const double sinHalfLambda = std::sin(dLambda / 2.0);
const double a = sinHalfPhi * sinHalfPhi
+ std::cos(phi1) * std::cos(phi2) * sinHalfLambda * sinHalfLambda;
const double c = 2.0 * std::atan2(std::sqrt(a), std::sqrt(std::max(0.0, 1.0 - a)));
return EarthRadiusMeters * c;
}
std::optional<double> parseCoordinateText(QString text, double minimum, double maximum)
{
text = text.trimmed();
text.replace(',', '.');
bool ok = false;
const double value = text.toDouble(&ok);
if (!ok || !std::isfinite(value) || value < minimum || value > maximum) {
return std::nullopt;
}
return value;
}
double calculateCep50(const QVector<TelemetryPoint> &points,
double referenceLatitude, double referenceLongitude)
{
QVector<double> radialErrors;
radialErrors.reserve(points.size());
for (const TelemetryPoint &point : points) {
radialErrors.push_back(distanceMeters(referenceLatitude, referenceLongitude,
point.latitude, point.longitude));
}
std::sort(radialErrors.begin(), radialErrors.end());
const int middle = radialErrors.size() / 2;
if (radialErrors.size() % 2 == 1) {
return radialErrors[middle];
}
return (radialErrors[middle - 1] + radialErrors[middle]) / 2.0;
}
LocationAnalysis analyzeLocations(const QVector<TelemetryPoint> &points)
{
LocationAnalysis analysis;
if (points.isEmpty()) {
return analysis;
}
double x = 0.0;
double y = 0.0;
double z = 0.0;
for (const TelemetryPoint &point : points) {
const double lat = degreesToRadians(point.latitude);
const double lon = degreesToRadians(point.longitude);
const double cosLat = std::cos(lat);
x += cosLat * std::cos(lon);
y += cosLat * std::sin(lon);
z += std::sin(lat);
}
x /= points.size();
y /= points.size();
z /= points.size();
const double horizontal = std::sqrt(x * x + y * y);
analysis.averageLat = radiansToDegrees(std::atan2(z, horizontal));
analysis.averageLon = radiansToDegrees(std::atan2(y, x));
analysis.valid = true;
double distanceSum = 0.0;
double squaredDistanceSum = 0.0;
for (const TelemetryPoint &point : points) {
const double distance = distanceMeters(point.latitude, point.longitude,
analysis.averageLat, analysis.averageLon);
distanceSum += distance;
squaredDistanceSum += distance * distance;
analysis.maxDeviationMeters = std::max(analysis.maxDeviationMeters, distance);
}
analysis.meanDeviationMeters = distanceSum / points.size();
analysis.rmsScatterMeters = std::sqrt(squaredDistanceSum / points.size());
const TelemetryPoint &last = points.last();
analysis.lastDeviationMeters = distanceMeters(last.latitude, last.longitude,
analysis.averageLat, analysis.averageLon);
return analysis;
}
QVector<TelemetryPoint> preferredAnalysisPoints(const QVector<TelemetryPoint> &points)
{
QVector<TelemetryPoint> corrected;
for (const TelemetryPoint &point : points) {
if (point.source == LocationSource::ImuCorrected) {
corrected.push_back(point);
}
}
return corrected.isEmpty() ? points : corrected;
}
QString metersText(double meters)
{
if (!std::isfinite(meters)) {
return QStringLiteral("-");
}
const int precision = meters < 10.0 ? 2 : (meters < 100.0 ? 1 : 0);
return QStringLiteral("%1 m").arg(meters, 0, 'f', precision);
}
QString optionalDoubleText(const std::optional<double> &value, int precision)
{
if (!value.has_value()) {
return QStringLiteral("-");
}
return QString::number(*value, 'f', precision);
}
QString preciseCoordinateText(double value)
{
return QString::number(value, 'f', 15);
}
QString coordinateText(double value, const QString &sourceText)
{
return sourceText.isEmpty() ? preciseCoordinateText(value) : sourceText;
}
QString optionalIntText(const std::optional<int> &value)
{
if (!value.has_value()) {
return QStringLiteral("-");
}
return QString::number(*value);
}
QString optionalDopText(const std::optional<double> &value)
{
if (!value.has_value() || !std::isfinite(*value)) {
return QStringLiteral("-");
}
return QString::number(*value, 'f', 3);
}
QStringList detectedSerialPorts()
{
const QDir deviceDirectory(QStringLiteral("/dev"));
QStringList patterns;
#ifdef Q_OS_MACOS
// macOS exposes USB UARTs as paired callout (cu.*) and terminal (tty.*)
// devices. Prefer cu.* because it does not wait for carrier detect.
patterns = {
QStringLiteral("cu.usb*"), QStringLiteral("cu.wchusbserial*"),
QStringLiteral("cu.SLAB_USBtoUART*"), QStringLiteral("cu.*USB*"),
QStringLiteral("tty.usb*"), QStringLiteral("tty.wchusbserial*"),
QStringLiteral("tty.SLAB_USBtoUART*"), QStringLiteral("tty.*USB*")};
#else
patterns = {QStringLiteral("ttyUSB*"), QStringLiteral("ttyACM*")};
#endif
QStringList ports;
for (const QString &pattern : patterns) {
const QStringList names = deviceDirectory.entryList(
{pattern}, QDir::System | QDir::Files, QDir::Name);
for (const QString &name : names) {
const QString path = QStringLiteral("/dev/") + name;
if (!ports.contains(path)) {
ports.push_back(path);
}
}
}
return ports;
}
QString fallbackSerialPort()
{
#ifdef Q_OS_MACOS
return QStringLiteral("/dev/cu.usbserial");
#else
return QStringLiteral("/dev/ttyUSB2");
#endif
}
void mergeDopValues(DopValues &target, const DopValues &source)
{
if (source.gdop.has_value()) {
target.gdop = source.gdop;
}
if (source.pdop.has_value()) {
target.pdop = source.pdop;
}
if (source.hdop.has_value()) {
target.hdop = source.hdop;
}
if (source.vdop.has_value()) {
target.vdop = source.vdop;
}
if (source.tdop.has_value()) {
target.tdop = source.tdop;
}
}
QString satelliteQualityText(const std::optional<int> &satelliteCount)
{
if (!satelliteCount.has_value()) {
return QStringLiteral("Uydu bekleniyor");
}
if (*satelliteCount >= 5) {
return QStringLiteral("Cozum icin yeterli");
}
if (*satelliteCount >= 4) {
return QStringLiteral("Minimum navigasyon seviyesi");
}
return QStringLiteral("Konum cozumu icin yetersiz");
}
} // namespace
class TelemetryTableModel final : public QAbstractTableModel {
public:
explicit TelemetryTableModel(QObject *parent = nullptr)
: QAbstractTableModel(parent)
{
}
void setPoints(const QVector<TelemetryPoint> &points)
{
beginResetModel();
m_points = points;
endResetModel();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
return parent.isValid() ? 0 : m_points.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
return parent.isValid() ? 0 : 7;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_points.size()) {
return {};
}
if (role == Qt::TextAlignmentRole) {
return Qt::AlignCenter;
}
if (role != Qt::DisplayRole) {
return {};
}
const TelemetryPoint &point = m_points[index.row()];
switch (index.column()) {
case 0:
return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz"));
case 1:
return locationSourceText(point.source);
case 2:
return coordinateText(point.latitude, point.latitudeText);
case 3:
return coordinateText(point.longitude, point.longitudeText);
case 4:
return optionalDoubleText(point.hheight, 2);
case 5:
return optionalIntText(point.satellites);
case 6:
return optionalDopText(point.dop.pdop);
default:
return {};
}
}
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override
{
if (role != Qt::DisplayRole || orientation != Qt::Horizontal) {
return {};
}
static const QStringList headers = {
QStringLiteral("Saat"), QStringLiteral("Kaynak"), QStringLiteral("Lat"), QStringLiteral("Long"),
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP")};
return section >= 0 && section < headers.size() ? headers[section] : QVariant();
}
private:
QVector<TelemetryPoint> m_points;
};
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, m_serial(new SerialReader(this))
, m_rinex(new RinexNavigation(this))
{
buildUi();
m_locationTimeoutTimer = new QTimer(this);
m_locationTimeoutTimer->setSingleShot(true);
m_locationTimeoutTimer->setInterval(LocationTimeoutMs);
m_uiRefreshTimer = new QTimer(this);
m_uiRefreshTimer->setSingleShot(true);
m_uiRefreshTimer->setInterval(UiRefreshIntervalMs);
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_locationTimeoutTimer, &QTimer::timeout, this, &MainWindow::handleLocationTimeout);
connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates);
connect(m_rinex, &RinexNavigation::satelliteMotionUpdated,
m_globe, &GlobeWidget::setSatelliteMotion);
connect(m_rinex, &RinexNavigation::statusChanged, this,
[this](const QString &status, bool error) {
m_globe->setRinexStatus(status, error);
statusBar()->showMessage(status, error ? 8000 : 4000);
});
m_rinex->refresh();
auto *rinexRefreshTimer = new QTimer(this);
rinexRefreshTimer->setInterval(15 * 60 * 1000);
connect(rinexRefreshTimer, &QTimer::timeout, m_rinex, &RinexNavigation::refresh);
rinexRefreshTimer->start();
resize(1480, 900);
setWindowTitle(QStringLiteral("Canli Konum Haritasi"));
}
void MainWindow::toggleSerial()
{
if (m_serial->isOpen()) {
m_serial->closePort();
if (m_locationTimeoutTimer != nullptr) {
m_locationTimeoutTimer->stop();
}
updateConnectionControls();
return;
}
if (m_serial->openPort(m_portCombo->currentText().trimmed(), selectedBaudRate())) {
updateConnectionControls();
}
}
void MainWindow::clearPoints()
{
if (m_locationTimeoutTimer != nullptr) {
m_locationTimeoutTimer->stop();
}
resetTelemetryState();
}
bool MainWindow::loadTelemetryLog(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
const QString message = QStringLiteral("Log acilamadi: %1").arg(file.errorString());
m_statusLabel->setText(message);
statusBar()->showMessage(message, 8000);
return false;
}
const QList<QByteArray> lines = file.readAll().split('\n');
resetTelemetryState();
const int replayCapacity = static_cast<int>(std::min<qsizetype>(10000, lines.size()));
m_maxPointsSpin->setValue(std::max(10, replayCapacity));
int parsedLocations = 0;
for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed();
if (line.isEmpty() || line.startsWith(QLatin1Char('#'))) {
continue;
}
const auto telemetry = LocationParser::parseLine(line);
if (telemetry.has_value() && telemetry->hasLocation()) {
++parsedLocations;
}
handleSerialLine(line);
}
flushUiUpdates();
m_map->fitToPoints();
const QString message = QStringLiteral("Replay yuklendi: %1 konum - %2")
.arg(parsedLocations).arg(filePath);
m_statusLabel->setText(message);
statusBar()->showMessage(message, 8000);
return parsedLocations > 0;
}
void MainWindow::openTelemetryLog()
{
const QString filePath = QFileDialog::getOpenFileName(
this, QStringLiteral("Sentetik telemetri logunu ac"), QString(),
QStringLiteral("Telemetry logs (*.log *.txt);;All files (*)"));
if (!filePath.isEmpty()) {
loadTelemetryLog(filePath);
}
}
void MainWindow::refreshSerialPorts()
{
const QString previousPort = m_portCombo->currentText().trimmed();
const QString configuredPort = qEnvironmentVariable("LOCATION_PLOTTER_PORT").trimmed();
const QStringList detectedPorts = detectedSerialPorts();
m_portCombo->clear();
for (const QString &port : detectedPorts) {
m_portCombo->addItem(port);
}
QString selectedPort;
if (!configuredPort.isEmpty()) {
selectedPort = configuredPort;
} else if (!previousPort.isEmpty() && detectedPorts.contains(previousPort)) {
selectedPort = previousPort;
} else if (!detectedPorts.isEmpty()) {
selectedPort = detectedPorts.first();
} else {
selectedPort = previousPort.isEmpty() ? fallbackSerialPort() : previousPort;
}
if (m_portCombo->findText(selectedPort) < 0) {
m_portCombo->addItem(selectedPort);
}
m_portCombo->setCurrentText(selectedPort);
}
void MainWindow::handleSerialLine(const QString &line)
{
static const QRegularExpression solveStartRegex(
QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"),
QRegularExpression::CaseInsensitiveOption);
const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch();
if (solveStarted) {
m_observedPrns.clear();
}
const QSet<QString> linePrns = LocationParser::parseGpsPrns(line);
if (!linePrns.isEmpty()) {
m_observedPrns.unite(linePrns);
}
if (solveStarted || !linePrns.isEmpty()) {
m_globe->setObservedPrns(m_observedPrns);
}
const std::optional<ParsedTelemetry> telemetry = LocationParser::parseLine(line);
const bool solveFinished = line.trimmed().startsWith(
QStringLiteral("NAV solve done"), Qt::CaseInsensitive);
appendLogLine(line, telemetry.has_value() || solveFinished);
if (!telemetry.has_value()) {
if (solveFinished && m_pendingTelemetry.hasLocation()) {
commitPendingTelemetry();
}
return;
}
if (telemetry->satellites.has_value()) {
m_lastSatelliteCount = telemetry->satellites;
}
if (telemetry->dop.hasAnyValue()) {
mergeDopValues(m_lastDop, telemetry->dop);
}
if (telemetry->hasLocation()) {
m_pendingTelemetry = {};
addTelemetryPoint(*telemetry);
return;
}
const bool hasLocationPart = telemetry->latitude.has_value() || telemetry->longitude.has_value()
|| telemetry->hheight.has_value();
if (hasLocationPart) {
mergePendingTelemetry(*telemetry);
if (m_pendingTelemetry.hasLocation() && telemetry->hheight.has_value()) {
commitPendingTelemetry();
} else {
scheduleTelemetryRefresh();
}
} else if (m_pendingTelemetry.hasLocation() && solveFinished) {
commitPendingTelemetry();
} else {
scheduleTelemetryRefresh();
}
}
void MainWindow::handleSerialStatus(const QString &status)
{
flushPendingLogLines();
m_statusLabel->setText(status);
m_log->appendPlainText(QStringLiteral("[STATUS] ") + status);
statusBar()->showMessage(status, 4000);
updateConnectionControls();
}
void MainWindow::handleSerialError(const QString &message)
{
flushPendingLogLines();
m_statusLabel->setText(message);
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
statusBar()->showMessage(message, 8000);
updateConnectionControls();
}
void MainWindow::handleLocationTimeout()
{
resetTelemetryState();
const QString message = QStringLiteral("10 sn konum gelmedi, degerler sifirlandi");
m_statusLabel->setText(message);
m_log->appendPlainText(QStringLiteral("[STATUS] ") + message);
statusBar()->showMessage(message, 8000);
}
void MainWindow::trimToMaxPoints()
{
const int maxPoints = m_maxPointsSpin->value();
while (m_points.size() > maxPoints) {
m_points.removeFirst();
}
m_telemetryRefreshPending = false;
refreshTelemetryViews();
}
void MainWindow::updateReferenceLocation()
{
const QString latitudeText = m_referenceLatEdit->text().trimmed();
const QString longitudeText = m_referenceLonEdit->text().trimmed();
const std::optional<double> latitude = parseCoordinateText(latitudeText, -90.0, 90.0);
const std::optional<double> longitude = parseCoordinateText(longitudeText, -180.0, 180.0);
const bool latitudeInvalid = !latitudeText.isEmpty() && !latitude.has_value();
const bool longitudeInvalid = !longitudeText.isEmpty() && !longitude.has_value();
m_referenceLatEdit->setStyleSheet(latitudeInvalid
? QStringLiteral("border: 1px solid #b42318;") : QString());
m_referenceLonEdit->setStyleSheet(longitudeInvalid
? QStringLiteral("border: 1px solid #b42318;") : QString());
if (latitude.has_value() && longitude.has_value()) {
m_referenceLatitude = latitude;
m_referenceLongitude = longitude;
} else {
m_referenceLatitude.reset();
m_referenceLongitude.reset();
}
updateAnalysis();
}
void MainWindow::flushUiUpdates()
{
flushPendingLogLines();
if (!m_telemetryRefreshPending) {
return;
}
m_telemetryRefreshPending = false;
refreshTelemetryViews();
}
void MainWindow::buildUi()
{
auto *central = new QWidget(this);
central->setObjectName(QStringLiteral("centralRoot"));
central->setStyleSheet(QStringLiteral(R"(
#centralRoot {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #08111d, stop:0.5 #101d2b, stop:1 #172737);
}
#brandBar, #controlBar {
background: rgba(18, 31, 45, 248);
border: 1px solid #30465a;
border-radius: 10px;
}
QLabel#appTitle {
color: #f2f7fb;
font-size: 18px;
font-weight: 800;
}
QLabel#appSubtitle {
color: #9fb0c1;
font-size: 11px;
}
QLabel#liveBadge {
color: #08745f;
background: #e4f5ef;
border: 1px solid #b7e4d5;
border-radius: 10px;
padding: 4px 10px;
font-weight: 700;
}
QGroupBox {
background: rgba(20, 34, 48, 248);
border: 1px solid #344a5f;
border-radius: 8px;
margin-top: 10px;
padding: 10px 8px 8px 8px;
font-weight: 700;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 4px;
color: #dce8f2;
}
QLabel {
color: #d5e0e9;
}
QLabel#metricValue {
color: #62b8e8;
font-size: 30px;
font-weight: 800;
}
QLabel#subtleText {
color: #94a8b9;
font-weight: 500;
}
QLabel#dopValue {
color: #eef5fa;
font-weight: 800;
}
QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox, QPlainTextEdit, QTableView {
color: #e7eef4;
background: #0d1925;
border: 1px solid #3b5267;
border-radius: 6px;
padding: 4px;
selection-background-color: #2d6f9f;
}
QComboBox QAbstractItemView {
color: #e7eef4;
background-color: #132332;
border: 1px solid #4b657c;
selection-color: #ffffff;
selection-background-color: #23658a;
outline: 0;
padding: 4px;
}
QComboBox QAbstractItemView::item {
min-height: 26px;
padding: 3px 8px;
}
QPushButton, QToolButton {
color: #e7eef4;
background: #182b3d;
border: 1px solid #456078;
border-radius: 6px;
padding: 5px 10px;
font-weight: 600;
}
QPushButton:hover, QToolButton:hover {
background: #23445d;
border-color: #62b8e8;
}
QPushButton:pressed, QToolButton:pressed { background: #dbeaf6; }
QCheckBox { spacing: 7px; color: #d5e0e9; }
QCheckBox::indicator { width: 17px; height: 17px; }
QCheckBox::indicator:unchecked {
background: #0d1925; border: 1px solid #6d8294; border-radius: 4px;
}
QCheckBox::indicator:checked {
background: #167b70; border: 1px solid #167b70; border-radius: 4px;
}
QHeaderView::section {
color: #dce8f2;
background: #1d3143;
border: 0;
border-right: 1px solid #d8dee7;
padding: 5px;
font-weight: 700;
}
QTabWidget::pane {
border: 1px solid #344a5f;
background: #081522;
}
QTabBar::tab {
color: #9fb0c1;
background: #101f2d;
border: 1px solid #344a5f;
border-bottom: 0;
padding: 7px 14px;
margin-right: 2px;
}
QTabBar::tab:selected {
color: #f1f7fb;
background: #1d3850;
border-top: 2px solid #57bceb;
}
QSplitter::handle { background: #263c4f; }
QSplitter::handle:hover { background: #57bceb; }
QScrollArea, QScrollArea > QWidget > QWidget { background: transparent; }
QWidget#dashboardPanel { background: #0b1723; }
)"));
auto *rootLayout = new QVBoxLayout(central);
rootLayout->setContentsMargins(10, 10, 10, 10);
rootLayout->setSpacing(8);
auto *brandBar = new QWidget(central);
brandBar->setObjectName(QStringLiteral("brandBar"));
auto *brandLayout = new QHBoxLayout(brandBar);
brandLayout->setContentsMargins(14, 8, 14, 8);
auto *logoLabel = new QLabel(brandBar);
const QPixmap logo(QStringLiteral(":/resources/ankasys-logo.png"));
logoLabel->setPixmap(logo.scaled(124, 52, Qt::KeepAspectRatio, Qt::SmoothTransformation));
logoLabel->setFixedSize(132, 54);
logoLabel->setAlignment(Qt::AlignCenter);
logoLabel->setToolTip(QStringLiteral("ANKASYS - Anka Microelectronic Systems"));
auto *titleLayout = new QVBoxLayout();
titleLayout->setSpacing(1);
auto *titleLabel = new QLabel(QStringLiteral("GNSS Konum Izleme Merkezi"), brandBar);
titleLabel->setObjectName(QStringLiteral("appTitle"));
auto *subtitleLabel = new QLabel(QStringLiteral("Gercek zamanli telemetri, sapma ve CEP50 analizi"), brandBar);
subtitleLabel->setObjectName(QStringLiteral("appSubtitle"));
titleLayout->addWidget(titleLabel);
titleLayout->addWidget(subtitleLabel);
auto *liveBadge = new QLabel(QStringLiteral("● SISTEM HAZIR"), brandBar);
liveBadge->setObjectName(QStringLiteral("liveBadge"));
brandLayout->addWidget(logoLabel);
brandLayout->addLayout(titleLayout);
brandLayout->addStretch(1);
brandLayout->addWidget(liveBadge);
rootLayout->addWidget(brandBar);
auto *controlBar = new QWidget(central);
controlBar->setObjectName(QStringLiteral("controlBar"));
auto *toolbarLayout = new QHBoxLayout(controlBar);
toolbarLayout->setContentsMargins(12, 7, 12, 7);
toolbarLayout->setSpacing(8);
m_portCombo = new QComboBox(this);
m_portCombo->setObjectName(QStringLiteral("portSelector"));
m_portCombo->setMinimumWidth(220);
m_portCombo->setEditable(true);
m_portCombo->setToolTip(QStringLiteral(
"USB UART portu. macOS'ta /dev/cu.usb... tercih edilir; /dev/tty.usb... da desteklenir."));
refreshSerialPorts();
auto *refreshPortsButton = new QToolButton(this);
refreshPortsButton->setObjectName(QStringLiteral("refreshPortsButton"));
refreshPortsButton->setText(QStringLiteral("Yenile"));
refreshPortsButton->setToolTip(QStringLiteral("Seri port listesini yenile"));
m_baudCombo = new QComboBox(this);
for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) {
m_baudCombo->addItem(QString::number(baud));
}
m_baudCombo->setCurrentText(QStringLiteral("115200"));
m_maxPointsSpin = new QSpinBox(this);
m_maxPointsSpin->setObjectName(QStringLiteral("maxPointsInput"));
m_maxPointsSpin->setRange(1, 10000);
m_maxPointsSpin->setValue(10);
m_maxPointsSpin->setSuffix(QStringLiteral(" nokta"));
m_connectButton = new QPushButton(this);
m_connectButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
m_clearButton = new QPushButton(QStringLiteral("Temizle"), this);
m_clearButton->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
m_openLogButton = new QPushButton(QStringLiteral("Senaryo Logu Ac"), this);
m_openLogButton->setObjectName(QStringLiteral("openLogButton"));
m_openLogButton->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton));
m_autoFollowCheck = new QCheckBox(QStringLiteral("Kamerayi takip et"), this);
m_autoFollowCheck->setChecked(true);
auto *zoomInButton = new QToolButton(this);
zoomInButton->setText(QStringLiteral("+"));
zoomInButton->setToolTip(QStringLiteral("Yaklastir"));
auto *zoomOutButton = new QToolButton(this);
zoomOutButton->setText(QStringLiteral("-"));
zoomOutButton->setToolTip(QStringLiteral("Uzaklastir"));
toolbarLayout->addWidget(new QLabel(QStringLiteral("Port"), this));
toolbarLayout->addWidget(m_portCombo);
toolbarLayout->addWidget(refreshPortsButton);
toolbarLayout->addWidget(new QLabel(QStringLiteral("Baud"), this));
toolbarLayout->addWidget(m_baudCombo);
toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this));
toolbarLayout->addWidget(m_maxPointsSpin);
toolbarLayout->addWidget(m_connectButton);
toolbarLayout->addWidget(m_openLogButton);
toolbarLayout->addWidget(m_clearButton);
toolbarLayout->addWidget(m_autoFollowCheck);
toolbarLayout->addWidget(zoomOutButton);
toolbarLayout->addWidget(zoomInButton);
toolbarLayout->addStretch(1);
rootLayout->addWidget(controlBar);
m_map = new MapWidget(this);
m_globe = new GlobeWidget(this);
auto *viewTabs = new QTabWidget(this);
viewTabs->setObjectName(QStringLiteral("mainViewTabs"));
viewTabs->addTab(m_map, QStringLiteral("2B Harita"));
viewTabs->addTab(m_globe, QStringLiteral("3B Dunya · Uydular"));
auto *sidePanel = new QWidget(this);
sidePanel->setMinimumWidth(390);
auto *sideOuterLayout = new QVBoxLayout(sidePanel);
sideOuterLayout->setContentsMargins(8, 0, 0, 0);
sideOuterLayout->setSpacing(0);
auto *dashboard = new QWidget(sidePanel);
dashboard->setObjectName(QStringLiteral("dashboardPanel"));
auto *sideLayout = new QVBoxLayout(dashboard);
sideLayout->setContentsMargins(4, 4, 4, 4);
sideLayout->setSpacing(8);
auto *statusGroup = new QGroupBox(QStringLiteral("Baglanti"), sidePanel);
auto *statusLayout = new QGridLayout(statusGroup);
m_countLabel = new QLabel(QStringLiteral("Konum: 0"), statusGroup);
m_statusLabel = new QLabel(QStringLiteral("Hazir"), statusGroup);
m_statusLabel->setObjectName(QStringLiteral("subtleText"));
m_statusLabel->setWordWrap(true);
statusLayout->addWidget(new QLabel(QStringLiteral("Durum"), statusGroup), 0, 0);
statusLayout->addWidget(m_statusLabel, 0, 1);
statusLayout->addWidget(new QLabel(QStringLiteral("Kayitli nokta"), statusGroup), 1, 0);
statusLayout->addWidget(m_countLabel, 1, 1);
statusLayout->setColumnStretch(1, 1);
sideLayout->addWidget(statusGroup);
auto *satelliteGroup = new QGroupBox(QStringLiteral("Uydu durumu"), sidePanel);
auto *satelliteLayout = new QVBoxLayout(satelliteGroup);
m_satelliteLabel = new QLabel(QStringLiteral("-"), satelliteGroup);
m_satelliteLabel->setObjectName(QStringLiteral("metricValue"));
m_satelliteLabel->setAlignment(Qt::AlignCenter);
m_satelliteQualityLabel = new QLabel(QStringLiteral("Uydu bekleniyor"), satelliteGroup);
m_satelliteQualityLabel->setObjectName(QStringLiteral("subtleText"));
m_satelliteQualityLabel->setAlignment(Qt::AlignCenter);
m_satelliteQualityLabel->setWordWrap(true);
satelliteLayout->addWidget(m_satelliteLabel);
satelliteLayout->addWidget(m_satelliteQualityLabel);
sideLayout->addWidget(satelliteGroup);
auto *dopGroup = new QGroupBox(QStringLiteral("DOP degerleri"), sidePanel);
auto *dopLayout = new QGridLayout(dopGroup);
m_gdopLabel = new QLabel(QStringLiteral("-"), dopGroup);
m_hdopLabel = new QLabel(QStringLiteral("-"), dopGroup);
m_vdopLabel = new QLabel(QStringLiteral("-"), dopGroup);
m_tdopLabel = new QLabel(QStringLiteral("-"), dopGroup);
const auto addDopRow = [dopLayout](int row, const QString &label, QLabel *value) {
auto *nameLabel = new QLabel(label);
nameLabel->setObjectName(QStringLiteral("subtleText"));
value->setObjectName(QStringLiteral("dopValue"));
value->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
value->setTextInteractionFlags(Qt::TextSelectableByMouse);
dopLayout->addWidget(nameLabel, row, 0);
dopLayout->addWidget(value, row, 1);
};
addDopRow(0, QStringLiteral("GDOP"), m_gdopLabel);
addDopRow(1, QStringLiteral("HDOP"), m_hdopLabel);
addDopRow(2, QStringLiteral("VDOP"), m_vdopLabel);
addDopRow(3, QStringLiteral("TDOP"), m_tdopLabel);
dopLayout->setColumnStretch(1, 1);
sideLayout->addWidget(dopGroup);
auto *analysisGroup = new QGroupBox(QStringLiteral("Konum analizi"), sidePanel);
auto *analysisLayout = new QGridLayout(analysisGroup);
auto *layersGroup = new QGroupBox(QStringLiteral("Harita katmanlari"), sidePanel);
auto *layersLayout = new QVBoxLayout(layersGroup);
m_showTelemetryCheck = new QCheckBox(QStringLiteral("Harita isaretlerini goster"), layersGroup);
m_showTelemetryCheck->setObjectName(QStringLiteral("showTelemetryCheck"));
m_showTelemetryCheck->setChecked(true);
m_showRecentPointsCheck = new QCheckBox(QStringLiteral("Son konumlari goster"), layersGroup);
m_showRecentPointsCheck->setObjectName(QStringLiteral("showRecentPointsCheck"));
m_showRecentPointsCheck->setChecked(true);
m_showAverageCheck = new QCheckBox(QStringLiteral("Ortalamayi goster"), layersGroup);
m_showAverageCheck->setObjectName(QStringLiteral("showAverageCheck"));
m_showAverageCheck->setChecked(true);
auto *elevationMaskSpin = new QDoubleSpinBox(layersGroup);
elevationMaskSpin->setObjectName(QStringLiteral("elevationMaskSpin"));
elevationMaskSpin->setRange(0.0, 90.0);
elevationMaskSpin->setDecimals(1);
elevationMaskSpin->setSingleStep(0.5);
elevationMaskSpin->setValue(10.0);
elevationMaskSpin->setKeyboardTracking(false);
elevationMaskSpin->setAccelerated(true);
elevationMaskSpin->setMinimumWidth(86);
elevationMaskSpin->setSuffix(QStringLiteral("°"));
elevationMaskSpin->setToolTip(
QStringLiteral("0-90 arasinda bir deger yazin ve Enter'a basin"));
auto *elevationMaskLayout = new QHBoxLayout();
elevationMaskLayout->addWidget(new QLabel(QStringLiteral("Min. elevation"), layersGroup));
elevationMaskLayout->addStretch(1);
elevationMaskLayout->addWidget(elevationMaskSpin);
layersLayout->addWidget(m_showTelemetryCheck);
layersLayout->addWidget(m_showRecentPointsCheck);
layersLayout->addWidget(m_showAverageCheck);
layersLayout->addLayout(elevationMaskLayout);
sideLayout->addWidget(layersGroup);
m_referenceLatEdit = new QLineEdit(analysisGroup);
m_referenceLatEdit->setObjectName(QStringLiteral("referenceLatInput"));
m_referenceLatEdit->setPlaceholderText(QStringLiteral("Orn. 40.789250342142"));
m_referenceLatEdit->setClearButtonEnabled(true);
m_referenceLatEdit->setMaxLength(32);
m_referenceLatEdit->setToolTip(QStringLiteral("Kullanici referansi; CEP50 hesabini etkilemez"));
m_referenceLonEdit = new QLineEdit(analysisGroup);
m_referenceLonEdit->setObjectName(QStringLiteral("referenceLonInput"));
m_referenceLonEdit->setPlaceholderText(QStringLiteral("Orn. 29.462775308417"));
m_referenceLonEdit->setClearButtonEnabled(true);
m_referenceLonEdit->setMaxLength(32);
m_referenceLonEdit->setToolTip(QStringLiteral("Kullanici referansi; CEP50 hesabini etkilemez"));
auto *mapSelectionHint = new QLabel(
QStringLiteral("Sag tik: kullanici referansi. CEP50, olcumlerin %50'sine gore otomatik hesaplanir."), analysisGroup);
mapSelectionHint->setObjectName(QStringLiteral("subtleText"));
mapSelectionHint->setWordWrap(true);
m_cepLabel = new QLabel(QStringLiteral("-"), analysisGroup);
m_cepLabel->setObjectName(QStringLiteral("cepValue"));
m_cepLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_cepLabel->setToolTip(QStringLiteral("Radyal konum hatalarinin yuzde 50 yuzdeligi"));
m_meanLocationLabel = new QLabel(QStringLiteral("-"), analysisGroup);
m_meanLocationLabel->setMinimumHeight(36);
m_lastDeviationLabel = new QLabel(QStringLiteral("-"), analysisGroup);
m_meanDeviationLabel = new QLabel(QStringLiteral("-"), analysisGroup);
m_rmsScatterLabel = new QLabel(QStringLiteral("-"), analysisGroup);
m_maxScatterLabel = new QLabel(QStringLiteral("-"), analysisGroup);
const auto addAnalysisRow = [analysisLayout](int row, const QString &label, QLabel *value) {
auto *nameLabel = new QLabel(label);
value->setTextInteractionFlags(Qt::TextSelectableByMouse);
value->setWordWrap(true);
analysisLayout->addWidget(nameLabel, row, 0);
analysisLayout->addWidget(value, row, 1);
};
analysisLayout->addWidget(mapSelectionHint, 0, 0, 1, 2);
analysisLayout->addWidget(new QLabel(QStringLiteral("Referans enlem"), analysisGroup), 1, 0);
analysisLayout->addWidget(m_referenceLatEdit, 1, 1);
analysisLayout->addWidget(new QLabel(QStringLiteral("Referans boylam"), analysisGroup), 2, 0);
analysisLayout->addWidget(m_referenceLonEdit, 2, 1);
analysisLayout->addWidget(new QLabel(QStringLiteral("CEP50"), analysisGroup), 3, 0);
analysisLayout->addWidget(m_cepLabel, 3, 1);
addAnalysisRow(4, QStringLiteral("Ortalama"), m_meanLocationLabel);
addAnalysisRow(5, QStringLiteral("Son sapma"), m_lastDeviationLabel);
addAnalysisRow(6, QStringLiteral("Ort. sapma"), m_meanDeviationLabel);
addAnalysisRow(7, QStringLiteral("RMS sacilim"), m_rmsScatterLabel);
addAnalysisRow(8, QStringLiteral("Maks sapma"), m_maxScatterLabel);
analysisLayout->setColumnStretch(1, 1);
sideLayout->addWidget(analysisGroup);
auto *telemetryTabs = new QTabWidget(sidePanel);
m_table = new QTableView(telemetryTabs);
m_tableModel = new TelemetryTableModel(m_table);
m_table->setModel(m_tableModel);
m_table->verticalHeader()->setVisible(false);
m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_table->setAlternatingRowColors(true);
m_log = new QPlainTextEdit(telemetryTabs);
m_log->setReadOnly(true);
m_log->setLineWrapMode(QPlainTextEdit::NoWrap);
m_log->setMaximumBlockCount(500);
m_log->setPlaceholderText(QStringLiteral("Ham seri debug satirlari"));
telemetryTabs->addTab(m_log, QStringLiteral("UART Log"));
telemetryTabs->addTab(m_table, QStringLiteral("Konumlar"));
telemetryTabs->setCurrentWidget(m_log);
telemetryTabs->setMinimumHeight(210);
auto *dashboardScroll = new QScrollArea(sidePanel);
dashboardScroll->setObjectName(QStringLiteral("dashboardScroll"));
dashboardScroll->setWidgetResizable(true);
dashboardScroll->setFrameShape(QFrame::NoFrame);
dashboardScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
dashboardScroll->setWidget(dashboard);
auto *sideSplitter = new QSplitter(Qt::Vertical, sidePanel);
sideSplitter->setObjectName(QStringLiteral("sideSplitter"));
sideSplitter->setChildrenCollapsible(false);
sideSplitter->setHandleWidth(7);
sideSplitter->addWidget(dashboardScroll);
sideSplitter->addWidget(telemetryTabs);
sideSplitter->setStretchFactor(0, 2);
sideSplitter->setStretchFactor(1, 3);
sideSplitter->setSizes({380, 410});
sideOuterLayout->addWidget(sideSplitter);
auto *splitter = new QSplitter(Qt::Horizontal, central);
splitter->setObjectName(QStringLiteral("mainSplitter"));
splitter->setChildrenCollapsible(false);
splitter->setHandleWidth(7);
splitter->addWidget(viewTabs);
splitter->addWidget(sidePanel);
splitter->setStretchFactor(0, 4);
splitter->setStretchFactor(1, 1);
splitter->setSizes({960, 500});
rootLayout->addWidget(splitter, 1);
setCentralWidget(central);
updateConnectionControls();
updateSummaryLabels();
updateAnalysis();
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(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts);
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
&MainWindow::trimToMaxPoints);
connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow);
connect(m_showTelemetryCheck, &QCheckBox::toggled,
m_map, &MapWidget::setTelemetryOverlayEnabled);
connect(m_showTelemetryCheck, &QCheckBox::toggled, this, [this](bool enabled) {
m_showRecentPointsCheck->setEnabled(enabled);
m_showAverageCheck->setEnabled(enabled);
});
connect(m_showRecentPointsCheck, &QCheckBox::toggled,
m_map, &MapWidget::setRecentPointsEnabled);
connect(m_showAverageCheck, &QCheckBox::toggled, m_map, &MapWidget::setAverageOverlayEnabled);
connect(elevationMaskSpin, qOverload<double>(&QDoubleSpinBox::valueChanged),
m_globe, &GlobeWidget::setElevationMask);
connect(m_referenceLatEdit, &QLineEdit::editingFinished,
this, &MainWindow::updateReferenceLocation);
connect(m_referenceLonEdit, &QLineEdit::editingFinished,
this, &MainWindow::updateReferenceLocation);
connect(m_map, &MapWidget::autoFollowChanged, m_autoFollowCheck, &QCheckBox::setChecked);
connect(m_map, &MapWidget::referenceLocationSelected, this,
[this](double latitude, double longitude) {
m_referenceLatEdit->setText(preciseCoordinateText(latitude));
m_referenceLonEdit->setText(preciseCoordinateText(longitude));
updateReferenceLocation();
statusBar()->showMessage(QStringLiteral("CEP merkezi haritadan secildi"), 4000);
});
connect(zoomInButton, &QToolButton::clicked, m_map, &MapWidget::zoomIn);
connect(zoomOutButton, &QToolButton::clicked, m_map, &MapWidget::zoomOut);
}
void MainWindow::updateConnectionControls()
{
const bool open = m_serial->isOpen();
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);
}
void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
{
TelemetryPoint point;
point.source = telemetry.source;
point.latitude = *telemetry.latitude;
point.longitude = *telemetry.longitude;
point.latitudeText = telemetry.latitudeText;
point.longitudeText = telemetry.longitudeText;
point.hheight = telemetry.hheight;
point.satellites = telemetry.satellites.has_value() ? telemetry.satellites : m_lastSatelliteCount;
point.dop = m_lastDop;
mergeDopValues(point.dop, telemetry.dop);
point.rawLine = telemetry.rawLine;
point.receivedAt = QDateTime::currentDateTimeUtc();
m_points.push_back(point);
m_globe->setReceiverLocation(point.latitude, point.longitude);
if (m_locationTimeoutTimer != nullptr && m_serial->isOpen()) {
m_locationTimeoutTimer->start();
}
const int maxPoints = m_maxPointsSpin->value();
while (m_points.size() > maxPoints) {
m_points.removeFirst();
}
scheduleTelemetryRefresh();
}
void MainWindow::refreshTable()
{
m_tableModel->setPoints(m_points);
if (m_tableModel->rowCount() > 0) {
m_table->scrollToBottom();
}
}
void MainWindow::updateSummaryLabels()
{
m_satelliteLabel->setText(
m_lastSatelliteCount.has_value()
? QString::number(*m_lastSatelliteCount)
: QStringLiteral("-"));
m_satelliteQualityLabel->setText(satelliteQualityText(m_lastSatelliteCount));
m_countLabel->setText(QStringLiteral("Konum: %1 / %2")
.arg(m_points.size())
.arg(m_maxPointsSpin->value()));
updateDopLabels();
}
void MainWindow::updateDopLabels()
{
m_gdopLabel->setText(optionalDopText(m_lastDop.gdop));
m_hdopLabel->setText(optionalDopText(m_lastDop.hdop));
m_vdopLabel->setText(optionalDopText(m_lastDop.vdop));
m_tdopLabel->setText(optionalDopText(m_lastDop.tdop));
}
void MainWindow::updateAnalysis()
{
const QVector<TelemetryPoint> analysisPoints = preferredAnalysisPoints(m_points);
const LocationAnalysis analysis = analyzeLocations(analysisPoints);
if (!analysis.valid) {
m_cepLabel->setText(QStringLiteral("-"));
m_meanLocationLabel->setText(QStringLiteral("-"));
m_lastDeviationLabel->setText(QStringLiteral("-"));
m_meanDeviationLabel->setText(QStringLiteral("-"));
m_rmsScatterLabel->setText(QStringLiteral("-"));
m_maxScatterLabel->setText(QStringLiteral("-"));
m_map->clearAverageMarker();
m_map->clearCepMarker();
return;
}
m_meanLocationLabel->setText(QStringLiteral("%1\n%2")
.arg(preciseCoordinateText(analysis.averageLat))
.arg(preciseCoordinateText(analysis.averageLon)));
m_lastDeviationLabel->setText(metersText(analysis.lastDeviationMeters));
m_meanDeviationLabel->setText(metersText(analysis.meanDeviationMeters));
m_rmsScatterLabel->setText(metersText(analysis.rmsScatterMeters));
m_maxScatterLabel->setText(metersText(analysis.maxDeviationMeters));
m_map->setAverageMarker(analysis.averageLat, analysis.averageLon,
analysis.rmsScatterMeters);
const double cep50 = calculateCep50(analysisPoints, analysis.averageLat,
analysis.averageLon);
m_cepLabel->setText(QStringLiteral("%1 (P50, N=%2)")
.arg(metersText(cep50))
.arg(analysisPoints.size()));
m_map->setCepMarker(analysis.averageLat, analysis.averageLon, cep50);
}
void MainWindow::appendLogLine(const QString &line, bool parsed)
{
const QString prefix = parsed ? QStringLiteral("[OK] ") : QStringLiteral("[..] ");
if (m_pendingLogLines.size() >= MaxPendingLogLines) {
m_pendingLogLines.removeFirst();
}
m_pendingLogLines.push_back(prefix + line);
if (m_uiRefreshTimer != nullptr && !m_uiRefreshTimer->isActive()) {
m_uiRefreshTimer->start();
}
}
void MainWindow::mergePendingTelemetry(const ParsedTelemetry &telemetry)
{
if (telemetry.latitude.has_value() && m_pendingTelemetry.hasLocation()) {
commitPendingTelemetry();
}
if (telemetry.latitude.has_value()) {
m_pendingTelemetry.source = telemetry.source;
m_pendingTelemetry.latitude = telemetry.latitude;
m_pendingTelemetry.latitudeText = telemetry.latitudeText;
}
if (telemetry.longitude.has_value()) {
m_pendingTelemetry.longitude = telemetry.longitude;
m_pendingTelemetry.longitudeText = telemetry.longitudeText;
}
if (telemetry.hheight.has_value()) {
m_pendingTelemetry.hheight = telemetry.hheight;
}
if (telemetry.satellites.has_value()) {
m_pendingTelemetry.satellites = telemetry.satellites;
}
if (telemetry.dop.hasAnyValue()) {
mergeDopValues(m_pendingTelemetry.dop, telemetry.dop);
}
if (m_pendingTelemetry.rawLine.isEmpty()) {
m_pendingTelemetry.rawLine = telemetry.rawLine;
} else if (!telemetry.rawLine.isEmpty()) {
m_pendingTelemetry.rawLine += QStringLiteral(" | ") + telemetry.rawLine;
}
}
void MainWindow::commitPendingTelemetry()
{
if (!m_pendingTelemetry.hasLocation()) {
return;
}
addTelemetryPoint(m_pendingTelemetry);
m_pendingTelemetry = {};
}
void MainWindow::resetTelemetryState()
{
m_telemetryRefreshPending = false;
m_points.clear();
m_lastSatelliteCount.reset();
m_observedPrns.clear();
m_lastDop = {};
m_pendingTelemetry = {};
m_map->clearPoints();
m_globe->clearReceiverLocation();
m_globe->setObservedPrns({});
refreshTable();
updateSummaryLabels();
updateAnalysis();
}
void MainWindow::scheduleTelemetryRefresh()
{
m_telemetryRefreshPending = true;
if (m_uiRefreshTimer != nullptr && !m_uiRefreshTimer->isActive()) {
m_uiRefreshTimer->start();
}
}
void MainWindow::refreshTelemetryViews()
{
m_map->setPoints(m_points);
refreshTable();
updateSummaryLabels();
updateAnalysis();
}
void MainWindow::flushPendingLogLines()
{
if (m_pendingLogLines.isEmpty()) {
return;
}
m_log->appendPlainText(m_pendingLogLines.join('\n'));
m_pendingLogLines.clear();
}
int MainWindow::selectedBaudRate() const
{
bool ok = false;
const int baudRate = m_baudCombo->currentText().toInt(&ok);
return ok ? baudRate : 115200;
}