Merge Linux development branch

This commit is contained in:
onur cayan
2026-08-14 13:03:15 +03:00
22 changed files with 1947 additions and 283 deletions
+1
View File
@@ -8,3 +8,4 @@ build-*/
*.autosave
*.swp
*~
uart_logs/
+46 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20)
project(location_plotter VERSION 0.1.0 LANGUAGES CXX)
project(location_plotter VERSION 0.3.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -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,19 @@ 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
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "Location Plotter"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.ankasys.location-plotter"
MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
)
endif()
qt_add_resources(location_plotter "app_resources"
PREFIX "/"
@@ -52,6 +67,9 @@ add_executable(location_parser_tests
src/TelemetryPoint.h
)
target_link_libraries(location_parser_tests PRIVATE Qt6::Core Qt6::Network ZLIB::ZLIB)
target_compile_options(location_parser_tests PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG>
)
add_test(NAME location_parser_tests COMMAND location_parser_tests)
add_executable(location_main_window_tests
@@ -66,12 +84,20 @@ 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>
)
qt_add_resources(location_main_window_tests "test_app_resources"
PREFIX "/"
FILES resources/ankasys-logo.png resources/land-110m.json
@@ -82,3 +108,22 @@ endif()
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
)
+130 -5
View File
@@ -1,7 +1,7 @@
# Location Plotter
Qt6/C++ GUI that listens to `/dev/ttyUSB2`, parses normal debug lines, and plots live
locations on a map.
Qt6/C++ GUI that listens to a USB UART on Linux or macOS, parses normal debug lines,
and plots live locations on a map.
The application also includes an interactive 3D Earth/orbit view. It downloads the current
stream-derived RINEX 3 navigation file from the BKG/IGS archive at startup and every 15
@@ -24,6 +24,9 @@ VDOP : 2.2370935468
TDOP : 2.2526165852
tracking PRNs: G03, G08, 14, 21
SV=G27
[LOCATION] source=gps lat=40.7357 lon=31.5985 height=812.0 gdop=4.7 pdop=4.1 hdop=2.7 vdop=3.0 tdop=2.3 tracked=6 used=5
[LOCATION] source=imu_corrected lat=40.7356 lon=31.5985 height=812.7 gdop=4.7 pdop=4.1 hdop=2.7 vdop=3.0 tdop=2.3 tracked=6 used=5
[LOCATION] source=trajectory lat=40.7358 lon=31.5987 height=812.1 tracked=4 used=4
lattitude : 40.78925034214206846173
longitude : 29.46277530841727809729
height : -89.05779508221894502640
@@ -38,6 +41,9 @@ Accepted location labels:
`activeSats`, `satellites_active`, `num_sats`, `uydu`
- DOP: `GDOP`, `PDOP`, `HDOP`, `VDOP`, `TDOP`
- GPS identifiers: `G03`, `PRN: 3`, `PRNs: 3, 8, 14`, `SV=G21`, `SVID: 27`
- Location source: `source=gps`, `source=imu_corrected`, `source=trajectory`
- `tracked` is accepted as the active satellite count; `used` remains visible in
the raw UART line and denotes satellites used by the WLS solution.
If a satellite count arrives without a location, the active satellite display is updated.
If a location arrives without a satellite count, the latest known satellite count is attached
@@ -47,6 +53,109 @@ section and are attached to the next plotted point.
If latitude, longitude, and height arrive as separate consecutive lines, they are combined
into a single plotted point.
The GPS firmware's level-1 paired protocol is source-aware. Every received
`source=gps` location is added to the blue GPS series and every
`source=imu_corrected` location is added to the red corrected series at the
firmware-configured UART cadence.
Paths are built independently, so alternating UART lines do not create GPS-to-IMU
zigzags. A corrected point newer than one second is also preferred for map
auto-follow, the current-position label and the globe receiver marker. Raw GPS
outliers remain visible in blue for diagnosis without pulling the whole view away.
When corrected points exist, CEP50, average and scatter calculations use only that
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:
```bash
cd /home/onur/Desktop/gps
make -C soc/software/fusion/tests scenario-logs
```
Sonra GUI'yi açıp `Senaryo Logu Ac` düğmesinden örneğin
`soc/software/fusion/tests/output/scenarios/car_gps200_imu20.log` dosyasını seçin.
GUI logdaki bütün rotayı otomatik sığdırır. Aynı dosya komut satırından da
doğrudan açılabilir:
```bash
cd /home/onur/Desktop/location_plotter
cmake -S . -B build
cmake --build build -j
./build/location_plotter --replay \
/home/onur/Desktop/gps/soc/software/fusion/tests/output/scenarios/car_gps200_imu20.log
```
Tek bir özel kombinasyon üretmek için:
```bash
cd /home/onur/Desktop/gps/soc/software/fusion/tests
make platform_scenario_matrix_test
./platform_scenario_matrix_test --log aircraft 500 19 aircraft.log
```
Desteklenen senaryolar `stationary`, `pedestrian`, `car`, `aircraft`; GNSS
periyotları 200/500/1000 ms, IMU periyotları 19/20/200/300 ms'dir.
## macOS'ta kurulum (MacBook Air, Apple Silicon veya Intel)
Guncel macOS surumunde once [Homebrew](https://brew.sh) kurulu olmalidir. Ardindan:
```bash
cd location_plotter
chmod +x setup-macos.sh run.sh
./setup-macos.sh
./run.sh
```
Betik Homebrew ile guncel `cmake` ve `qt` paketlerini kurar, yerel mimari icin
`location_plotter.app` paketini derler, gerekli Qt kutuphanelerini uygulama paketine
ekler ve testleri calistirir.
macOS USB UART aygitlarini genellikle iki adla gosterir:
```text
/dev/cu.usbserial-XXXXXXXX
/dev/tty.usbserial-XXXXXXXX
```
veya bir CDC aygitinda:
```text
/dev/cu.usbmodemXXXX
/dev/tty.usbmodemXXXX
```
Uygulama her iki bicimi de algilar. Mümkünse `cu.usb...` yolunu secin; macOS'ta
disariya baglanti baslatan uygulamalar icin uygun olan aygit budur. UART uygulama
acikken takildiysa port alaninin yanindaki `Yenile` dugmesine basin. Otomatik
algilanmayan bir yol port alanina elle de yazilabilir.
Istenen port baslatmadan once ortam degiskeniyle de secilebilir:
```bash
LOCATION_PLOTTER_PORT=/dev/cu.usbserial-XXXXXXXX ./run.sh
```
Port hic gorunmuyorsa `ls /dev/cu.usb* /dev/tty.usb*` ile macOS'un aygiti olusturup
olusturmadigini kontrol edin. Hic sonuc yoksa kabloyu/veri baglantisini ve UART
donaniminizin macOS surucusunu kontrol edin.
## Ubuntu'da kurulum
Ubuntu 22.04 ve daha yeni surumlerde gerekli paketleri kurmak, uygulamayi derlemek ve
@@ -92,8 +201,11 @@ Uygulama Qt tabanli bir masaustu arayuzudur; grafik oturumu olan bir Ubuntu maki
calistirilmalidir. Harita karolari ve guncel RINEX verisi icin internet erisimi gerekir,
ancak son gecerli RINEX verisi ve harita onbellegiyle cevrimdisi kullanim da desteklenir.
The port selector lists only detected `/dev/ttyUSB*` devices. The default selection is
`/dev/ttyUSB2` and the default baud rate is `115200`.
Port secici Linux'ta `/dev/ttyUSB*` ve `/dev/ttyACM*`; macOS'ta USB ile ilgili
`/dev/cu.*` ve `/dev/tty.*` aygitlarini listeler. Hic aygit algilanmazsa Linux'ta
`/dev/ttyUSB2`, macOS'ta `/dev/cu.usbserial` duzenlenebilir bir varsayilan yol olarak
gosterilir. 50 Hz IMU-duzeltilmis konum akisini kayipsiz tasimak icin varsayilan baud
rate `460800`'dur.
If the port cannot be opened, make sure the user has serial access:
@@ -122,7 +234,14 @@ without logging out.
receiver marker without resetting a globe orientation chosen by dragging.
- RINEX download starts at every launch, repeats every 15 minutes, and falls back through the
previous two UTC days if today's stream file is unavailable. Current and next-minute satellite
positions are propagated from the cached ephemeris, then smoothly interpolated at 30 FPS.
positions are propagated from the cached ephemeris, then smoothly interpolated at 10 FPS.
- The 3D Earth tab includes a `RINEX Guncelle` button for an immediate manual refresh. Its status
line shows the system-local time of the last successful download or cache load.
- Complete UART channel snapshots such as `tracked satellites=5 prn_indices=[...]` replace the
previous connected-PRN set, so satellites that were lost no longer remain green. Firmware
channel indices are zero-based and are normalized for display (`0` is GPS PRN `G01`).
- RINEX text is parsed once per download and retained as compact ephemeris records. The static
globe is cached between camera changes, reducing CPU and allocation load during orbit animation.
- The globe uses an embedded Natural Earth coastline mask with terrain-like land colouring,
shallow-water edges, atmospheric shading, and a cached Earth-fixed texture for smooth rotation.
- The main content width and the right-side dashboard/UART height are both adjustable by
@@ -136,6 +255,9 @@ without logging out.
marker can be toggled on the map.
- The map layers panel can hide all telemetry overlays, show only the newest location instead
of the recent path, and independently toggle the average marker.
- GPS points and paths are blue. IMU+ESKF corrected points and their independent
path are red. Synthetic ground-truth trajectory is green; the locations table
shows the parsed source in its `Kaynak` column.
- The application calculates empirical CEP50 as the median horizontal distance of the retained
measurements from their spherical mean, then draws the circle around that measurement center.
This is the radius containing 50 percent of the retained position fixes and is independent of
@@ -152,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.
+24 -7
View File
@@ -5,29 +5,46 @@ cd "$(dirname "$0")"
print_qt_install_hint() {
echo
echo "Qt6 bulunamadi. Ubuntu'da gerekli paketleri kurmak icin:"
echo " sudo apt update"
echo " sudo apt install build-essential cmake qt6-base-dev"
if [[ "$(uname -s)" == "Darwin" ]]; then
echo "Qt6 bulunamadi. macOS'ta gerekli paketleri kurmak icin:"
echo " brew install cmake qt"
echo "veya doğrudan: ./setup-macos.sh"
else
echo "Qt6 bulunamadi. Ubuntu'da gerekli paketleri kurmak icin:"
echo " sudo apt update"
echo " sudo apt install build-essential cmake qt6-base-dev"
fi
}
project_dir="$(pwd -P)"
if [[ -f ./build/CMakeCache.txt ]]; then
cached_source="$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' ./build/CMakeCache.txt)"
if [[ -n "$cached_source" && "$cached_source" != "$project_dir" ]]; then
echo "Build klasoru farkli bir proje yoluna ait, yeniden olusturuluyor:"
echo " $cached_source"
cached_system="$(sed -n 's/^CMAKE_SYSTEM_NAME:INTERNAL=//p' ./build/CMakeCache.txt)"
host_system="$(uname -s)"
if [[ ( -n "$cached_source" && "$cached_source" != "$project_dir" ) \
|| ( -n "$cached_system" && "$cached_system" != "$host_system" ) ]]; then
echo "Build klasoru farkli bir proje yolu veya sisteme ait, yeniden olusturuluyor."
rm -rf ./build
fi
fi
if ! cmake -S . -B build; then
cmake_args=(-S . -B build)
if [[ "$(uname -s)" == "Darwin" ]] && command -v brew >/dev/null 2>&1; then
cmake_args+=("-DCMAKE_PREFIX_PATH=$(brew --prefix qt)")
fi
if ! cmake "${cmake_args[@]}"; then
print_qt_install_hint
exit 1
fi
cmake --build build
if [[ "$(uname -s)" == "Darwin" ]]; then
exec ./build/location_plotter.app/Contents/MacOS/location_plotter "$@"
fi
missing_libs="$(ldd ./build/location_plotter 2>/dev/null | awk '/not found/ { print $1 }' || true)"
if [[ -n "$missing_libs" ]]; then
echo "Eksik paylasimli kutuphaneler:"
Executable
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "Bu kurulum betigi macOS icindir." >&2
exit 1
fi
if ! command -v brew >/dev/null 2>&1; then
echo "Homebrew bulunamadi. Once https://brew.sh adresindeki Homebrew kurulumunu tamamlayin." >&2
exit 1
fi
echo "Gerekli macOS paketleri kuruluyor..."
brew install cmake qt
qt_prefix="$(brew --prefix qt)"
parallel_jobs="$(sysctl -n hw.logicalcpu 2>/dev/null || echo 4)"
if [[ -f build/CMakeCache.txt ]]; then
cached_system="$(sed -n 's/^CMAKE_SYSTEM_NAME:INTERNAL=//p' build/CMakeCache.txt)"
if [[ -n "$cached_system" && "$cached_system" != "Darwin" ]]; then
echo "Baska bir sisteme ait build klasoru yeniden olusturuluyor."
rm -rf build
fi
fi
echo "Uygulama derleniyor..."
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$qt_prefix"
cmake --build build --parallel "$parallel_jobs"
echo "Testler calistiriliyor..."
ctest --test-dir build --output-on-failure
echo "macOS uygulama paketi hazirlaniyor..."
"$qt_prefix/bin/macdeployqt" build/location_plotter.app -always-overwrite
echo
echo "Kurulum tamamlandi. Uygulamayi baslatmak icin:"
echo " ./run.sh"
echo
echo "USB UART icin port listesinden /dev/cu.usb... aygitini secin."
echo "/dev/tty.usb... gorunuyorsa o yol da desteklenir."
+129 -73
View File
@@ -15,6 +15,7 @@
#include <algorithm>
#include <cmath>
#include <utility>
namespace {
constexpr double Pi = 3.14159265358979323846;
@@ -211,8 +212,10 @@ GlobeWidget::GlobeWidget(QWidget *parent)
setMouseTracking(true);
setCursor(Qt::OpenHandCursor);
setToolTip(QStringLiteral("Dunyayi dondurmek icin surukleyin, yakinlastirmak icin tekerlegi kullanin"));
m_satelliteAnimationTimer.setInterval(33);
m_satelliteAnimationTimer.setTimerType(Qt::PreciseTimer);
// GNSS satellites move only a few screen pixels per minute. Ten frames per
// second is visually smooth here and avoids repainting a static globe at 30 FPS.
m_satelliteAnimationTimer.setInterval(100);
m_satelliteAnimationTimer.setTimerType(Qt::CoarseTimer);
connect(&m_satelliteAnimationTimer, &QTimer::timeout,
this, &GlobeWidget::updateSatelliteAnimation);
}
@@ -235,8 +238,29 @@ void GlobeWidget::setSatelliteMotion(const QVector<SatellitePosition> &current,
// Continue from the currently rendered points when a fresh ephemeris arrives,
// avoiding even a small visual jump at the update boundary.
m_satelliteMotionStart = m_satellites.isEmpty() ? current : m_satellites;
m_satelliteMotionTarget = next;
const QVector<SatellitePosition> &candidateStart =
m_satellites.isEmpty() ? current : m_satellites;
QHash<QString, SatellitePosition> starts;
starts.reserve(candidateStart.size());
for (const SatellitePosition &satellite : candidateStart) {
starts.insert(satellite.prn, satellite);
}
m_satelliteMotionStart.clear();
m_satelliteMotionTarget.clear();
m_satelliteMotionStart.reserve(next.size());
m_satelliteMotionTarget.reserve(next.size());
for (const SatellitePosition &target : next) {
const auto start = starts.constFind(target.prn);
if (start == starts.cend()) continue;
m_satelliteMotionStart.push_back(start.value());
m_satelliteMotionTarget.push_back(target);
}
if (m_satelliteMotionStart.isEmpty()) {
setSatellites(current);
m_satelliteAnimationTimer.stop();
return;
}
m_satelliteMotionDurationMs = durationMs;
m_satelliteMotionClock.restart();
m_satelliteAnimationTimer.start();
@@ -250,18 +274,11 @@ void GlobeWidget::updateSatelliteAnimation()
const double progress = std::clamp(
static_cast<double>(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs,
0.0, 1.0);
QHash<QString, SatellitePosition> targets;
targets.reserve(m_satelliteMotionTarget.size());
for (const SatellitePosition &satellite : m_satelliteMotionTarget) {
targets.insert(satellite.prn, satellite);
}
QVector<SatellitePosition> interpolated;
interpolated.reserve(m_satelliteMotionStart.size());
for (const SatellitePosition &start : m_satelliteMotionStart) {
const auto targetIt = targets.constFind(start.prn);
if (targetIt == targets.cend()) continue;
const SatellitePosition &target = targetIt.value();
for (qsizetype i = 0; i < m_satelliteMotionStart.size(); ++i) {
const SatellitePosition &start = m_satelliteMotionStart[i];
const SatellitePosition &target = m_satelliteMotionTarget[i];
SatellitePosition point = start;
point.x += (target.x - start.x) * progress;
point.y += (target.y - start.y) * progress;
@@ -336,6 +353,74 @@ void GlobeWidget::paintEvent(QPaintEvent *)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
const qreal devicePixelRatio = devicePixelRatioF();
if (m_staticLayer.isNull() || m_staticLayerSize != size()
|| !qFuzzyCompare(m_staticLayerDevicePixelRatio, devicePixelRatio)
|| m_staticLayerYaw != m_yaw || m_staticLayerPitch != m_pitch
|| m_staticLayerZoom != m_zoom) {
rebuildStaticLayer();
}
painter.drawPixmap(0, 0, m_staticLayer);
if (m_hasReceiver) drawVisibilityCone(painter);
if (m_hasReceiver) drawReceiver(painter);
const int visibleCount = drawSatellites(painter);
painter.setPen(QColor(229, 240, 249));
QFont titleFont = painter.font();
titleFont.setPixelSize(18);
titleFont.setBold(true);
painter.setFont(titleFont);
painter.drawText(QRect(20, 16, width() - 40, 28), Qt::AlignLeft | Qt::AlignVCenter,
QStringLiteral("GNSS YORUNGE GORUNUMU"));
QFont smallFont = painter.font();
smallFont.setPixelSize(11);
smallFont.setBold(false);
painter.setFont(smallFont);
painter.setPen(m_statusError ? QColor(255, 177, 77) : QColor(132, 177, 204));
painter.drawText(QRect(20, 43, width() - 40, 22), Qt::AlignLeft | Qt::AlignVCenter, m_status);
const int legendY = height() - 25;
const int legendX = width() / 2 - 125;
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(42, 224, 137));
painter.drawEllipse(QPointF(legendX, legendY - 3), 4, 4);
painter.setPen(QColor(101, 239, 176));
painter.drawText(legendX + 9, legendY + 1, QStringLiteral("BULUNDU"));
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(255, 79, 95));
painter.drawEllipse(QPointF(legendX + 78, legendY - 3), 4, 4);
painter.setPen(QColor(255, 132, 143));
painter.drawText(legendX + 87, legendY + 1, QStringLiteral("BULUNAMADI"));
painter.setPen(QColor(138, 183, 207));
painter.drawText(legendX + 181, legendY + 1,
QStringLiteral("%1/%2 PRN · ELEV≥%3°: %4")
.arg(m_observedPrns.size()).arg(m_satellites.size())
.arg(m_elevationMaskDegrees, 0, 'f', 1).arg(visibleCount));
if (!m_hasReceiver) {
painter.setPen(QColor(219, 232, 241));
QFont waitingFont = painter.font();
waitingFont.setPixelSize(16);
waitingFont.setBold(true);
painter.setFont(waitingFont);
painter.drawText(rect().adjusted(30, 70, -30, -50), Qt::AlignCenter,
QStringLiteral("Ilk gecerli konum bekleniyor"));
}
}
void GlobeWidget::rebuildStaticLayer()
{
const qreal devicePixelRatio = devicePixelRatioF();
const QSize pixelSize(
std::max(1, qRound(width() * devicePixelRatio)),
std::max(1, qRound(height() * devicePixelRatio)));
QPixmap layer(pixelSize);
layer.setDevicePixelRatio(devicePixelRatio);
layer.fill(Qt::transparent);
QPainter painter(&layer);
painter.setRenderHint(QPainter::Antialiasing, true);
QLinearGradient background(0, 0, width(), height());
background.setColorAt(0.0, QColor(3, 10, 21));
background.setColorAt(0.55, QColor(8, 24, 43));
@@ -374,63 +459,23 @@ void GlobeWidget::paintEvent(QPaintEvent *)
painter.restore();
for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0);
if (m_hasReceiver) drawVisibilityCone(painter);
if (m_hasReceiver) drawReceiver(painter);
drawSatellites(painter);
painter.end();
painter.setPen(QColor(229, 240, 249));
QFont titleFont = painter.font();
titleFont.setPixelSize(18);
titleFont.setBold(true);
painter.setFont(titleFont);
painter.drawText(QRect(20, 16, width() - 40, 28), Qt::AlignLeft | Qt::AlignVCenter,
QStringLiteral("GNSS YORUNGE GORUNUMU"));
QFont smallFont = painter.font();
smallFont.setPixelSize(11);
smallFont.setBold(false);
painter.setFont(smallFont);
painter.setPen(m_statusError ? QColor(255, 177, 77) : QColor(132, 177, 204));
painter.drawText(QRect(20, 43, width() - 40, 22), Qt::AlignLeft | Qt::AlignVCenter, m_status);
const int legendY = height() - 25;
const int legendX = width() / 2 - 125;
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(42, 224, 137));
painter.drawEllipse(QPointF(legendX, legendY - 3), 4, 4);
painter.setPen(QColor(101, 239, 176));
painter.drawText(legendX + 9, legendY + 1, QStringLiteral("BULUNDU"));
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(255, 79, 95));
painter.drawEllipse(QPointF(legendX + 78, legendY - 3), 4, 4);
painter.setPen(QColor(255, 132, 143));
painter.drawText(legendX + 87, legendY + 1, QStringLiteral("BULUNAMADI"));
painter.setPen(QColor(138, 183, 207));
int visibleCount = 0;
for (const auto &satellite : m_satellites) {
if (satelliteElevation(satellite) >= m_elevationMaskDegrees) ++visibleCount;
}
painter.drawText(legendX + 181, legendY + 1,
QStringLiteral("%1/%2 PRN · ELEV≥%3°: %4")
.arg(m_observedPrns.size()).arg(m_satellites.size())
.arg(m_elevationMaskDegrees, 0, 'f', 1).arg(visibleCount));
if (!m_hasReceiver) {
painter.setPen(QColor(219, 232, 241));
QFont waitingFont = painter.font();
waitingFont.setPixelSize(16);
waitingFont.setBold(true);
painter.setFont(waitingFont);
painter.drawText(rect().adjusted(30, 70, -30, -50), Qt::AlignCenter,
QStringLiteral("Ilk gecerli konum bekleniyor"));
}
m_staticLayer = std::move(layer);
m_staticLayerSize = size();
m_staticLayerDevicePixelRatio = devicePixelRatio;
m_staticLayerYaw = m_yaw;
m_staticLayerPitch = m_pitch;
m_staticLayerZoom = m_zoom;
}
void GlobeWidget::drawContinents(QPainter &painter) const
{
const double radius = std::min(width(), height()) * 0.255 * m_zoom;
const double pixelRatio = std::min(devicePixelRatioF(), 1.5);
const double pixelRatio = m_dragging ? 0.75 : std::min(devicePixelRatioF(), 1.5);
const int textureSize = std::clamp(
static_cast<int>(std::ceil(radius * 2.0 * pixelRatio)), 320, 900);
static_cast<int>(std::ceil(radius * 2.0 * pixelRatio)),
m_dragging ? 240 : 320, m_dragging ? 480 : 900);
const QPointF center(width() * 0.5, height() * 0.52);
const QRectF target(center.x() - radius, center.y() - radius,
radius * 2.0, radius * 2.0);
@@ -715,33 +760,41 @@ void GlobeWidget::drawReceiver(QPainter &painter) const
painter.drawText(p.point + QPointF(10, -8), QStringLiteral("ALICI"));
}
void GlobeWidget::drawSatellites(QPainter &painter) const
int GlobeWidget::drawSatellites(QPainter &painter) const
{
if (!m_hasReceiver) return;
if (!m_hasReceiver) return 0;
const double orbitRadius = std::min(width(), height()) * 0.255 * m_zoom * 1.47;
struct Item { SatellitePosition satellite; ProjectedPoint projected; };
struct Item {
SatellitePosition satellite;
ProjectedPoint projected;
double elevation = -90.0;
};
QVector<Item> items;
items.reserve(m_satellites.size());
for (const auto &satellite : m_satellites) {
const double length = std::sqrt(satellite.x * satellite.x + satellite.y * satellite.y
+ satellite.z * satellite.z);
if (length <= 0.0) continue;
items.push_back({satellite, project(satellite.x / length, satellite.y / length,
satellite.z / length, orbitRadius)});
satellite.z / length, orbitRadius),
satelliteElevation(satellite)});
}
std::sort(items.begin(), items.end(), [](const Item &a, const Item &b) {
return a.projected.depth < b.projected.depth;
});
const double earthRadius = std::min(width(), height()) * 0.255 * m_zoom;
const Vec3 receiver = globeVector(m_receiverLatitude, m_receiverLongitude);
const auto receiverPoint = project(receiver.x, receiver.y, receiver.z,
earthRadius * 1.02);
int visibleCount = 0;
for (const Item &item : items) {
const bool found = m_observedPrns.contains(item.satellite.prn);
const bool inElevationMask = satelliteElevation(item.satellite) >= m_elevationMaskDegrees;
const bool inElevationMask = item.elevation >= m_elevationMaskDegrees;
if (inElevationMask) ++visibleCount;
QColor color = found ? QColor(42, 224, 137) : QColor(255, 79, 95);
if (item.projected.depth < 0.0) color.setAlpha(75);
const double markerRadius = found ? 6.5 : 5.0;
if (inElevationMask) {
const double earthRadius = std::min(width(), height()) * 0.255 * m_zoom;
const Vec3 receiver = globeVector(m_receiverLatitude, m_receiverLongitude);
const auto receiverPoint = project(receiver.x, receiver.y, receiver.z,
earthRadius * 1.02);
QColor sightLine(70, 224, 231, item.projected.depth < 0.0 ? 25 : 80);
painter.setPen(QPen(sightLine, 0.8, Qt::DashLine));
painter.drawLine(receiverPoint.point, item.projected.point);
@@ -757,6 +810,7 @@ void GlobeWidget::drawSatellites(QPainter &painter) const
painter.drawText(item.projected.point + QPointF(7, -6), item.satellite.prn);
}
}
return visibleCount;
}
double GlobeWidget::satelliteElevation(const SatellitePosition &satellite) const
@@ -802,7 +856,9 @@ void GlobeWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_dragging = false;
m_staticLayer = QPixmap();
setCursor(Qt::OpenHandCursor);
update();
}
}
+10 -1
View File
@@ -5,6 +5,7 @@
#include <QElapsedTimer>
#include <QImage>
#include <QPoint>
#include <QPixmap>
#include <QSet>
#include <QTimer>
#include <QWidget>
@@ -23,6 +24,7 @@ public:
void setReceiverLocation(double latitude, double longitude);
void clearReceiverLocation();
void setRinexStatus(const QString &status, bool error);
[[nodiscard]] QSet<QString> observedPrns() const { return m_observedPrns; }
public slots:
void setElevationMask(double degrees);
@@ -47,9 +49,10 @@ private:
void drawVisibilityCone(QPainter &painter) const;
void drawGraticule(QPainter &painter) const;
void drawReceiver(QPainter &painter) const;
void drawSatellites(QPainter &painter) const;
int drawSatellites(QPainter &painter) const;
double satelliteElevation(const SatellitePosition &satellite) const;
void updateSatelliteAnimation();
void rebuildStaticLayer();
QVector<SatellitePosition> m_satellites;
QVector<SatellitePosition> m_satelliteMotionStart;
@@ -60,6 +63,12 @@ private:
mutable QImage m_landTexture;
mutable double m_landTextureYaw = 0.0;
mutable double m_landTexturePitch = 0.0;
QPixmap m_staticLayer;
QSize m_staticLayerSize;
qreal m_staticLayerDevicePixelRatio = 0.0;
double m_staticLayerYaw = 0.0;
double m_staticLayerPitch = 0.0;
double m_staticLayerZoom = 0.0;
QSet<QString> m_observedPrns;
QString m_status = QStringLiteral("Konum bekleniyor");
bool m_statusError = false;
+65 -9
View File
@@ -49,6 +49,10 @@ QString normalizeKey(QString key)
return "satellites";
}
if (key == "tracked") {
return "satellites";
}
if (key == "gdop" || key == "pdop" || key == "hdop" || key == "vdop" || key == "tdop") {
return key;
}
@@ -76,7 +80,7 @@ std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
}
static const QRegularExpression labeledValueRegex(
QStringLiteral(R"((^|[^A-Za-z0-9_])(?<key>lat(?:t?itude)?|long(?:itude)?|lon|lng|hheight|height|alt(?:itude)?|active[_ -]?sat(?:ellite)?s?|num(?:ber)?[_ -]?sat(?:ellite)?s?|sat(?:ellite)?s?[_ -]?active|sat(?:ellite)?s?|sats|uydu(?:lar)?|[gphvt][_ -]?dop)(?:\s*(?:=|:|->)?\s*)(?<value>[+-]?(?:\d+(?:[\.,]\d+)?|[\.,]\d+)(?:[eE][+-]?\d+)?))"),
QStringLiteral(R"((^|[^A-Za-z0-9_])(?<key>lat(?:t?itude)?|long(?:itude)?|lon|lng|hheight|height|alt(?:itude)?|active[_ -]?sat(?:ellite)?s?|num(?:ber)?[_ -]?sat(?:ellite)?s?|sat(?:ellite)?s?[_ -]?active|sat(?:ellite)?s?|sats|tracked|uydu(?:lar)?|[gphvt][_ -]?dop)(?:\s*(?:=|:|->)?\s*)(?<value>[+-]?(?:\d+(?:[\.,]\d+)?|[\.,]\d+)(?:[eE][+-]?\d+)?))"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression satelliteCountPhraseRegex(
QStringLiteral(R"((^|[^A-Za-z0-9_\.])(?<value>\d+)\s+(?:active\s+)?(?:sat(?:ellite)?s?|uydu(?:lar)?)(?=$|[\s,.;)\]]))"),
@@ -84,6 +88,44 @@ 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);
const QRegularExpressionMatch sourceMatch = sourceRegex.match(trimmed);
if (sourceMatch.hasMatch()) {
const QString source = sourceMatch.captured(QStringLiteral("source"));
if (source.startsWith(QStringLiteral("imu"), Qt::CaseInsensitive)) {
telemetry.source = LocationSource::ImuCorrected;
} else if (source.startsWith(QStringLiteral("trajectory"), Qt::CaseInsensitive)
|| source.startsWith(QStringLiteral("ground"), Qt::CaseInsensitive)) {
telemetry.source = LocationSource::Trajectory;
} else {
telemetry.source = LocationSource::Gps;
}
}
static const QRegularExpression fusionModeRegex(
QStringLiteral(R"((?:^|\s)mode\s*=\s*(?<mode>gps[_-]?aided|dead[_-]?reckoning)(?=$|\s))"),
QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch fusionModeMatch = fusionModeRegex.match(trimmed);
if (fusionModeMatch.hasMatch()) {
telemetry.fusionMode = fusionModeMatch.captured(QStringLiteral("mode")).startsWith(
QStringLiteral("dead"), Qt::CaseInsensitive)
? FusionMode::DeadReckoning : FusionMode::GpsAided;
}
static const QRegularExpression gpsAgeRegex(
QStringLiteral(R"((?:^|\s)gps[_-]?age[_-]?ms\s*=\s*(?<age>\d+)(?=$|\s))"),
QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch gpsAgeMatch = gpsAgeRegex.match(trimmed);
if (gpsAgeMatch.hasMatch()) {
bool ok = false;
const qint64 ageMs = gpsAgeMatch.captured(QStringLiteral("age")).toLongLong(&ok);
if (ok) {
telemetry.gpsAgeMs = ageMs;
}
}
bool hadCoordinateField = false;
bool hadLatitudeField = false;
bool hadLongitudeField = false;
@@ -195,10 +237,11 @@ QSet<QString> LocationParser::parseGpsPrns(const QString &line)
QSet<QString> result;
const auto addPrn = [&result](QString value, bool zeroBased = false) {
value = value.trimmed().toUpper();
if (value.startsWith(QLatin1Char('G'))) value.remove(0, 1);
const bool explicitGpsPrn = value.startsWith(QLatin1Char('G'));
if (explicitGpsPrn) value.remove(0, 1);
bool ok = false;
int number = value.toInt(&ok);
if (ok && zeroBased) ++number;
if (ok && zeroBased && !explicitGpsPrn) ++number;
if (ok && number >= 1 && number <= 63) {
result.insert(QStringLiteral("G%1").arg(number, 2, 10, QLatin1Char('0')));
}
@@ -210,18 +253,31 @@ QSet<QString> LocationParser::parseGpsPrns(const QString &line)
auto gpsMatches = gpsToken.globalMatch(line);
while (gpsMatches.hasNext()) addPrn(gpsMatches.next().captured(QStringLiteral("prn")));
static const QRegularExpression labeledList(
QStringLiteral(R"((?<label>\bPRNS?\b|\bSVS?\b|\bSVNS?\b|\bSVIDS?\b)\s*(?:=|:|->)?\s*[\[(]?\s*(?<list>(?:G?\d{1,2})(?:\s*[,;/| ]\s*G?\d{1,2})*)\s*[\])]?)"),
// Current firmware versions called their zero-based hardware channel values
// "prns" in complete tracking snapshots. Keep accepting those messages while
// newer firmware uses the unambiguous "prn_indices" label.
static const QRegularExpression legacyTrackingIndices(
QStringLiteral(R"(\btracked\s+satellites\s*=\s*\d+\s+prns?\s*=)"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression number(QStringLiteral(R"(G?(\d{1,2}))"),
const bool legacyZeroBasedPrns = legacyTrackingIndices.match(line).hasMatch();
static const QRegularExpression labeledList(
QStringLiteral(R"((?<label>\bPRN[_ -]?INDICES\b|\bPRNS?\b|\bSVS?\b|\bSVNS?\b|\bSVIDS?\b)\s*(?:=|:|->)?\s*[\[(]?\s*(?<list>(?:G?\d{1,2})(?:\s*[,;/| ]\s*G?\d{1,2})*)\s*[\])]?)"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression number(QStringLiteral(R"(G?\d{1,2})"),
QRegularExpression::CaseInsensitiveOption);
auto listMatches = labeledList.globalMatch(line);
while (listMatches.hasNext()) {
const QRegularExpressionMatch listMatch = listMatches.next();
const bool zeroBased = listMatch.captured(QStringLiteral("label")).startsWith(
QStringLiteral("SVN"), Qt::CaseInsensitive);
QString label = listMatch.captured(QStringLiteral("label")).toUpper();
label.remove(QLatin1Char('_'));
label.remove(QLatin1Char('-'));
label.remove(QLatin1Char(' '));
const bool zeroBased = label.startsWith(QStringLiteral("SVN"))
|| label == QStringLiteral("PRNINDICES")
|| (legacyZeroBasedPrns && label.startsWith(QStringLiteral("PRN")));
auto values = number.globalMatch(listMatch.captured(QStringLiteral("list")));
while (values.hasNext()) addPrn(values.next().captured(1), zeroBased);
while (values.hasNext()) addPrn(values.next().captured(0), zeroBased);
}
return result;
}
+513 -32
View File
@@ -5,26 +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>
@@ -155,6 +164,17 @@ LocationAnalysis analyzeLocations(const QVector<TelemetryPoint> &points)
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)) {
@@ -199,6 +219,45 @@ QString optionalDopText(const std::optional<double> &value)
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()) {
@@ -255,7 +314,7 @@ public:
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
return parent.isValid() ? 0 : 6;
return parent.isValid() ? 0 : 8;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
@@ -275,15 +334,29 @@ public:
case 0:
return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz"));
case 1:
return coordinateText(point.latitude, point.latitudeText);
if (point.source == LocationSource::ImuCorrected
&& point.fusionMode == FusionMode::DeadReckoning) {
return QStringLiteral("IMU corrected (DR)");
}
if (point.source == LocationSource::ImuCorrected
&& point.fusionMode == FusionMode::GpsAided) {
return QStringLiteral("IMU corrected (GPS)");
}
return locationSourceText(point.source);
case 2:
return coordinateText(point.longitude, point.longitudeText);
return coordinateText(point.latitude, point.latitudeText);
case 3:
return optionalDoubleText(point.hheight, 2);
return coordinateText(point.longitude, point.longitudeText);
case 4:
return optionalIntText(point.satellites);
return optionalDoubleText(point.hheight, 2);
case 5:
return optionalIntText(point.satellites);
case 6:
return optionalDopText(point.dop.pdop);
case 7:
return point.gpsAgeMs.has_value()
? QStringLiteral("%1 ms").arg(*point.gpsAgeMs)
: QStringLiteral("-");
default:
return {};
}
@@ -296,8 +369,9 @@ public:
return {};
}
static const QStringList headers = {
QStringLiteral("Saat"), QStringLiteral("Lat"), QStringLiteral("Long"),
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP")};
QStringLiteral("Saat"), QStringLiteral("Kaynak"), QStringLiteral("Lat"), QStringLiteral("Long"),
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP"),
QStringLiteral("GPS yas")};
return section >= 0 && section < headers.size() ? headers[section] : QVariant();
}
@@ -308,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();
@@ -323,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,
@@ -332,6 +441,14 @@ MainWindow::MainWindow(QWidget *parent)
m_globe->setRinexStatus(status, error);
statusBar()->showMessage(status, error ? 8000 : 4000);
});
connect(m_rinex, &RinexNavigation::loadingChanged, this, [this](bool loading) {
m_refreshRinexButton->setEnabled(!loading);
m_refreshRinexButton->setText(
loading ? QStringLiteral("RINEX indiriliyor...")
: QStringLiteral("RINEX Guncelle"));
});
connect(m_refreshRinexButton, &QPushButton::clicked,
m_rinex, &RinexNavigation::refresh);
m_rinex->refresh();
auto *rinexRefreshTimer = new QTimer(this);
rinexRefreshTimer->setInterval(15 * 60 * 1000);
@@ -342,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();
}
@@ -353,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) {
@@ -366,20 +613,193 @@ void MainWindow::clearPoints()
resetTelemetryState();
}
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());
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::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();
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)
{
appendUartLogLine(line);
static const QRegularExpression solveStartRegex(
QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression trackingSnapshotRegex(
QStringLiteral(
R"(\btracked\s+satellites\s*=\s*\d+\s+(?:prn[_ -]?indices|prns?|svns?)\s*=\s*\[[^\]]*\])"),
QRegularExpression::CaseInsensitiveOption);
const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch();
const bool trackingSnapshot = trackingSnapshotRegex.match(line).hasMatch();
if (solveStarted) {
m_observedPrns.clear();
}
const QSet<QString> linePrns = LocationParser::parseGpsPrns(line);
if (!linePrns.isEmpty()) {
if (trackingSnapshot) {
// This line is a complete point-in-time channel snapshot. Replacing
// prevents satellites from older solves remaining green indefinitely.
m_observedPrns = linePrns;
} else if (!linePrns.isEmpty()) {
m_observedPrns.unite(linePrns);
}
if (solveStarted || !linePrns.isEmpty()) {
if (solveStarted || trackingSnapshot || !linePrns.isEmpty()) {
m_globe->setObservedPrns(m_observedPrns);
}
@@ -435,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);
@@ -668,25 +1089,22 @@ void MainWindow::buildUi()
m_portCombo = new QComboBox(this);
m_portCombo->setObjectName(QStringLiteral("portSelector"));
m_portCombo->setMinimumWidth(165);
m_portCombo->setEditable(false);
QDir deviceDirectory(QStringLiteral("/dev"));
const QStringList deviceNames = deviceDirectory.entryList(
{QStringLiteral("ttyUSB*")},
QDir::System | QDir::Files | QDir::Readable, QDir::Name);
for (const QString &deviceName : deviceNames) {
m_portCombo->addItem(QStringLiteral("/dev/") + deviceName);
}
if (m_portCombo->findText(QStringLiteral("/dev/ttyUSB2")) < 0) {
m_portCombo->addItem(QStringLiteral("/dev/ttyUSB2"));
}
m_portCombo->setCurrentText(QStringLiteral("/dev/ttyUSB2"));
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_baudCombo->setCurrentText(QStringLiteral("460800"));
m_maxPointsSpin = new QSpinBox(this);
m_maxPointsSpin->setObjectName(QStringLiteral("maxPointsInput"));
@@ -698,6 +1116,14 @@ void MainWindow::buildUi()
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_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);
@@ -711,11 +1137,14 @@ void MainWindow::buildUi()
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_replayButton);
toolbarLayout->addWidget(m_clearButton);
toolbarLayout->addWidget(m_autoFollowCheck);
toolbarLayout->addWidget(zoomOutButton);
@@ -728,7 +1157,25 @@ void MainWindow::buildUi()
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 *globeTab = new QWidget(viewTabs);
auto *globeTabLayout = new QVBoxLayout(globeTab);
globeTabLayout->setContentsMargins(0, 0, 0, 0);
globeTabLayout->setSpacing(6);
auto *rinexToolbar = new QWidget(globeTab);
auto *rinexToolbarLayout = new QHBoxLayout(rinexToolbar);
rinexToolbarLayout->setContentsMargins(10, 6, 10, 0);
rinexToolbarLayout->addWidget(new QLabel(
QStringLiteral("GPS yayin efemerisi · otomatik: 15 dk"), rinexToolbar));
rinexToolbarLayout->addStretch(1);
m_refreshRinexButton = new QPushButton(QStringLiteral("RINEX Guncelle"), rinexToolbar);
m_refreshRinexButton->setObjectName(QStringLiteral("refreshRinexButton"));
m_refreshRinexButton->setToolTip(
QStringLiteral("Guncel RINEX navigasyon dosyasini simdi indir"));
rinexToolbarLayout->addWidget(m_refreshRinexButton);
globeTabLayout->addWidget(rinexToolbar);
globeTabLayout->addWidget(m_globe, 1);
viewTabs->addTab(globeTab, QStringLiteral("3B Dunya · Uydular"));
auto *sidePanel = new QWidget(this);
sidePanel->setMinimumWidth(390);
@@ -932,6 +1379,9 @@ 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);
connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow);
@@ -965,15 +1415,25 @@ 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)
{
TelemetryPoint point;
point.source = telemetry.source;
point.fusionMode = telemetry.fusionMode;
point.gpsAgeMs = telemetry.gpsAgeMs;
point.latitude = *telemetry.latitude;
point.longitude = *telemetry.longitude;
point.latitudeText = telemetry.latitudeText;
@@ -985,8 +1445,21 @@ void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
point.rawLine = telemetry.rawLine;
point.receivedAt = QDateTime::currentDateTimeUtc();
constexpr qint64 CorrectedPreferenceWindowMs = 1000;
bool correctedPositionFresh = false;
for (auto existing = m_points.crbegin(); existing != m_points.crend(); ++existing) {
if (existing->source == LocationSource::ImuCorrected) {
correctedPositionFresh =
existing->receivedAt.msecsTo(point.receivedAt)
<= CorrectedPreferenceWindowMs;
break;
}
}
m_points.push_back(point);
m_globe->setReceiverLocation(point.latitude, point.longitude);
if (point.source == LocationSource::ImuCorrected || !correctedPositionFresh) {
m_globe->setReceiverLocation(point.latitude, point.longitude);
}
if (m_locationTimeoutTimer != nullptr && m_serial->isOpen()) {
m_locationTimeoutTimer->start();
}
@@ -1028,7 +1501,8 @@ void MainWindow::updateDopLabels()
void MainWindow::updateAnalysis()
{
const LocationAnalysis analysis = analyzeLocations(m_points);
const QVector<TelemetryPoint> analysisPoints = preferredAnalysisPoints(m_points);
const LocationAnalysis analysis = analyzeLocations(analysisPoints);
if (!analysis.valid) {
m_cepLabel->setText(QStringLiteral("-"));
m_meanLocationLabel->setText(QStringLiteral("-"));
@@ -1051,11 +1525,11 @@ void MainWindow::updateAnalysis()
m_map->setAverageMarker(analysis.averageLat, analysis.averageLon,
analysis.rmsScatterMeters);
const double cep50 = calculateCep50(m_points, analysis.averageLat,
const double cep50 = calculateCep50(analysisPoints, analysis.averageLat,
analysis.averageLon);
m_cepLabel->setText(QStringLiteral("%1 (P50, N=%2)")
.arg(metersText(cep50))
.arg(m_points.size()));
.arg(analysisPoints.size()));
m_map->setCepMarker(analysis.averageLat, analysis.averageLon, cep50);
}
@@ -1077,7 +1551,14 @@ void MainWindow::mergePendingTelemetry(const ParsedTelemetry &telemetry)
commitPendingTelemetry();
}
if (telemetry.fusionMode != FusionMode::Unknown) {
m_pendingTelemetry.fusionMode = telemetry.fusionMode;
}
if (telemetry.gpsAgeMs.has_value()) {
m_pendingTelemetry.gpsAgeMs = telemetry.gpsAgeMs;
}
if (telemetry.latitude.has_value()) {
m_pendingTelemetry.source = telemetry.source;
m_pendingTelemetry.latitude = telemetry.latitude;
m_pendingTelemetry.latitudeText = telemetry.latitudeText;
}
@@ -1157,5 +1638,5 @@ int MainWindow::selectedBaudRate() const
{
bool ok = false;
const int baudRate = m_baudCombo->currentText().toInt(&ok);
return ok ? baudRate : 115200;
return ok ? baudRate : 460800;
}
+22
View File
@@ -18,18 +18,27 @@ 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
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();
@@ -41,6 +50,12 @@ private slots:
void trimToMaxPoints();
void updateReferenceLocation();
void flushUiUpdates();
void openTelemetryLog();
void openUartReplay();
void stopUartReplay();
void refreshSerialPorts();
bool startUartLogging(const QString &port, int baudRate);
void stopUartLogging();
private:
void buildUi();
@@ -57,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;
@@ -70,6 +87,9 @@ private:
QLineEdit *m_referenceLonEdit = nullptr;
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;
QCheckBox *m_showRecentPointsCheck = nullptr;
@@ -93,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;
+131 -27
View File
@@ -52,6 +52,20 @@ double metersPerPixel(double latitude, int zoom)
return latitudeScale * 2.0 * Pi * WebMercatorRadiusMeters / scale;
}
int preferredCurrentPointIndex(const QVector<TelemetryPoint> &points)
{
constexpr qint64 CorrectedPreferenceWindowMs = 1000;
const int newestIndex = points.size() - 1;
for (int index = points.size() - 1; index >= 0; --index) {
if (points[index].source == LocationSource::ImuCorrected) {
const qint64 ageMs = points[index].receivedAt.msecsTo(
points[newestIndex].receivedAt);
return ageMs <= CorrectedPreferenceWindowMs ? index : newestIndex;
}
}
return newestIndex;
}
} // namespace
MapWidget::MapWidget(QWidget *parent)
@@ -90,9 +104,9 @@ void MapWidget::setPoints(const QVector<TelemetryPoint> &points)
m_points = points;
if (!m_points.isEmpty() && m_autoFollow) {
const TelemetryPoint &last = m_points.last();
m_centerLat = last.latitude;
m_centerLon = last.longitude;
const TelemetryPoint &current = m_points[preferredCurrentPointIndex(m_points)];
m_centerLat = current.latitude;
m_centerLon = current.longitude;
if (wasEmpty) {
m_zoom = MaxZoom;
}
@@ -108,6 +122,41 @@ void MapWidget::clearPoints()
update();
}
void MapWidget::fitToPoints()
{
if (m_points.isEmpty()) {
return;
}
double minimumLatitude = m_points.first().latitude;
double maximumLatitude = minimumLatitude;
double minimumLongitude = m_points.first().longitude;
double maximumLongitude = minimumLongitude;
for (const TelemetryPoint &point : m_points) {
minimumLatitude = std::min(minimumLatitude, point.latitude);
maximumLatitude = std::max(maximumLatitude, point.latitude);
minimumLongitude = std::min(minimumLongitude, point.longitude);
maximumLongitude = std::max(maximumLongitude, point.longitude);
}
m_centerLat = 0.5 * (minimumLatitude + maximumLatitude);
m_centerLon = 0.5 * (minimumLongitude + maximumLongitude);
const double availableWidth = std::max(100.0, width() * 0.82);
const double availableHeight = std::max(100.0, height() * 0.82);
for (int zoom = MaxZoom; zoom >= MinZoom; --zoom) {
const QPointF topLeft = coordinateToWorld(maximumLatitude, minimumLongitude, zoom);
const QPointF bottomRight = coordinateToWorld(minimumLatitude, maximumLongitude, zoom);
if (std::fabs(bottomRight.x() - topLeft.x()) <= availableWidth
&& std::fabs(bottomRight.y() - topLeft.y()) <= availableHeight) {
m_zoom = zoom;
break;
}
}
if (m_autoFollow) {
m_autoFollow = false;
emit autoFollowChanged(false);
}
update();
}
void MapWidget::setAverageMarker(double latitude, double longitude, double radiusMeters)
{
m_hasAverageMarker = true;
@@ -156,6 +205,11 @@ void MapWidget::clearCepMarker()
update();
}
QPointF MapWidget::centerCoordinate() const
{
return QPointF(m_centerLat, m_centerLon);
}
void MapWidget::setAutoFollow(bool enabled)
{
if (m_autoFollow == enabled) {
@@ -164,9 +218,9 @@ void MapWidget::setAutoFollow(bool enabled)
m_autoFollow = enabled;
if (m_autoFollow && !m_points.isEmpty()) {
const TelemetryPoint &last = m_points.last();
m_centerLat = last.latitude;
m_centerLon = last.longitude;
const TelemetryPoint &current = m_points[preferredCurrentPointIndex(m_points)];
m_centerLat = current.latitude;
m_centerLon = current.longitude;
update();
}
emit autoFollowChanged(m_autoFollow);
@@ -216,6 +270,7 @@ void MapWidget::paintEvent(QPaintEvent *)
painter.fillRect(rect(), QColor(219, 225, 220));
drawTiles(painter);
drawTelemetry(painter);
drawLegend(painter);
if (m_points.isEmpty()) {
drawEmptyState(painter);
@@ -387,36 +442,53 @@ void MapWidget::drawTelemetry(QPainter &painter)
drawCepOverlay(painter);
QPainterPath path;
for (int i = 0; i < m_points.size(); ++i) {
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
if (i == 0) {
path.moveTo(screen);
} else {
path.lineTo(screen);
const auto drawSourcePath = [this, &painter](LocationSource source, const QColor &color) {
QPainterPath path;
bool havePoint = false;
int pointCount = 0;
for (const TelemetryPoint &point : m_points) {
if (point.source != source) {
continue;
}
const QPointF screen = coordinateToScreen(point.latitude, point.longitude);
havePoint ? path.lineTo(screen) : path.moveTo(screen);
havePoint = true;
++pointCount;
}
}
if (m_showRecentPoints && m_points.size() > 1) {
QPen pathPen(QColor(210, 105, 30), 3.0);
pathPen.setCapStyle(Qt::RoundCap);
pathPen.setJoinStyle(Qt::RoundJoin);
painter.setPen(pathPen);
painter.drawPath(path);
}
if (m_showRecentPoints && pointCount > 1) {
QPen pathPen(color, 3.0);
pathPen.setCapStyle(Qt::RoundCap);
pathPen.setJoinStyle(Qt::RoundJoin);
painter.setPen(pathPen);
painter.drawPath(path);
}
};
drawSourcePath(LocationSource::Trajectory, QColor(22, 163, 74));
drawSourcePath(LocationSource::Generic, QColor(210, 105, 30));
drawSourcePath(LocationSource::Gps, QColor(37, 99, 235));
drawSourcePath(LocationSource::ImuCorrected, QColor(220, 38, 38));
drawAverageOverlay(painter);
drawCepMarker(painter);
const bool drawNumbers = m_points.size() <= 150;
const int firstPoint = m_showRecentPoints ? 0 : m_points.size() - 1;
for (int i = firstPoint; i < m_points.size(); ++i) {
const bool isLast = i == m_points.size() - 1;
const int currentPointIndex = preferredCurrentPointIndex(m_points);
const int firstPoint = m_showRecentPoints ? 0 : currentPointIndex;
const int endPoint = m_showRecentPoints ? m_points.size() : currentPointIndex + 1;
for (int i = firstPoint; i < endPoint; ++i) {
const bool isLast = i == currentPointIndex;
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
const double radius = isLast ? 8.0 : 5.0;
const QColor pointColor = m_points[i].source == LocationSource::ImuCorrected
? QColor(220, 38, 38)
: (m_points[i].source == LocationSource::Gps
? QColor(37, 99, 235)
: (m_points[i].source == LocationSource::Trajectory
? QColor(22, 163, 74)
: (isLast ? QColor(210, 105, 30) : QColor(234, 88, 12))));
painter.setPen(QPen(Qt::white, isLast ? 3.0 : 2.0));
painter.setBrush(isLast ? QColor(31, 150, 82) : QColor(37, 99, 235));
painter.setBrush(pointColor);
painter.drawEllipse(screen, radius, radius);
if (drawNumbers) {
@@ -427,7 +499,7 @@ void MapWidget::drawTelemetry(QPainter &painter)
}
}
const TelemetryPoint &last = m_points.last();
const TelemetryPoint &last = m_points[currentPointIndex];
const QPointF lastScreen = coordinateToScreen(last.latitude, last.longitude);
QStringList lines;
const QString latitudeText = last.latitudeText.isEmpty()
@@ -436,6 +508,7 @@ void MapWidget::drawTelemetry(QPainter &painter)
? QString::number(last.longitude, 'f', 15) : last.longitudeText;
lines << QStringLiteral("Lat: %1").arg(latitudeText);
lines << QStringLiteral("Long: %1").arg(longitudeText);
lines << QStringLiteral("Kaynak: %1").arg(locationSourceText(last.source));
if (last.hheight.has_value()) {
lines << QStringLiteral("HHeight: %1").arg(*last.hheight, 0, 'f', 2);
}
@@ -466,6 +539,37 @@ void MapWidget::drawTelemetry(QPainter &painter)
painter.drawText(bubble.adjusted(9, 7, -9, -7), Qt::AlignLeft | Qt::AlignVCenter, label);
}
void MapWidget::drawLegend(QPainter &painter)
{
if (m_points.isEmpty() || !m_showTelemetryOverlay) {
return;
}
const struct {
const char *text;
QColor color;
} entries[] = {
{"Trajectory (truth)", QColor(22, 163, 74)},
{"GPS (4 satellite)", QColor(37, 99, 235)},
{"IMU + ESKF corrected", QColor(220, 38, 38)}
};
const QRectF box(12.0, 12.0, 190.0, 76.0);
painter.setPen(QPen(QColor(50, 58, 65, 180), 1.0));
painter.setBrush(QColor(255, 255, 255, 232));
painter.drawRoundedRect(box, 6.0, 6.0);
painter.setFont(QFont(painter.font().family(), 9, QFont::DemiBold));
for (int index = 0; index < 3; ++index) {
const double y = 27.0 + index * 22.0;
painter.setPen(QPen(entries[index].color, 3.0));
painter.drawLine(QPointF(23.0, y), QPointF(43.0, y));
painter.setBrush(entries[index].color);
painter.drawEllipse(QPointF(33.0, y), 4.0, 4.0);
painter.setPen(QColor(25, 32, 38));
painter.drawText(QRectF(50.0, y - 9.0, 142.0, 18.0),
Qt::AlignLeft | Qt::AlignVCenter,
QString::fromLatin1(entries[index].text));
}
}
void MapWidget::drawCepOverlay(QPainter &painter)
{
if (!m_hasCepMarker || !m_cepRadiusMeters.has_value() || *m_cepRadiusMeters <= 0.0) {
+3
View File
@@ -20,12 +20,14 @@ public:
explicit MapWidget(QWidget *parent = nullptr);
void setPoints(const QVector<TelemetryPoint> &points);
void fitToPoints();
void clearPoints();
void setAverageMarker(double latitude, double longitude, double radiusMeters);
void clearAverageMarker();
void setCepMarker(double latitude, double longitude,
const std::optional<double> &radiusMeters);
void clearCepMarker();
[[nodiscard]] QPointF centerCoordinate() const;
public slots:
void setAutoFollow(bool enabled);
@@ -56,6 +58,7 @@ private:
void drawTiles(QPainter &painter);
void drawTelemetry(QPainter &painter);
void drawLegend(QPainter &painter);
void drawAverageOverlay(QPainter &painter);
void drawCepOverlay(QPainter &painter);
void drawCepMarker(QPainter &painter);
+135 -100
View File
@@ -2,6 +2,7 @@
#include <QDir>
#include <QFile>
#include <QHash>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QStandardPaths>
@@ -12,6 +13,7 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <utility>
namespace {
@@ -22,29 +24,6 @@ constexpr double HalfGpsWeek = GpsWeekSeconds / 2.0;
constexpr double CurrentGpsUtcOffsetSeconds = 18.0;
constexpr int PositionUpdateIntervalMs = 60000;
struct Ephemeris {
QString prn;
QDateTime toc;
double crs = 0.0;
double deltaN = 0.0;
double m0 = 0.0;
double cuc = 0.0;
double eccentricity = 0.0;
double cus = 0.0;
double sqrtA = 0.0;
double toe = 0.0;
double cic = 0.0;
double omega0 = 0.0;
double cis = 0.0;
double i0 = 0.0;
double crc = 0.0;
double omega = 0.0;
double omegaDot = 0.0;
double idot = 0.0;
int gpsWeek = 0;
bool healthy = true;
};
double parseField(const QString &line, int field)
{
QString text = line.mid(4 + field * 19, 19).trimmed();
@@ -93,49 +72,6 @@ QDateTime gpsEpoch()
return QDateTime(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC);
}
SatellitePosition propagate(const Ephemeris &eph, const QDateTime &whenUtc)
{
const double gpsSeconds = gpsEpoch().msecsTo(whenUtc) / 1000.0
+ CurrentGpsUtcOffsetSeconds;
const double absoluteToe = eph.gpsWeek * GpsWeekSeconds + eph.toe;
double tk = gpsSeconds - absoluteToe;
while (tk > HalfGpsWeek) tk -= GpsWeekSeconds;
while (tk < -HalfGpsWeek) tk += GpsWeekSeconds;
const double a = eph.sqrtA * eph.sqrtA;
const double meanMotion = std::sqrt(GpsMu / (a * a * a)) + eph.deltaN;
const double meanAnomaly = eph.m0 + meanMotion * tk;
double eccentricAnomaly = meanAnomaly;
for (int iteration = 0; iteration < 12; ++iteration) {
eccentricAnomaly = meanAnomaly + eph.eccentricity * std::sin(eccentricAnomaly);
}
const double trueAnomaly = std::atan2(
std::sqrt(1.0 - eph.eccentricity * eph.eccentricity) * std::sin(eccentricAnomaly),
std::cos(eccentricAnomaly) - eph.eccentricity);
const double phi = trueAnomaly + eph.omega;
const double twoPhi = 2.0 * phi;
const double u = phi + eph.cus * std::sin(twoPhi) + eph.cuc * std::cos(twoPhi);
const double radius = a * (1.0 - eph.eccentricity * std::cos(eccentricAnomaly))
+ eph.crs * std::sin(twoPhi) + eph.crc * std::cos(twoPhi);
const double inclination = eph.i0 + eph.idot * tk
+ eph.cis * std::sin(twoPhi) + eph.cic * std::cos(twoPhi);
const double orbitalX = radius * std::cos(u);
const double orbitalY = radius * std::sin(u);
const double ascendingNode = eph.omega0 + (eph.omegaDot - EarthRotation) * tk
- EarthRotation * eph.toe;
SatellitePosition result;
result.prn = eph.prn;
result.x = orbitalX * std::cos(ascendingNode)
- orbitalY * std::cos(inclination) * std::sin(ascendingNode);
result.y = orbitalX * std::sin(ascendingNode)
+ orbitalY * std::cos(inclination) * std::cos(ascendingNode);
result.z = orbitalY * std::sin(inclination);
result.healthy = eph.healthy;
return result;
}
} // namespace
RinexNavigation::RinexNavigation(QObject *parent)
@@ -150,10 +86,10 @@ RinexNavigation::RinexNavigation(QObject *parent)
void RinexNavigation::refresh()
{
if (m_loading) return;
if (m_latestRinex.isEmpty()) {
if (m_ephemerides.isEmpty()) {
loadCache();
}
m_loading = true;
setLoading(true);
emit statusChanged(QStringLiteral("Guncel RINEX efemerisi indiriliyor..."), false);
requestDay(QDateTime::currentDateTimeUtc().date(), 0);
}
@@ -181,8 +117,13 @@ void RinexNavigation::requestDay(const QDate &date, int fallbackIndex)
if (fallbackIndex < 2) {
requestDay(date.addDays(-1), fallbackIndex + 1);
} else {
m_loading = false;
if (!loadCache()) {
setLoading(false);
if (!m_ephemerides.isEmpty()) {
publishCurrentMotion();
emit statusChanged(
QStringLiteral("RINEX indirilemedi; mevcut efemeris kullaniliyor"),
true);
} else if (!loadCache()) {
emit statusChanged(QStringLiteral("RINEX indirilemedi; internet baglantisini kontrol edin"), true);
}
}
@@ -197,14 +138,16 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &
{
const QByteArray rinex = gunzip(compressed);
QString error;
const QVector<SatellitePosition> positions = parseGpsNavigation(
rinex, QDateTime::currentDateTimeUtc(), &error);
QVector<Ephemeris> ephemerides = parseEphemerides(rinex, &error);
const QDateTime now = QDateTime::currentDateTimeUtc();
const QVector<SatellitePosition> positions = propagateEphemerides(
selectEphemerides(ephemerides, now), now);
if (positions.isEmpty()) {
if (fallbackIndex < 2) {
requestDay(date.addDays(-1), fallbackIndex + 1);
return;
}
m_loading = false;
setLoading(false);
emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true);
return;
}
@@ -212,11 +155,14 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &
QFile cache(cachePath());
QDir().mkpath(QFileInfo(cache).absolutePath());
if (cache.open(QIODevice::WriteOnly)) cache.write(rinex);
m_latestRinex = rinex;
m_loading = false;
m_ephemerides = std::move(ephemerides);
setLoading(false);
publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 (UTC)")
.arg(positions.size()).arg(date.toString(QStringLiteral("dd.MM.yyyy"))), false);
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 · %3")
.arg(positions.size())
.arg(date.toString(QStringLiteral("dd.MM.yyyy")))
.arg(now.toLocalTime().toString(
QStringLiteral("HH:mm:ss t"))), false);
}
bool RinexNavigation::loadCache()
@@ -224,12 +170,17 @@ bool RinexNavigation::loadCache()
QFile cache(cachePath());
if (!cache.open(QIODevice::ReadOnly)) return false;
QString error;
m_latestRinex = cache.readAll();
const QVector<SatellitePosition> positions = parseGpsNavigation(
m_latestRinex, QDateTime::currentDateTimeUtc(), &error);
QVector<Ephemeris> ephemerides = parseEphemerides(cache.readAll(), &error);
const QDateTime now = QDateTime::currentDateTimeUtc();
const QVector<SatellitePosition> positions = propagateEphemerides(
selectEphemerides(ephemerides, now), now);
if (positions.isEmpty()) return false;
m_ephemerides = std::move(ephemerides);
publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX").arg(positions.size()), true);
emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX · %2")
.arg(positions.size())
.arg(now.toLocalTime().toString(
QStringLiteral("HH:mm:ss t"))), true);
return true;
}
@@ -241,28 +192,36 @@ QString RinexNavigation::cachePath() const
void RinexNavigation::updateCurrentPositions()
{
if (m_latestRinex.isEmpty()) return;
if (m_ephemerides.isEmpty()) return;
publishCurrentMotion();
}
void RinexNavigation::publishCurrentMotion()
{
if (m_latestRinex.isEmpty()) return;
if (m_ephemerides.isEmpty()) return;
const QDateTime now = QDateTime::currentDateTimeUtc();
const QVector<SatellitePosition> current = parseGpsNavigation(m_latestRinex, now);
const QVector<Ephemeris> selected = selectEphemerides(m_ephemerides, now);
const QVector<SatellitePosition> current = propagateEphemerides(selected, now);
if (current.isEmpty()) return;
const QVector<SatellitePosition> next = parseGpsNavigation(
m_latestRinex, now.addMSecs(PositionUpdateIntervalMs));
const QVector<SatellitePosition> next = propagateEphemerides(
selected, now.addMSecs(PositionUpdateIntervalMs));
emit satellitesUpdated(current);
if (!next.isEmpty()) {
emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs);
}
}
QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
const QByteArray &rinex, const QDateTime &whenUtc, QString *error)
void RinexNavigation::setLoading(bool loading)
{
if (m_loading == loading) return;
m_loading = loading;
emit loadingChanged(loading);
}
QVector<RinexNavigation::Ephemeris> RinexNavigation::parseEphemerides(
const QByteArray &rinex, QString *error)
{
const QString text = QString::fromLatin1(rinex);
const QStringList lines = text.split('\n');
@@ -274,8 +233,8 @@ QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
}
++index;
QHash<QString, Ephemeris> closest;
QHash<QString, qint64> closestAge;
QVector<Ephemeris> result;
result.reserve(512);
while (index < lines.size()) {
const QString first = lines[index];
if (first.size() < 3) { ++index; continue; }
@@ -321,19 +280,95 @@ QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
eph.cus, eph.sqrtA, eph.toe, eph.cic, eph.omega0, eph.cis,
eph.i0, eph.crc, eph.omega, eph.omegaDot, eph.idot})
&& eph.sqrtA > 4000.0 && eph.gpsWeek > 0) {
const qint64 age = std::abs(toc.secsTo(whenUtc));
if (!closestAge.contains(eph.prn) || age < closestAge[eph.prn]) {
closest[eph.prn] = eph;
closestAge[eph.prn] = age;
}
result.push_back(eph);
}
index += 8;
}
QVector<SatellitePosition> result;
result.reserve(closest.size());
for (const Ephemeris &eph : closest) result.push_back(propagate(eph, whenUtc));
std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { return a.prn < b.prn; });
if (result.isEmpty() && error) *error = QStringLiteral("GPS efemerisi bulunamadi");
return result;
}
QVector<RinexNavigation::Ephemeris> RinexNavigation::selectEphemerides(
const QVector<Ephemeris> &ephemerides, const QDateTime &whenUtc)
{
QHash<QString, Ephemeris> closest;
QHash<QString, qint64> closestAge;
// GPS navigation epochs are labelled in GPS system time, not UTC.
const QDateTime whenGps = whenUtc.addSecs(
static_cast<qint64>(CurrentGpsUtcOffsetSeconds));
for (const Ephemeris &eph : ephemerides) {
const qint64 age = std::abs(eph.toc.msecsTo(whenGps));
if (!closestAge.contains(eph.prn) || age < closestAge[eph.prn]) {
closest[eph.prn] = eph;
closestAge[eph.prn] = age;
}
}
QVector<Ephemeris> result;
result.reserve(closest.size());
for (const Ephemeris &eph : closest) result.push_back(eph);
std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) {
return a.prn < b.prn;
});
return result;
}
QVector<SatellitePosition> RinexNavigation::propagateEphemerides(
const QVector<Ephemeris> &ephemerides, const QDateTime &whenUtc)
{
QVector<SatellitePosition> result;
result.reserve(ephemerides.size());
for (const Ephemeris &eph : ephemerides) result.push_back(propagate(eph, whenUtc));
std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { return a.prn < b.prn; });
return result;
}
SatellitePosition RinexNavigation::propagate(const Ephemeris &eph, const QDateTime &whenUtc)
{
const double gpsSeconds = gpsEpoch().msecsTo(whenUtc) / 1000.0
+ CurrentGpsUtcOffsetSeconds;
const double absoluteToe = eph.gpsWeek * GpsWeekSeconds + eph.toe;
double tk = gpsSeconds - absoluteToe;
while (tk > HalfGpsWeek) tk -= GpsWeekSeconds;
while (tk < -HalfGpsWeek) tk += GpsWeekSeconds;
const double a = eph.sqrtA * eph.sqrtA;
const double meanMotion = std::sqrt(GpsMu / (a * a * a)) + eph.deltaN;
const double meanAnomaly = eph.m0 + meanMotion * tk;
double eccentricAnomaly = meanAnomaly;
for (int iteration = 0; iteration < 12; ++iteration) {
eccentricAnomaly = meanAnomaly + eph.eccentricity * std::sin(eccentricAnomaly);
}
const double trueAnomaly = std::atan2(
std::sqrt(1.0 - eph.eccentricity * eph.eccentricity) * std::sin(eccentricAnomaly),
std::cos(eccentricAnomaly) - eph.eccentricity);
const double phi = trueAnomaly + eph.omega;
const double twoPhi = 2.0 * phi;
const double u = phi + eph.cus * std::sin(twoPhi) + eph.cuc * std::cos(twoPhi);
const double radius = a * (1.0 - eph.eccentricity * std::cos(eccentricAnomaly))
+ eph.crs * std::sin(twoPhi) + eph.crc * std::cos(twoPhi);
const double inclination = eph.i0 + eph.idot * tk
+ eph.cis * std::sin(twoPhi) + eph.cic * std::cos(twoPhi);
const double orbitalX = radius * std::cos(u);
const double orbitalY = radius * std::sin(u);
const double ascendingNode = eph.omega0 + (eph.omegaDot - EarthRotation) * tk
- EarthRotation * eph.toe;
SatellitePosition result;
result.prn = eph.prn;
result.x = orbitalX * std::cos(ascendingNode)
- orbitalY * std::cos(inclination) * std::sin(ascendingNode);
result.y = orbitalX * std::sin(ascendingNode)
+ orbitalY * std::cos(inclination) * std::cos(ascendingNode);
result.z = orbitalY * std::sin(inclination);
result.healthy = eph.healthy;
return result;
}
QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
const QByteArray &rinex, const QDateTime &whenUtc, QString *error)
{
const QVector<Ephemeris> ephemerides = parseEphemerides(rinex, error);
return propagateEphemerides(selectEphemerides(ephemerides, whenUtc), whenUtc);
}
+33 -1
View File
@@ -26,17 +26,49 @@ signals:
const QVector<SatellitePosition> &next,
int durationMs);
void statusChanged(const QString &status, bool error);
void loadingChanged(bool loading);
private:
struct Ephemeris {
QString prn;
QDateTime toc;
double crs = 0.0;
double deltaN = 0.0;
double m0 = 0.0;
double cuc = 0.0;
double eccentricity = 0.0;
double cus = 0.0;
double sqrtA = 0.0;
double toe = 0.0;
double cic = 0.0;
double omega0 = 0.0;
double cis = 0.0;
double i0 = 0.0;
double crc = 0.0;
double omega = 0.0;
double omegaDot = 0.0;
double idot = 0.0;
int gpsWeek = 0;
bool healthy = true;
};
void requestDay(const QDate &date, int fallbackIndex);
void handleDownload(const QByteArray &compressed, const QDate &date, int fallbackIndex);
bool loadCache();
QString cachePath() const;
void updateCurrentPositions();
void publishCurrentMotion();
void setLoading(bool loading);
static QVector<Ephemeris> parseEphemerides(
const QByteArray &rinex, QString *error = nullptr);
static QVector<Ephemeris> selectEphemerides(
const QVector<Ephemeris> &ephemerides, const QDateTime &whenUtc);
static QVector<SatellitePosition> propagateEphemerides(
const QVector<Ephemeris> &ephemerides, const QDateTime &whenUtc);
static SatellitePosition propagate(const Ephemeris &eph, const QDateTime &whenUtc);
QNetworkAccessManager m_network;
QTimer m_positionTimer;
QByteArray m_latestRinex;
QVector<Ephemeris> m_ephemerides;
bool m_loading = false;
};
+57 -21
View File
@@ -10,38 +10,70 @@
#include <termios.h>
#include <unistd.h>
#ifdef Q_OS_MACOS
#include <IOKit/serial/ioss.h>
#endif
namespace {
constexpr qsizetype MaxBytesPerReadCycle = 64 * 1024;
speed_t baudToSpeed(int baudRate)
bool applyBaudRate(termios &options, int baudRate)
{
speed_t speed = 0;
switch (baudRate) {
case 1200:
return B1200;
speed = B1200;
break;
case 2400:
return B2400;
speed = B2400;
break;
case 4800:
return B4800;
speed = B4800;
break;
case 9600:
return B9600;
speed = B9600;
break;
case 19200:
return B19200;
speed = B19200;
break;
case 38400:
return B38400;
speed = B38400;
break;
case 57600:
return B57600;
speed = B57600;
break;
case 115200:
return B115200;
speed = B115200;
break;
#ifdef B230400
case 230400:
return B230400;
speed = B230400;
break;
#endif
#ifdef B460800
case 460800:
return B460800;
speed = B460800;
break;
#endif
#ifdef B921600
case 921600:
return B921600;
speed = B921600;
break;
#endif
default:
return 0;
#ifdef Q_OS_MACOS
// IOSSIOSPEED below applies the exact requested rate. A valid termios
// placeholder is still required before that ioctl can be issued.
speed = B9600;
break;
#else
return false;
#endif
}
return ::cfsetispeed(&options, speed) == 0
&& ::cfsetospeed(&options, speed) == 0;
}
} // namespace
@@ -176,12 +208,6 @@ void SerialReader::flushPartialLine()
bool SerialReader::configurePort(int baudRate)
{
const speed_t speed = baudToSpeed(baudRate);
if (speed == 0) {
emit errorOccurred(QStringLiteral("Desteklenmeyen baud rate: %1").arg(baudRate));
return false;
}
termios options {};
if (::tcgetattr(m_fd, &options) != 0) {
emitSystemError(QStringLiteral("Port ayarlari okunamadi"));
@@ -189,8 +215,10 @@ bool SerialReader::configurePort(int baudRate)
}
::cfmakeraw(&options);
::cfsetispeed(&options, speed);
::cfsetospeed(&options, speed);
if (!applyBaudRate(options, baudRate)) {
emit errorOccurred(QStringLiteral("Bu sistemde desteklenmeyen baud rate: %1").arg(baudRate));
return false;
}
options.c_cflag |= CLOCAL | CREAD;
options.c_cflag &= ~HUPCL;
@@ -206,6 +234,14 @@ bool SerialReader::configurePort(int baudRate)
return false;
}
#ifdef Q_OS_MACOS
speed_t requestedSpeed = static_cast<speed_t>(baudRate);
if (::ioctl(m_fd, IOSSIOSPEED, &requestedSpeed) != 0) {
emitSystemError(QStringLiteral("macOS baud rate uygulanamadi"));
return false;
}
#endif
::tcflush(m_fd, TCIFLUSH);
return true;
}
+34
View File
@@ -5,6 +5,34 @@
#include <optional>
enum class LocationSource {
Generic,
Gps,
ImuCorrected,
Trajectory
};
enum class FusionMode {
Unknown,
GpsAided,
DeadReckoning
};
inline QString locationSourceText(LocationSource source)
{
switch (source) {
case LocationSource::Gps:
return QStringLiteral("GPS");
case LocationSource::ImuCorrected:
return QStringLiteral("IMU corrected");
case LocationSource::Trajectory:
return QStringLiteral("Trajectory (truth)");
case LocationSource::Generic:
default:
return QStringLiteral("Generic");
}
}
struct DopValues {
std::optional<double> gdop;
std::optional<double> pdop;
@@ -20,6 +48,9 @@ struct DopValues {
};
struct TelemetryPoint {
LocationSource source = LocationSource::Generic;
FusionMode fusionMode = FusionMode::Unknown;
std::optional<qint64> gpsAgeMs;
double latitude = 0.0;
double longitude = 0.0;
QString latitudeText;
@@ -32,6 +63,9 @@ struct TelemetryPoint {
};
struct ParsedTelemetry {
LocationSource source = LocationSource::Generic;
FusionMode fusionMode = FusionMode::Unknown;
std::optional<qint64> gpsAgeMs;
std::optional<double> latitude;
std::optional<double> longitude;
QString latitudeText;
+207
View File
@@ -0,0 +1,207 @@
#include "UartReplay.h"
#include "LocationParser.h"
#include <QFile>
#include <QRegularExpression>
#include <QTimer>
#include <algorithm>
#include <limits>
UartReplay::UartReplay(QObject *parent)
: QObject(parent)
, m_timer(new QTimer(this))
{
m_timer->setSingleShot(true);
connect(m_timer, &QTimer::timeout, this, &UartReplay::emitNext);
}
bool UartReplay::isRunning() const
{
return m_running;
}
int UartReplay::locationCount() const
{
return m_locationCount;
}
bool UartReplay::start(const QString &filePath, int gpsIntervalMs, int sensorIntervalMs)
{
stop();
if (gpsIntervalMs < 1 || sensorIntervalMs < 1) {
emit errorOccurred(QStringLiteral("Replay araliklari en az 1 ms olmalidir"));
return false;
}
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
emit errorOccurred(QStringLiteral("UART replay logu acilamadi: %1")
.arg(file.errorString()));
return false;
}
qint64 nextGpsMs = 0;
qint64 nextSensorMs = 0;
qint64 lastDueMs = 0;
bool pendingLatitude = false;
bool pendingLongitude = false;
LocationSource pendingSource = LocationSource::Generic;
const QList<QByteArray> rawLines = file.readAll().split('\n');
m_lines.reserve(rawLines.size());
for (const QByteArray &rawLine : rawLines) {
const QString line = removeRecorderTimestamp(QString::fromUtf8(rawLine));
if (line.isEmpty() || line.startsWith(QLatin1Char('#'))
|| line.startsWith(QStringLiteral("[SESSION START]"))
|| line.startsWith(QStringLiteral("[SESSION END]"))) {
continue;
}
Stream stream = classifyLine(line);
const std::optional<ParsedTelemetry> telemetry = LocationParser::parseLine(line);
if (telemetry.has_value() && telemetry->hasLocation()) {
pendingLatitude = false;
pendingLongitude = false;
pendingSource = LocationSource::Generic;
} else if (telemetry.has_value()) {
if (telemetry->latitude.has_value()) {
if (pendingLatitude) {
pendingLongitude = false;
}
pendingLatitude = true;
pendingSource = telemetry->source;
}
if (telemetry->longitude.has_value()) {
pendingLongitude = true;
if (telemetry->source != LocationSource::Generic) {
pendingSource = telemetry->source;
}
}
if (pendingLatitude && pendingLongitude) {
stream = pendingSource == LocationSource::ImuCorrected
|| pendingSource == LocationSource::Trajectory
? Stream::Sensor : Stream::Gps;
pendingLatitude = false;
pendingLongitude = false;
pendingSource = LocationSource::Generic;
}
}
qint64 dueMs = lastDueMs;
if (stream == Stream::Gps) {
dueMs = std::max(lastDueMs, nextGpsMs);
nextGpsMs = dueMs + gpsIntervalMs;
++m_locationCount;
} else if (stream == Stream::Sensor) {
dueMs = std::max(lastDueMs, nextSensorMs);
nextSensorMs = dueMs + sensorIntervalMs;
++m_locationCount;
}
ReplayLine replayLine;
replayLine.text = line;
replayLine.delayBeforeMs = static_cast<int>(std::min<qint64>(
dueMs - lastDueMs, std::numeric_limits<int>::max()));
m_lines.push_back(std::move(replayLine));
lastDueMs = dueMs;
}
if (m_locationCount == 0) {
m_lines.clear();
emit errorOccurred(QStringLiteral(
"UART replay logunda GPS veya fused konum kaydi bulunamadi"));
return false;
}
m_running = true;
emit replayStarted(m_lines.size(), m_locationCount);
m_timer->start(0);
return true;
}
void UartReplay::stop()
{
if (!m_running && m_lines.isEmpty()) {
return;
}
m_timer->stop();
m_running = false;
m_lines.clear();
m_index = 0;
m_locationCount = 0;
}
void UartReplay::emitNext()
{
if (!m_running) {
return;
}
constexpr int MaxImmediateLines = 250;
int emitted = 0;
while (m_index < m_lines.size() && emitted < MaxImmediateLines) {
const ReplayLine &line = m_lines.at(m_index);
if (line.delayBeforeMs > 0) {
if (emitted > 0) {
emit progressChanged(m_index, m_lines.size());
}
const int delay = line.delayBeforeMs;
m_lines[m_index].delayBeforeMs = 0;
m_timer->start(delay);
return;
}
emit lineReady(line.text);
++m_index;
++emitted;
}
emit progressChanged(m_index, m_lines.size());
if (m_index >= m_lines.size()) {
finish();
} else {
m_timer->start(0);
}
}
QString UartReplay::removeRecorderTimestamp(const QString &line)
{
QString result = line.trimmed();
static const QRegularExpression timestampPrefix(
QStringLiteral(R"(^\[\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[\.,]\d+)?\]\s*)"));
result.remove(timestampPrefix);
return result.trimmed();
}
UartReplay::Stream UartReplay::classifyLine(const QString &line)
{
const std::optional<ParsedTelemetry> telemetry = LocationParser::parseLine(line);
if (!telemetry.has_value() || !telemetry->hasLocation()) {
return Stream::Diagnostic;
}
switch (telemetry->source) {
case LocationSource::Gps:
return Stream::Gps;
case LocationSource::ImuCorrected:
case LocationSource::Trajectory:
return Stream::Sensor;
case LocationSource::Generic:
default:
// Older firmware used a [FUSION] record without an explicit source.
return line.startsWith(QStringLiteral("[FUSION]"), Qt::CaseInsensitive)
? Stream::Sensor : Stream::Gps;
}
}
void UartReplay::finish()
{
m_timer->stop();
m_running = false;
m_lines.clear();
m_index = 0;
m_locationCount = 0;
emit replayFinished();
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <QObject>
#include <QString>
#include <QVector>
class QTimer;
class UartReplay final : public QObject {
Q_OBJECT
public:
explicit UartReplay(QObject *parent = nullptr);
[[nodiscard]] bool isRunning() const;
[[nodiscard]] int locationCount() const;
public slots:
bool start(const QString &filePath, int gpsIntervalMs = 200,
int sensorIntervalMs = 50);
void stop();
signals:
void lineReady(const QString &line);
void replayStarted(int lineCount, int locationCount);
void progressChanged(int linesProcessed, int lineCount);
void replayFinished();
void errorOccurred(const QString &message);
private slots:
void emitNext();
private:
enum class Stream {
Diagnostic,
Gps,
Sensor
};
struct ReplayLine {
QString text;
int delayBeforeMs = 0;
};
static QString removeRecorderTimestamp(const QString &line);
static Stream classifyLine(const QString &line);
void finish();
QTimer *m_timer = nullptr;
QVector<ReplayLine> m_lines;
int m_index = 0;
int m_locationCount = 0;
bool m_running = false;
};
+4
View File
@@ -1,6 +1,7 @@
#include "MainWindow.h"
#include <QApplication>
#include <QFileInfo>
int main(int argc, char *argv[])
{
@@ -9,6 +10,9 @@ int main(int argc, char *argv[])
QApplication::setOrganizationName(QStringLiteral("rocket"));
MainWindow window;
if (argc == 3 && QString::fromLocal8Bit(argv[1]) == QStringLiteral("--replay")) {
window.loadTelemetryLog(QFileInfo(QString::fromLocal8Bit(argv[2])).absoluteFilePath());
}
window.show();
return QApplication::exec();
+112 -4
View File
@@ -1,17 +1,24 @@
#include "../src/MainWindow.h"
#include "../src/GlobeWidget.h"
#include "../src/MapWidget.h"
#include <QApplication>
#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>
@@ -36,7 +43,35 @@ void flushUpdates(MainWindow &window)
int main(int argc, char **argv)
{
QApplication app(argc, argv);
qputenv("LOCATION_PLOTTER_PORT", "/dev/test-usb-uart");
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"));
@@ -48,6 +83,8 @@ int main(int argc, char **argv)
auto *showRecentPoints = window.findChild<QCheckBox *>(QStringLiteral("showRecentPointsCheck"));
auto *showAverage = window.findChild<QCheckBox *>(QStringLiteral("showAverageCheck"));
auto *portSelector = window.findChild<QComboBox *>(QStringLiteral("portSelector"));
auto *refreshRinexButton =
window.findChild<QPushButton *>(QStringLiteral("refreshRinexButton"));
auto *map = window.findChild<MapWidget *>();
auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs"));
auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter"));
@@ -66,10 +103,14 @@ int main(int argc, char **argv)
assert(showRecentPoints != nullptr);
assert(showAverage != nullptr);
assert(portSelector != nullptr);
assert(!portSelector->isEditable());
assert(refreshRinexButton != nullptr);
assert(refreshRinexButton->text() == QStringLiteral("RINEX Guncelle")
|| refreshRinexButton->text() == QStringLiteral("RINEX indiriliyor..."));
assert(portSelector->isEditable());
assert(portSelector->count() >= 1);
assert(portSelector->currentText() == QStringLiteral("/dev/test-usb-uart"));
for (int i = 0; i < portSelector->count(); ++i) {
assert(portSelector->itemText(i).startsWith(QStringLiteral("/dev/ttyUSB")));
assert(portSelector->itemText(i).startsWith(QStringLiteral("/dev/")));
assert(!portSelector->itemText(i).trimmed().isEmpty());
}
assert(map != nullptr);
@@ -78,6 +119,15 @@ int main(int argc, char **argv)
assert(sideSplitter != nullptr);
assert(sideSplitter->orientation() == Qt::Vertical);
sendLine(window, QStringLiteral("[TRK] tracked satellites=2 prn_indices=[2,7]"));
assert(map != nullptr);
auto *globe = window.findChild<GlobeWidget *>();
assert(globe != nullptr);
assert(globe->observedPrns()
== QSet<QString>({QStringLiteral("G03"), QStringLiteral("G08")}));
sendLine(window, QStringLiteral("[TRK] tracked satellites=1 prn_indices=[13]"));
assert(globe->observedPrns() == QSet<QString>({QStringLiteral("G14")}));
assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection,
Q_ARG(double, 40.25), Q_ARG(double, 29.5)));
assert(referenceLatInput->text() == QStringLiteral("40.250000000000000"));
@@ -99,11 +149,48 @@ int main(int argc, char **argv)
sendLine(window, QStringLiteral("NAV solve done"));
flushUpdates(window);
assert(table->model()->rowCount() == 1);
assert(table->model()->data(table->model()->index(0, 1)).toString()
== QStringLiteral("40.78925034214206846173"));
assert(table->model()->data(table->model()->index(0, 2)).toString()
== QStringLiteral("40.78925034214206846173"));
assert(table->model()->data(table->model()->index(0, 3)).toString()
== QStringLiteral("29.46277530841727809729"));
sendLine(window, QStringLiteral(
"[LOCATION] source=gps lat=40.1 lon=29.1 height=100 gdop=2 pdop=1.5 "
"hdop=1 vdop=1.2 tdop=0.8 tracked=5 used=5"));
sendLine(window, QStringLiteral(
"[LOCATION] source=imu_corrected lat=40.10001 lon=29.10001 height=100.2 "
"gdop=2 pdop=1.5 hdop=1 vdop=1.2 tdop=0.8 tracked=5 used=5"));
flushUpdates(window);
assert(table->model()->rowCount() == 3);
assert(table->model()->data(table->model()->index(1, 1)).toString()
== QStringLiteral("GPS"));
assert(table->model()->data(table->model()->index(2, 1)).toString()
== QStringLiteral("IMU corrected"));
sendLine(window, QStringLiteral(
"[LOCATION] source=trajectory lat=40.10002 lon=29.10002 height=100 tracked=4 used=4"));
flushUpdates(window);
assert(table->model()->rowCount() == 4);
assert(table->model()->data(table->model()->index(3, 1)).toString()
== QStringLiteral("Trajectory (truth)"));
QTemporaryFile replayLog;
assert(replayLog.open());
replayLog.write(
"[LOCATION] source=trajectory lat=40.0 lon=29.0 height=100 tracked=4 used=4\n"
"[LOCATION] source=gps lat=40.0001 lon=29.0001 height=104 tracked=4 used=4\n"
"[LOCATION] source=imu_corrected lat=40.00002 lon=29.00002 height=101 tracked=4 used=4\n");
replayLog.flush();
assert(window.loadTelemetryLog(replayLog.fileName()));
assert(table->model()->rowCount() == 3);
assert(table->model()->data(table->model()->index(0, 1)).toString()
== QStringLiteral("Trajectory (truth)"));
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
sendLine(window, QStringLiteral(
"lat=40.78925034214206846173 long=29.46277530841727809729"));
flushUpdates(window);
assert(table->model()->rowCount() == 1);
referenceLatInput->setText(QStringLiteral("40.78925034214206846173"));
referenceLonInput->setText(QStringLiteral("29.46277530841727809729"));
const bool referenceUpdated = QMetaObject::invokeMethod(
@@ -144,6 +231,27 @@ int main(int argc, char **argv)
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
maxPointsInput->setValue(10);
{
MapWidget followMap;
TelemetryPoint corrected;
corrected.source = LocationSource::ImuCorrected;
corrected.latitude = 40.1;
corrected.longitude = 29.1;
corrected.receivedAt = QDateTime::currentDateTimeUtc();
TelemetryPoint rawGps;
rawGps.source = LocationSource::Gps;
rawGps.latitude = 41.0;
rawGps.longitude = 30.0;
rawGps.receivedAt = corrected.receivedAt.addMSecs(100);
followMap.setPoints({corrected, rawGps});
assert(followMap.centerCoordinate() == QPointF(40.1, 29.1));
rawGps.receivedAt = corrected.receivedAt.addMSecs(1100);
followMap.setPoints({corrected, rawGps});
assert(followMap.centerCoordinate() == QPointF(41.0, 30.0));
}
const QByteArray screenshotPath = qgetenv("LOCATION_PLOTTER_TEST_SCREENSHOT");
if (!screenshotPath.isEmpty()) {
sendLine(window, QStringLiteral("NAV solve start sats:5"));
+120 -2
View File
@@ -9,9 +9,39 @@
namespace {
bool closeTo(double left, double right)
bool closeTo(double left, double right, double tolerance = 0.000001)
{
return std::fabs(left - right) < 0.000001;
return std::fabs(left - right) < tolerance;
}
QString rinexField(double value)
{
return QStringLiteral("%1").arg(value, 19, 'E', 12);
}
QString rinexContinuation(std::initializer_list<double> values)
{
QString line(4, QLatin1Char(' '));
for (double value : values) line += rinexField(value);
return line + QLatin1Char('\n');
}
QByteArray syntheticGpsNavigation(const QDateTime &gpsEpochLabel, int gpsWeek, double toe)
{
QString rinex = QStringLiteral(
" 3.05 N: GNSS NAV DATA M: MIXED RINEX VERSION / TYPE\n"
" END OF HEADER\n");
rinex += QStringLiteral("G01 %1").arg(
gpsEpochLabel.toString(QStringLiteral("yyyy MM dd HH mm ss")));
rinex += rinexField(0.0) + rinexField(0.0) + rinexField(0.0) + QLatin1Char('\n');
rinex += rinexContinuation({1.0, 0.0, 0.0, 0.0});
rinex += rinexContinuation({0.0, 0.0, 0.0, 5153.7954775});
rinex += rinexContinuation({toe, 0.0, 0.0, 0.0});
rinex += rinexContinuation({0.0, 0.0, 0.0, 0.0});
rinex += rinexContinuation({0.0, 0.0, static_cast<double>(gpsWeek), 0.0});
rinex += rinexContinuation({2.0, 0.0, 0.0, 1.0});
rinex += rinexContinuation({0.0, 0.0, 0.0, 0.0});
return rinex.toLatin1();
}
} // namespace
@@ -156,6 +186,25 @@ int main(int argc, char **argv)
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G32")}));
}
{
const QSet<QString> prns = LocationParser::parseGpsPrns(
QStringLiteral("[TRK] tracked satellites=3 prn_indices=[0,7,31]"));
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G08"),
QStringLiteral("G32")}));
}
{
const QSet<QString> prns = LocationParser::parseGpsPrns(
QStringLiteral("[TRK] tracked satellites=2 prns=[0,7]"));
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G08")}));
}
{
const QSet<QString> prns = LocationParser::parseGpsPrns(
QStringLiteral("[TRK] tracked satellites=2 prn_indices=[0,G08]"));
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G08")}));
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[OK] [NAV] DOP GDOP=7.0907891622 PDOP=5.6333862175 "
@@ -179,6 +228,75 @@ int main(int argc, char **argv)
assert(closeTo(*parsed->hheight, 198.80186179187148809433));
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[LOCATION] source=imu_corrected mode=dead_reckoning "
"lat=40.7356 lon=31.5985 height=812.7 gps_age_ms=3180 "
"gdop=4.7 pdop=4.1 hdop=2.7 vdop=3.0 tdop=2.3 tracked=6 used=5"));
assert(parsed.has_value());
assert(parsed->hasLocation());
assert(parsed->source == LocationSource::ImuCorrected);
assert(parsed->fusionMode == FusionMode::DeadReckoning);
assert(parsed->gpsAgeMs == 3180);
assert(parsed->satellites == 6);
assert(closeTo(*parsed->dop.hdop, 2.7));
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[LOCATION] source=gps lat=40.7357 lon=31.5986 height=811 tracked=5 used=5"));
assert(parsed.has_value());
assert(parsed->source == LocationSource::Gps);
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"));
assert(parsed.has_value());
assert(parsed->hasLocation());
assert(parsed->source == LocationSource::Trajectory);
assert(parsed->satellites == 4);
}
{
constexpr int gpsWeek = 2428;
constexpr double toe = 403200.0;
const QDateTime gpsEpoch(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC);
const QDateTime gpsLabel = gpsEpoch.addSecs(
static_cast<qint64>(gpsWeek * 604800.0 + toe));
const QDateTime matchingUtc = gpsLabel.addSecs(-18);
QString error;
const auto satellites = RinexNavigation::parseGpsNavigation(
syntheticGpsNavigation(gpsLabel, gpsWeek, toe), matchingUtc, &error);
assert(error.isEmpty());
assert(satellites.size() == 1);
const double semiMajorAxis = 5153.7954775 * 5153.7954775;
const double node = -7.2921151467e-5 * toe;
assert(closeTo(satellites[0].x, semiMajorAxis * std::cos(node), 0.1));
assert(closeTo(satellites[0].y, semiMajorAxis * std::sin(node), 0.1));
assert(closeTo(satellites[0].z, 0.0, 0.1));
// A local wall-clock representation must not move the satellites.
// 11:48 at UTC+03 and 08:48 UTC represent the same physical instant.
const auto localTimeSatellites = RinexNavigation::parseGpsNavigation(
syntheticGpsNavigation(gpsLabel, gpsWeek, toe),
matchingUtc.toOffsetFromUtc(3 * 60 * 60), &error);
assert(error.isEmpty());
assert(localTimeSatellites.size() == satellites.size());
assert(closeTo(localTimeSatellites[0].x, satellites[0].x, 0.001));
assert(closeTo(localTimeSatellites[0].y, satellites[0].y, 0.001));
assert(closeTo(localTimeSatellites[0].z, satellites[0].z, 0.001));
}
const QString rinexPath = qEnvironmentVariable("LOCATION_PLOTTER_TEST_RINEX");
if (!rinexPath.isEmpty()) {
QFile file(rinexPath);
+70
View File
@@ -0,0 +1,70 @@
#include "../src/UartReplay.h"
#include <QCoreApplication>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QTemporaryFile>
#include <QTimer>
#include <cassert>
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QTemporaryFile log;
assert(log.open());
log.write(
"[2026-08-12 10:00:00.000] [SESSION START] port=/dev/test baud=460800\n"
"[2026-08-12 10:00:00.001] [LOCATION] source=gps lat=40.0 lon=29.0\n"
"diagnostic line without timestamp\n"
"[LOCATION] source=imu_corrected lat=40.1 lon=29.1\n"
"[FUSION] lat=40.2 lon=29.2 height=10\n"
"[LOCATION] source=gps lat=40.3 lon=29.3\n"
"[2026-08-12 10:00:00.999] [SESSION END]\n");
log.flush();
UartReplay replay;
QStringList received;
QVector<qint64> receivedAt;
QElapsedTimer elapsed;
QEventLoop loop;
QObject::connect(&replay, &UartReplay::lineReady, &loop, [&](const QString &line) {
received.push_back(line);
receivedAt.push_back(elapsed.elapsed());
});
QObject::connect(&replay, &UartReplay::replayFinished, &loop, &QEventLoop::quit);
QTimer::singleShot(2000, &loop, &QEventLoop::quit);
elapsed.start();
assert(replay.start(log.fileName(), 20, 10));
assert(replay.locationCount() == 4);
loop.exec();
assert(!replay.isRunning());
assert(received.size() == 5);
assert(received.first().startsWith(QStringLiteral("[LOCATION] source=gps")));
assert(received.at(1) == QStringLiteral("diagnostic line without timestamp"));
assert(received.at(3).startsWith(QStringLiteral("[FUSION]")));
assert(receivedAt.last() >= 15);
QTemporaryFile splitLog;
assert(splitLog.open());
splitLog.write("latitude: 40.25\nlongitude: 29.50\n");
splitLog.flush();
received.clear();
receivedAt.clear();
elapsed.restart();
assert(replay.start(splitLog.fileName(), 1, 1));
assert(replay.locationCount() == 1);
loop.exec();
assert(received.size() == 2);
QTemporaryFile invalidLog;
assert(invalidLog.open());
invalidLog.write("only diagnostics\n");
invalidLog.flush();
assert(!replay.start(invalidLog.fileName(), 200, 50));
return 0;
}