Merge Linux development branch
This commit is contained in:
@@ -8,3 +8,4 @@ build-*/
|
|||||||
*.autosave
|
*.autosave
|
||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
|
uart_logs/
|
||||||
|
|||||||
+46
-1
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
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 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -22,6 +22,8 @@ add_executable(location_plotter
|
|||||||
src/MapWidget.h
|
src/MapWidget.h
|
||||||
src/SerialReader.cpp
|
src/SerialReader.cpp
|
||||||
src/SerialReader.h
|
src/SerialReader.h
|
||||||
|
src/UartReplay.cpp
|
||||||
|
src/UartReplay.h
|
||||||
src/RinexNavigation.cpp
|
src/RinexNavigation.cpp
|
||||||
src/RinexNavigation.h
|
src/RinexNavigation.h
|
||||||
src/SatelliteData.h
|
src/SatelliteData.h
|
||||||
@@ -30,6 +32,19 @@ add_executable(location_plotter
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(location_plotter PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
|
target_link_libraries(location_plotter PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
|
||||||
|
target_compile_definitions(location_plotter PRIVATE
|
||||||
|
LOCATION_PLOTTER_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
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"
|
qt_add_resources(location_plotter "app_resources"
|
||||||
PREFIX "/"
|
PREFIX "/"
|
||||||
@@ -52,6 +67,9 @@ add_executable(location_parser_tests
|
|||||||
src/TelemetryPoint.h
|
src/TelemetryPoint.h
|
||||||
)
|
)
|
||||||
target_link_libraries(location_parser_tests PRIVATE Qt6::Core Qt6::Network ZLIB::ZLIB)
|
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_test(NAME location_parser_tests COMMAND location_parser_tests)
|
||||||
|
|
||||||
add_executable(location_main_window_tests
|
add_executable(location_main_window_tests
|
||||||
@@ -66,12 +84,20 @@ add_executable(location_main_window_tests
|
|||||||
src/GlobeWidget.h
|
src/GlobeWidget.h
|
||||||
src/SerialReader.cpp
|
src/SerialReader.cpp
|
||||||
src/SerialReader.h
|
src/SerialReader.h
|
||||||
|
src/UartReplay.cpp
|
||||||
|
src/UartReplay.h
|
||||||
src/RinexNavigation.cpp
|
src/RinexNavigation.cpp
|
||||||
src/RinexNavigation.h
|
src/RinexNavigation.h
|
||||||
src/SatelliteData.h
|
src/SatelliteData.h
|
||||||
src/TelemetryPoint.h
|
src/TelemetryPoint.h
|
||||||
)
|
)
|
||||||
target_link_libraries(location_main_window_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
|
target_link_libraries(location_main_window_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
|
||||||
|
target_compile_definitions(location_main_window_tests PRIVATE
|
||||||
|
LOCATION_PLOTTER_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
)
|
||||||
|
target_compile_options(location_main_window_tests PRIVATE
|
||||||
|
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG>
|
||||||
|
)
|
||||||
qt_add_resources(location_main_window_tests "test_app_resources"
|
qt_add_resources(location_main_window_tests "test_app_resources"
|
||||||
PREFIX "/"
|
PREFIX "/"
|
||||||
FILES resources/ankasys-logo.png resources/land-110m.json
|
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)
|
add_test(NAME location_main_window_tests COMMAND location_main_window_tests)
|
||||||
set_tests_properties(location_main_window_tests PROPERTIES
|
set_tests_properties(location_main_window_tests PROPERTIES
|
||||||
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-test-cache")
|
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-test-cache")
|
||||||
|
|
||||||
|
add_executable(location_uart_replay_tests
|
||||||
|
tests/uart_replay_tests.cpp
|
||||||
|
src/UartReplay.cpp
|
||||||
|
src/UartReplay.h
|
||||||
|
src/LocationParser.cpp
|
||||||
|
src/LocationParser.h
|
||||||
|
src/TelemetryPoint.h
|
||||||
|
)
|
||||||
|
target_link_libraries(location_uart_replay_tests PRIVATE Qt6::Core)
|
||||||
|
target_compile_options(location_uart_replay_tests PRIVATE
|
||||||
|
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG>
|
||||||
|
)
|
||||||
|
add_test(NAME location_uart_replay_tests COMMAND location_uart_replay_tests)
|
||||||
|
|
||||||
|
install(TARGETS location_plotter
|
||||||
|
BUNDLE DESTINATION .
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Location Plotter
|
# Location Plotter
|
||||||
|
|
||||||
Qt6/C++ GUI that listens to `/dev/ttyUSB2`, parses normal debug lines, and plots live
|
Qt6/C++ GUI that listens to a USB UART on Linux or macOS, parses normal debug lines,
|
||||||
locations on a map.
|
and plots live locations on a map.
|
||||||
|
|
||||||
The application also includes an interactive 3D Earth/orbit view. It downloads the current
|
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
|
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
|
TDOP : 2.2526165852
|
||||||
tracking PRNs: G03, G08, 14, 21
|
tracking PRNs: G03, G08, 14, 21
|
||||||
SV=G27
|
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
|
lattitude : 40.78925034214206846173
|
||||||
longitude : 29.46277530841727809729
|
longitude : 29.46277530841727809729
|
||||||
height : -89.05779508221894502640
|
height : -89.05779508221894502640
|
||||||
@@ -38,6 +41,9 @@ Accepted location labels:
|
|||||||
`activeSats`, `satellites_active`, `num_sats`, `uydu`
|
`activeSats`, `satellites_active`, `num_sats`, `uydu`
|
||||||
- DOP: `GDOP`, `PDOP`, `HDOP`, `VDOP`, `TDOP`
|
- DOP: `GDOP`, `PDOP`, `HDOP`, `VDOP`, `TDOP`
|
||||||
- GPS identifiers: `G03`, `PRN: 3`, `PRNs: 3, 8, 14`, `SV=G21`, `SVID: 27`
|
- 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 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
|
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
|
If latitude, longitude, and height arrive as separate consecutive lines, they are combined
|
||||||
into a single plotted point.
|
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'da kurulum
|
||||||
|
|
||||||
Ubuntu 22.04 ve daha yeni surumlerde gerekli paketleri kurmak, uygulamayi derlemek ve
|
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,
|
calistirilmalidir. Harita karolari ve guncel RINEX verisi icin internet erisimi gerekir,
|
||||||
ancak son gecerli RINEX verisi ve harita onbellegiyle cevrimdisi kullanim da desteklenir.
|
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
|
Port secici Linux'ta `/dev/ttyUSB*` ve `/dev/ttyACM*`; macOS'ta USB ile ilgili
|
||||||
`/dev/ttyUSB2` and the default baud rate is `115200`.
|
`/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:
|
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.
|
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
|
- 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
|
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,
|
- 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.
|
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
|
- 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.
|
marker can be toggled on the map.
|
||||||
- The map layers panel can hide all telemetry overlays, show only the newest location instead
|
- 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.
|
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
|
- 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.
|
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
|
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
|
- UART log and telemetry views are refreshed in batches so a high-rate serial stream does not
|
||||||
rebuild the complete interface for every received line. The locations table uses a virtual
|
rebuild the complete interface for every received line. The locations table uses a virtual
|
||||||
model and remains usable with a high `Max` value.
|
model and remains usable with a high `Max` value.
|
||||||
|
- Her basarili UART baglantisi, dosya adinda ve her satirda yerel zaman damgasi bulunan ayri bir
|
||||||
|
log olusturur. Dosyalar kaynak kodun ana klasorundeki `uart_logs` dizinine yazilir;
|
||||||
|
`LOCATION_PLOTTER_UART_LOG_DIR` ile farkli bir klasor secilebilir.
|
||||||
- If no new plotted location arrives for 10 seconds while listening, current location
|
- If no new plotted location arrives for 10 seconds while listening, current location
|
||||||
points and analysis values are cleared.
|
points and analysis values are cleared.
|
||||||
|
|||||||
@@ -5,29 +5,46 @@ cd "$(dirname "$0")"
|
|||||||
|
|
||||||
print_qt_install_hint() {
|
print_qt_install_hint() {
|
||||||
echo
|
echo
|
||||||
echo "Qt6 bulunamadi. Ubuntu'da gerekli paketleri kurmak icin:"
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
echo " sudo apt update"
|
echo "Qt6 bulunamadi. macOS'ta gerekli paketleri kurmak icin:"
|
||||||
echo " sudo apt install build-essential cmake qt6-base-dev"
|
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)"
|
project_dir="$(pwd -P)"
|
||||||
|
|
||||||
if [[ -f ./build/CMakeCache.txt ]]; then
|
if [[ -f ./build/CMakeCache.txt ]]; then
|
||||||
cached_source="$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' ./build/CMakeCache.txt)"
|
cached_source="$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' ./build/CMakeCache.txt)"
|
||||||
if [[ -n "$cached_source" && "$cached_source" != "$project_dir" ]]; then
|
cached_system="$(sed -n 's/^CMAKE_SYSTEM_NAME:INTERNAL=//p' ./build/CMakeCache.txt)"
|
||||||
echo "Build klasoru farkli bir proje yoluna ait, yeniden olusturuluyor:"
|
host_system="$(uname -s)"
|
||||||
echo " $cached_source"
|
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
|
rm -rf ./build
|
||||||
fi
|
fi
|
||||||
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
|
print_qt_install_hint
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cmake --build build
|
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)"
|
missing_libs="$(ldd ./build/location_plotter 2>/dev/null | awk '/not found/ { print $1 }' || true)"
|
||||||
if [[ -n "$missing_libs" ]]; then
|
if [[ -n "$missing_libs" ]]; then
|
||||||
echo "Eksik paylasimli kutuphaneler:"
|
echo "Eksik paylasimli kutuphaneler:"
|
||||||
|
|||||||
Executable
+47
@@ -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
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr double Pi = 3.14159265358979323846;
|
constexpr double Pi = 3.14159265358979323846;
|
||||||
@@ -211,8 +212,10 @@ GlobeWidget::GlobeWidget(QWidget *parent)
|
|||||||
setMouseTracking(true);
|
setMouseTracking(true);
|
||||||
setCursor(Qt::OpenHandCursor);
|
setCursor(Qt::OpenHandCursor);
|
||||||
setToolTip(QStringLiteral("Dunyayi dondurmek icin surukleyin, yakinlastirmak icin tekerlegi kullanin"));
|
setToolTip(QStringLiteral("Dunyayi dondurmek icin surukleyin, yakinlastirmak icin tekerlegi kullanin"));
|
||||||
m_satelliteAnimationTimer.setInterval(33);
|
// GNSS satellites move only a few screen pixels per minute. Ten frames per
|
||||||
m_satelliteAnimationTimer.setTimerType(Qt::PreciseTimer);
|
// 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,
|
connect(&m_satelliteAnimationTimer, &QTimer::timeout,
|
||||||
this, &GlobeWidget::updateSatelliteAnimation);
|
this, &GlobeWidget::updateSatelliteAnimation);
|
||||||
}
|
}
|
||||||
@@ -235,8 +238,29 @@ void GlobeWidget::setSatelliteMotion(const QVector<SatellitePosition> ¤t,
|
|||||||
|
|
||||||
// Continue from the currently rendered points when a fresh ephemeris arrives,
|
// Continue from the currently rendered points when a fresh ephemeris arrives,
|
||||||
// avoiding even a small visual jump at the update boundary.
|
// avoiding even a small visual jump at the update boundary.
|
||||||
m_satelliteMotionStart = m_satellites.isEmpty() ? current : m_satellites;
|
const QVector<SatellitePosition> &candidateStart =
|
||||||
m_satelliteMotionTarget = next;
|
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_satelliteMotionDurationMs = durationMs;
|
||||||
m_satelliteMotionClock.restart();
|
m_satelliteMotionClock.restart();
|
||||||
m_satelliteAnimationTimer.start();
|
m_satelliteAnimationTimer.start();
|
||||||
@@ -250,18 +274,11 @@ void GlobeWidget::updateSatelliteAnimation()
|
|||||||
const double progress = std::clamp(
|
const double progress = std::clamp(
|
||||||
static_cast<double>(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs,
|
static_cast<double>(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs,
|
||||||
0.0, 1.0);
|
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;
|
QVector<SatellitePosition> interpolated;
|
||||||
interpolated.reserve(m_satelliteMotionStart.size());
|
interpolated.reserve(m_satelliteMotionStart.size());
|
||||||
for (const SatellitePosition &start : m_satelliteMotionStart) {
|
for (qsizetype i = 0; i < m_satelliteMotionStart.size(); ++i) {
|
||||||
const auto targetIt = targets.constFind(start.prn);
|
const SatellitePosition &start = m_satelliteMotionStart[i];
|
||||||
if (targetIt == targets.cend()) continue;
|
const SatellitePosition &target = m_satelliteMotionTarget[i];
|
||||||
const SatellitePosition &target = targetIt.value();
|
|
||||||
SatellitePosition point = start;
|
SatellitePosition point = start;
|
||||||
point.x += (target.x - start.x) * progress;
|
point.x += (target.x - start.x) * progress;
|
||||||
point.y += (target.y - start.y) * progress;
|
point.y += (target.y - start.y) * progress;
|
||||||
@@ -336,6 +353,74 @@ void GlobeWidget::paintEvent(QPaintEvent *)
|
|||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
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());
|
QLinearGradient background(0, 0, width(), height());
|
||||||
background.setColorAt(0.0, QColor(3, 10, 21));
|
background.setColorAt(0.0, QColor(3, 10, 21));
|
||||||
background.setColorAt(0.55, QColor(8, 24, 43));
|
background.setColorAt(0.55, QColor(8, 24, 43));
|
||||||
@@ -374,63 +459,23 @@ void GlobeWidget::paintEvent(QPaintEvent *)
|
|||||||
painter.restore();
|
painter.restore();
|
||||||
|
|
||||||
for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0);
|
for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0);
|
||||||
if (m_hasReceiver) drawVisibilityCone(painter);
|
painter.end();
|
||||||
if (m_hasReceiver) drawReceiver(painter);
|
|
||||||
drawSatellites(painter);
|
|
||||||
|
|
||||||
painter.setPen(QColor(229, 240, 249));
|
m_staticLayer = std::move(layer);
|
||||||
QFont titleFont = painter.font();
|
m_staticLayerSize = size();
|
||||||
titleFont.setPixelSize(18);
|
m_staticLayerDevicePixelRatio = devicePixelRatio;
|
||||||
titleFont.setBold(true);
|
m_staticLayerYaw = m_yaw;
|
||||||
painter.setFont(titleFont);
|
m_staticLayerPitch = m_pitch;
|
||||||
painter.drawText(QRect(20, 16, width() - 40, 28), Qt::AlignLeft | Qt::AlignVCenter,
|
m_staticLayerZoom = m_zoom;
|
||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GlobeWidget::drawContinents(QPainter &painter) const
|
void GlobeWidget::drawContinents(QPainter &painter) const
|
||||||
{
|
{
|
||||||
const double radius = std::min(width(), height()) * 0.255 * m_zoom;
|
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(
|
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 QPointF center(width() * 0.5, height() * 0.52);
|
||||||
const QRectF target(center.x() - radius, center.y() - radius,
|
const QRectF target(center.x() - radius, center.y() - radius,
|
||||||
radius * 2.0, radius * 2.0);
|
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"));
|
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;
|
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;
|
QVector<Item> items;
|
||||||
|
items.reserve(m_satellites.size());
|
||||||
for (const auto &satellite : m_satellites) {
|
for (const auto &satellite : m_satellites) {
|
||||||
const double length = std::sqrt(satellite.x * satellite.x + satellite.y * satellite.y
|
const double length = std::sqrt(satellite.x * satellite.x + satellite.y * satellite.y
|
||||||
+ satellite.z * satellite.z);
|
+ satellite.z * satellite.z);
|
||||||
if (length <= 0.0) continue;
|
if (length <= 0.0) continue;
|
||||||
items.push_back({satellite, project(satellite.x / length, satellite.y / length,
|
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) {
|
std::sort(items.begin(), items.end(), [](const Item &a, const Item &b) {
|
||||||
return a.projected.depth < b.projected.depth;
|
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) {
|
for (const Item &item : items) {
|
||||||
const bool found = m_observedPrns.contains(item.satellite.prn);
|
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);
|
QColor color = found ? QColor(42, 224, 137) : QColor(255, 79, 95);
|
||||||
if (item.projected.depth < 0.0) color.setAlpha(75);
|
if (item.projected.depth < 0.0) color.setAlpha(75);
|
||||||
const double markerRadius = found ? 6.5 : 5.0;
|
const double markerRadius = found ? 6.5 : 5.0;
|
||||||
if (inElevationMask) {
|
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);
|
QColor sightLine(70, 224, 231, item.projected.depth < 0.0 ? 25 : 80);
|
||||||
painter.setPen(QPen(sightLine, 0.8, Qt::DashLine));
|
painter.setPen(QPen(sightLine, 0.8, Qt::DashLine));
|
||||||
painter.drawLine(receiverPoint.point, item.projected.point);
|
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);
|
painter.drawText(item.projected.point + QPointF(7, -6), item.satellite.prn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return visibleCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
double GlobeWidget::satelliteElevation(const SatellitePosition &satellite) const
|
double GlobeWidget::satelliteElevation(const SatellitePosition &satellite) const
|
||||||
@@ -802,7 +856,9 @@ void GlobeWidget::mouseReleaseEvent(QMouseEvent *event)
|
|||||||
{
|
{
|
||||||
if (event->button() == Qt::LeftButton) {
|
if (event->button() == Qt::LeftButton) {
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
|
m_staticLayer = QPixmap();
|
||||||
setCursor(Qt::OpenHandCursor);
|
setCursor(Qt::OpenHandCursor);
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -5,6 +5,7 @@
|
|||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
|
#include <QPixmap>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
@@ -23,6 +24,7 @@ public:
|
|||||||
void setReceiverLocation(double latitude, double longitude);
|
void setReceiverLocation(double latitude, double longitude);
|
||||||
void clearReceiverLocation();
|
void clearReceiverLocation();
|
||||||
void setRinexStatus(const QString &status, bool error);
|
void setRinexStatus(const QString &status, bool error);
|
||||||
|
[[nodiscard]] QSet<QString> observedPrns() const { return m_observedPrns; }
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void setElevationMask(double degrees);
|
void setElevationMask(double degrees);
|
||||||
@@ -47,9 +49,10 @@ private:
|
|||||||
void drawVisibilityCone(QPainter &painter) const;
|
void drawVisibilityCone(QPainter &painter) const;
|
||||||
void drawGraticule(QPainter &painter) const;
|
void drawGraticule(QPainter &painter) const;
|
||||||
void drawReceiver(QPainter &painter) const;
|
void drawReceiver(QPainter &painter) const;
|
||||||
void drawSatellites(QPainter &painter) const;
|
int drawSatellites(QPainter &painter) const;
|
||||||
double satelliteElevation(const SatellitePosition &satellite) const;
|
double satelliteElevation(const SatellitePosition &satellite) const;
|
||||||
void updateSatelliteAnimation();
|
void updateSatelliteAnimation();
|
||||||
|
void rebuildStaticLayer();
|
||||||
|
|
||||||
QVector<SatellitePosition> m_satellites;
|
QVector<SatellitePosition> m_satellites;
|
||||||
QVector<SatellitePosition> m_satelliteMotionStart;
|
QVector<SatellitePosition> m_satelliteMotionStart;
|
||||||
@@ -60,6 +63,12 @@ private:
|
|||||||
mutable QImage m_landTexture;
|
mutable QImage m_landTexture;
|
||||||
mutable double m_landTextureYaw = 0.0;
|
mutable double m_landTextureYaw = 0.0;
|
||||||
mutable double m_landTexturePitch = 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;
|
QSet<QString> m_observedPrns;
|
||||||
QString m_status = QStringLiteral("Konum bekleniyor");
|
QString m_status = QStringLiteral("Konum bekleniyor");
|
||||||
bool m_statusError = false;
|
bool m_statusError = false;
|
||||||
|
|||||||
+65
-9
@@ -49,6 +49,10 @@ QString normalizeKey(QString key)
|
|||||||
return "satellites";
|
return "satellites";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key == "tracked") {
|
||||||
|
return "satellites";
|
||||||
|
}
|
||||||
|
|
||||||
if (key == "gdop" || key == "pdop" || key == "hdop" || key == "vdop" || key == "tdop") {
|
if (key == "gdop" || key == "pdop" || key == "hdop" || key == "vdop" || key == "tdop") {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
@@ -76,7 +80,7 @@ std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const QRegularExpression labeledValueRegex(
|
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);
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
static const QRegularExpression satelliteCountPhraseRegex(
|
static const QRegularExpression satelliteCountPhraseRegex(
|
||||||
QStringLiteral(R"((^|[^A-Za-z0-9_\.])(?<value>\d+)\s+(?:active\s+)?(?:sat(?:ellite)?s?|uydu(?:lar)?)(?=$|[\s,.;)\]]))"),
|
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;
|
ParsedTelemetry telemetry;
|
||||||
telemetry.rawLine = trimmed;
|
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 hadCoordinateField = false;
|
||||||
bool hadLatitudeField = false;
|
bool hadLatitudeField = false;
|
||||||
bool hadLongitudeField = false;
|
bool hadLongitudeField = false;
|
||||||
@@ -195,10 +237,11 @@ QSet<QString> LocationParser::parseGpsPrns(const QString &line)
|
|||||||
QSet<QString> result;
|
QSet<QString> result;
|
||||||
const auto addPrn = [&result](QString value, bool zeroBased = false) {
|
const auto addPrn = [&result](QString value, bool zeroBased = false) {
|
||||||
value = value.trimmed().toUpper();
|
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;
|
bool ok = false;
|
||||||
int number = value.toInt(&ok);
|
int number = value.toInt(&ok);
|
||||||
if (ok && zeroBased) ++number;
|
if (ok && zeroBased && !explicitGpsPrn) ++number;
|
||||||
if (ok && number >= 1 && number <= 63) {
|
if (ok && number >= 1 && number <= 63) {
|
||||||
result.insert(QStringLiteral("G%1").arg(number, 2, 10, QLatin1Char('0')));
|
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);
|
auto gpsMatches = gpsToken.globalMatch(line);
|
||||||
while (gpsMatches.hasNext()) addPrn(gpsMatches.next().captured(QStringLiteral("prn")));
|
while (gpsMatches.hasNext()) addPrn(gpsMatches.next().captured(QStringLiteral("prn")));
|
||||||
|
|
||||||
static const QRegularExpression labeledList(
|
// Current firmware versions called their zero-based hardware channel values
|
||||||
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*[\])]?)"),
|
// "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);
|
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);
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
auto listMatches = labeledList.globalMatch(line);
|
auto listMatches = labeledList.globalMatch(line);
|
||||||
while (listMatches.hasNext()) {
|
while (listMatches.hasNext()) {
|
||||||
const QRegularExpressionMatch listMatch = listMatches.next();
|
const QRegularExpressionMatch listMatch = listMatches.next();
|
||||||
const bool zeroBased = listMatch.captured(QStringLiteral("label")).startsWith(
|
QString label = listMatch.captured(QStringLiteral("label")).toUpper();
|
||||||
QStringLiteral("SVN"), Qt::CaseInsensitive);
|
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")));
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
+513
-32
@@ -5,26 +5,35 @@
|
|||||||
#include "MapWidget.h"
|
#include "MapWidget.h"
|
||||||
#include "RinexNavigation.h"
|
#include "RinexNavigation.h"
|
||||||
#include "SerialReader.h"
|
#include "SerialReader.h"
|
||||||
|
#include "UartReplay.h"
|
||||||
|
|
||||||
#include <QAbstractItemView>
|
#include <QAbstractItemView>
|
||||||
#include <QAbstractTableModel>
|
#include <QAbstractTableModel>
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
#include <QCloseEvent>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
#include <QDoubleSpinBox>
|
#include <QDoubleSpinBox>
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QPlainTextEdit>
|
#include <QPlainTextEdit>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
|
#include <QSettings>
|
||||||
#include <QStatusBar>
|
#include <QStatusBar>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
@@ -155,6 +164,17 @@ LocationAnalysis analyzeLocations(const QVector<TelemetryPoint> &points)
|
|||||||
return analysis;
|
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)
|
QString metersText(double meters)
|
||||||
{
|
{
|
||||||
if (!std::isfinite(meters)) {
|
if (!std::isfinite(meters)) {
|
||||||
@@ -199,6 +219,45 @@ QString optionalDopText(const std::optional<double> &value)
|
|||||||
return QString::number(*value, 'f', 3);
|
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)
|
void mergeDopValues(DopValues &target, const DopValues &source)
|
||||||
{
|
{
|
||||||
if (source.gdop.has_value()) {
|
if (source.gdop.has_value()) {
|
||||||
@@ -255,7 +314,7 @@ public:
|
|||||||
|
|
||||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override
|
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
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
|
||||||
@@ -275,15 +334,29 @@ public:
|
|||||||
case 0:
|
case 0:
|
||||||
return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz"));
|
return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz"));
|
||||||
case 1:
|
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:
|
case 2:
|
||||||
return coordinateText(point.longitude, point.longitudeText);
|
return coordinateText(point.latitude, point.latitudeText);
|
||||||
case 3:
|
case 3:
|
||||||
return optionalDoubleText(point.hheight, 2);
|
return coordinateText(point.longitude, point.longitudeText);
|
||||||
case 4:
|
case 4:
|
||||||
return optionalIntText(point.satellites);
|
return optionalDoubleText(point.hheight, 2);
|
||||||
case 5:
|
case 5:
|
||||||
|
return optionalIntText(point.satellites);
|
||||||
|
case 6:
|
||||||
return optionalDopText(point.dop.pdop);
|
return optionalDopText(point.dop.pdop);
|
||||||
|
case 7:
|
||||||
|
return point.gpsAgeMs.has_value()
|
||||||
|
? QStringLiteral("%1 ms").arg(*point.gpsAgeMs)
|
||||||
|
: QStringLiteral("-");
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -296,8 +369,9 @@ public:
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
static const QStringList headers = {
|
static const QStringList headers = {
|
||||||
QStringLiteral("Saat"), QStringLiteral("Lat"), QStringLiteral("Long"),
|
QStringLiteral("Saat"), QStringLiteral("Kaynak"), QStringLiteral("Lat"), QStringLiteral("Long"),
|
||||||
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP")};
|
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP"),
|
||||||
|
QStringLiteral("GPS yas")};
|
||||||
return section >= 0 && section < headers.size() ? headers[section] : QVariant();
|
return section >= 0 && section < headers.size() ? headers[section] : QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,6 +382,7 @@ private:
|
|||||||
MainWindow::MainWindow(QWidget *parent)
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
: QMainWindow(parent)
|
: QMainWindow(parent)
|
||||||
, m_serial(new SerialReader(this))
|
, m_serial(new SerialReader(this))
|
||||||
|
, m_replay(new UartReplay(this))
|
||||||
, m_rinex(new RinexNavigation(this))
|
, m_rinex(new RinexNavigation(this))
|
||||||
{
|
{
|
||||||
buildUi();
|
buildUi();
|
||||||
@@ -323,6 +398,40 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
connect(m_serial, &SerialReader::lineReceived, this, &MainWindow::handleSerialLine);
|
connect(m_serial, &SerialReader::lineReceived, this, &MainWindow::handleSerialLine);
|
||||||
connect(m_serial, &SerialReader::statusChanged, this, &MainWindow::handleSerialStatus);
|
connect(m_serial, &SerialReader::statusChanged, this, &MainWindow::handleSerialStatus);
|
||||||
connect(m_serial, &SerialReader::errorOccurred, this, &MainWindow::handleSerialError);
|
connect(m_serial, &SerialReader::errorOccurred, this, &MainWindow::handleSerialError);
|
||||||
|
connect(m_replay, &UartReplay::lineReady, this, &MainWindow::handleSerialLine);
|
||||||
|
connect(m_replay, &UartReplay::replayStarted, this,
|
||||||
|
[this](int lineCount, int locationCount) {
|
||||||
|
const QString message = QStringLiteral(
|
||||||
|
"UART replay basladi: %1 konum, %2 satir")
|
||||||
|
.arg(locationCount).arg(lineCount);
|
||||||
|
m_statusLabel->setText(message);
|
||||||
|
m_log->appendPlainText(QStringLiteral("[STATUS] ") + message);
|
||||||
|
updateConnectionControls();
|
||||||
|
});
|
||||||
|
connect(m_replay, &UartReplay::progressChanged, this,
|
||||||
|
[this](int processed, int total) {
|
||||||
|
if (processed == total || processed % 100 == 0) {
|
||||||
|
m_statusLabel->setText(QStringLiteral("UART replay: %1 / %2 satir")
|
||||||
|
.arg(processed).arg(total));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_replay, &UartReplay::replayFinished, this, [this] {
|
||||||
|
commitPendingTelemetry();
|
||||||
|
flushUiUpdates();
|
||||||
|
m_map->fitToPoints();
|
||||||
|
const QString message = QStringLiteral("UART replay tamamlandi: %1 konum")
|
||||||
|
.arg(m_points.size());
|
||||||
|
m_statusLabel->setText(message);
|
||||||
|
m_log->appendPlainText(QStringLiteral("[STATUS] ") + message);
|
||||||
|
statusBar()->showMessage(message, 8000);
|
||||||
|
updateConnectionControls();
|
||||||
|
});
|
||||||
|
connect(m_replay, &UartReplay::errorOccurred, this, [this](const QString &message) {
|
||||||
|
m_statusLabel->setText(message);
|
||||||
|
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
|
||||||
|
statusBar()->showMessage(message, 8000);
|
||||||
|
updateConnectionControls();
|
||||||
|
});
|
||||||
connect(m_locationTimeoutTimer, &QTimer::timeout, this, &MainWindow::handleLocationTimeout);
|
connect(m_locationTimeoutTimer, &QTimer::timeout, this, &MainWindow::handleLocationTimeout);
|
||||||
connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates);
|
connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates);
|
||||||
connect(m_rinex, &RinexNavigation::satelliteMotionUpdated,
|
connect(m_rinex, &RinexNavigation::satelliteMotionUpdated,
|
||||||
@@ -332,6 +441,14 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
m_globe->setRinexStatus(status, error);
|
m_globe->setRinexStatus(status, error);
|
||||||
statusBar()->showMessage(status, error ? 8000 : 4000);
|
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();
|
m_rinex->refresh();
|
||||||
auto *rinexRefreshTimer = new QTimer(this);
|
auto *rinexRefreshTimer = new QTimer(this);
|
||||||
rinexRefreshTimer->setInterval(15 * 60 * 1000);
|
rinexRefreshTimer->setInterval(15 * 60 * 1000);
|
||||||
@@ -342,10 +459,21 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
setWindowTitle(QStringLiteral("Canli Konum Haritasi"));
|
setWindowTitle(QStringLiteral("Canli Konum Haritasi"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::closeEvent(QCloseEvent *event)
|
||||||
|
{
|
||||||
|
m_replay->stop();
|
||||||
|
stopUartLogging();
|
||||||
|
QMainWindow::closeEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::toggleSerial()
|
void MainWindow::toggleSerial()
|
||||||
{
|
{
|
||||||
|
if (m_replay->isRunning()) {
|
||||||
|
stopUartReplay();
|
||||||
|
}
|
||||||
if (m_serial->isOpen()) {
|
if (m_serial->isOpen()) {
|
||||||
m_serial->closePort();
|
m_serial->closePort();
|
||||||
|
stopUartLogging();
|
||||||
if (m_locationTimeoutTimer != nullptr) {
|
if (m_locationTimeoutTimer != nullptr) {
|
||||||
m_locationTimeoutTimer->stop();
|
m_locationTimeoutTimer->stop();
|
||||||
}
|
}
|
||||||
@@ -353,11 +481,130 @@ void MainWindow::toggleSerial()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_serial->openPort(m_portCombo->currentText().trimmed(), selectedBaudRate())) {
|
const QString port = m_portCombo->currentText().trimmed();
|
||||||
|
const int baudRate = selectedBaudRate();
|
||||||
|
if (m_serial->openPort(port, baudRate)) {
|
||||||
|
startUartLogging(port, baudRate);
|
||||||
updateConnectionControls();
|
updateConnectionControls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MainWindow::startUartReplay(const QString &filePath, int gpsIntervalMs,
|
||||||
|
int sensorIntervalMs)
|
||||||
|
{
|
||||||
|
if (m_serial->isOpen()) {
|
||||||
|
m_serial->closePort();
|
||||||
|
stopUartLogging();
|
||||||
|
}
|
||||||
|
m_replay->stop();
|
||||||
|
resetTelemetryState();
|
||||||
|
|
||||||
|
if (!m_replay->start(filePath, gpsIntervalMs, sensorIntervalMs)) {
|
||||||
|
updateConnectionControls();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_maxPointsSpin->setValue(std::clamp(m_replay->locationCount(), 1, 10000));
|
||||||
|
statusBar()->showMessage(
|
||||||
|
QStringLiteral("UART replay: GPS %1 ms, sensor/fused %2 ms")
|
||||||
|
.arg(gpsIntervalMs).arg(sensorIntervalMs), 8000);
|
||||||
|
updateConnectionControls();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::stopUartReplay()
|
||||||
|
{
|
||||||
|
if (!m_replay->isRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_replay->stop();
|
||||||
|
commitPendingTelemetry();
|
||||||
|
flushUiUpdates();
|
||||||
|
const QString message = QStringLiteral("UART replay durduruldu: %1 konum")
|
||||||
|
.arg(m_points.size());
|
||||||
|
m_statusLabel->setText(message);
|
||||||
|
m_log->appendPlainText(QStringLiteral("[STATUS] ") + message);
|
||||||
|
statusBar()->showMessage(message, 5000);
|
||||||
|
updateConnectionControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MainWindow::startUartLogging(const QString &port, int baudRate)
|
||||||
|
{
|
||||||
|
stopUartLogging();
|
||||||
|
|
||||||
|
QString logDirectory = QString::fromLocal8Bit(qgetenv("LOCATION_PLOTTER_UART_LOG_DIR"));
|
||||||
|
if (logDirectory.isEmpty()) {
|
||||||
|
logDirectory = QDir(QStringLiteral(LOCATION_PLOTTER_SOURCE_DIR))
|
||||||
|
.filePath(QStringLiteral("uart_logs"));
|
||||||
|
} else if (QDir::isRelativePath(logDirectory)) {
|
||||||
|
logDirectory = QDir::current().absoluteFilePath(logDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!QDir().mkpath(logDirectory)) {
|
||||||
|
const QString message = QStringLiteral("UART log klasoru olusturulamadi: %1")
|
||||||
|
.arg(logDirectory);
|
||||||
|
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
|
||||||
|
statusBar()->showMessage(message, 8000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString fileName = QStringLiteral("uart_%1.log").arg(
|
||||||
|
QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss_zzz")));
|
||||||
|
m_uartLogPath = QDir(logDirectory).filePath(fileName);
|
||||||
|
m_uartLogFile = new QFile(m_uartLogPath, this);
|
||||||
|
if (!m_uartLogFile->open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
const QString message = QStringLiteral("UART log dosyasi acilamadi: %1")
|
||||||
|
.arg(m_uartLogFile->errorString());
|
||||||
|
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
|
||||||
|
statusBar()->showMessage(message, 8000);
|
||||||
|
delete m_uartLogFile;
|
||||||
|
m_uartLogFile = nullptr;
|
||||||
|
m_uartLogPath.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString header = QStringLiteral("[%1] [SESSION START] port=%2 baud=%3\n")
|
||||||
|
.arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")),
|
||||||
|
port)
|
||||||
|
.arg(baudRate);
|
||||||
|
m_uartLogFile->write(header.toUtf8());
|
||||||
|
m_uartLogFile->flush();
|
||||||
|
statusBar()->showMessage(QStringLiteral("UART log kaydi: %1").arg(m_uartLogPath), 8000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::stopUartLogging()
|
||||||
|
{
|
||||||
|
if (m_uartLogFile == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_uartLogFile->isOpen()) {
|
||||||
|
const QString footer = QStringLiteral("[%1] [SESSION END]\n").arg(
|
||||||
|
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")));
|
||||||
|
m_uartLogFile->write(footer.toUtf8());
|
||||||
|
m_uartLogFile->flush();
|
||||||
|
m_uartLogFile->close();
|
||||||
|
}
|
||||||
|
delete m_uartLogFile;
|
||||||
|
m_uartLogFile = nullptr;
|
||||||
|
m_uartLogPath.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::appendUartLogLine(const QString &line)
|
||||||
|
{
|
||||||
|
if (m_uartLogFile == nullptr || !m_uartLogFile->isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QString cleanLine = line;
|
||||||
|
cleanLine.replace(QLatin1Char('\r'), QStringLiteral("\\r"));
|
||||||
|
cleanLine.replace(QLatin1Char('\n'), QStringLiteral("\\n"));
|
||||||
|
const QString entry = QStringLiteral("[%1] %2\n")
|
||||||
|
.arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")),
|
||||||
|
cleanLine);
|
||||||
|
m_uartLogFile->write(entry.toUtf8());
|
||||||
|
m_uartLogFile->flush();
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::clearPoints()
|
void MainWindow::clearPoints()
|
||||||
{
|
{
|
||||||
if (m_locationTimeoutTimer != nullptr) {
|
if (m_locationTimeoutTimer != nullptr) {
|
||||||
@@ -366,20 +613,193 @@ void MainWindow::clearPoints()
|
|||||||
resetTelemetryState();
|
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)
|
void MainWindow::handleSerialLine(const QString &line)
|
||||||
{
|
{
|
||||||
|
appendUartLogLine(line);
|
||||||
static const QRegularExpression solveStartRegex(
|
static const QRegularExpression solveStartRegex(
|
||||||
QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"),
|
QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"),
|
||||||
QRegularExpression::CaseInsensitiveOption);
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
|
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 solveStarted = solveStartRegex.match(line.trimmed()).hasMatch();
|
||||||
|
const bool trackingSnapshot = trackingSnapshotRegex.match(line).hasMatch();
|
||||||
if (solveStarted) {
|
if (solveStarted) {
|
||||||
m_observedPrns.clear();
|
m_observedPrns.clear();
|
||||||
}
|
}
|
||||||
const QSet<QString> linePrns = LocationParser::parseGpsPrns(line);
|
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);
|
m_observedPrns.unite(linePrns);
|
||||||
}
|
}
|
||||||
if (solveStarted || !linePrns.isEmpty()) {
|
if (solveStarted || trackingSnapshot || !linePrns.isEmpty()) {
|
||||||
m_globe->setObservedPrns(m_observedPrns);
|
m_globe->setObservedPrns(m_observedPrns);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +855,7 @@ void MainWindow::handleSerialStatus(const QString &status)
|
|||||||
|
|
||||||
void MainWindow::handleSerialError(const QString &message)
|
void MainWindow::handleSerialError(const QString &message)
|
||||||
{
|
{
|
||||||
|
stopUartLogging();
|
||||||
flushPendingLogLines();
|
flushPendingLogLines();
|
||||||
m_statusLabel->setText(message);
|
m_statusLabel->setText(message);
|
||||||
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
|
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
|
||||||
@@ -668,25 +1089,22 @@ void MainWindow::buildUi()
|
|||||||
|
|
||||||
m_portCombo = new QComboBox(this);
|
m_portCombo = new QComboBox(this);
|
||||||
m_portCombo->setObjectName(QStringLiteral("portSelector"));
|
m_portCombo->setObjectName(QStringLiteral("portSelector"));
|
||||||
m_portCombo->setMinimumWidth(165);
|
m_portCombo->setMinimumWidth(220);
|
||||||
m_portCombo->setEditable(false);
|
m_portCombo->setEditable(true);
|
||||||
QDir deviceDirectory(QStringLiteral("/dev"));
|
m_portCombo->setToolTip(QStringLiteral(
|
||||||
const QStringList deviceNames = deviceDirectory.entryList(
|
"USB UART portu. macOS'ta /dev/cu.usb... tercih edilir; /dev/tty.usb... da desteklenir."));
|
||||||
{QStringLiteral("ttyUSB*")},
|
refreshSerialPorts();
|
||||||
QDir::System | QDir::Files | QDir::Readable, QDir::Name);
|
|
||||||
for (const QString &deviceName : deviceNames) {
|
auto *refreshPortsButton = new QToolButton(this);
|
||||||
m_portCombo->addItem(QStringLiteral("/dev/") + deviceName);
|
refreshPortsButton->setObjectName(QStringLiteral("refreshPortsButton"));
|
||||||
}
|
refreshPortsButton->setText(QStringLiteral("Yenile"));
|
||||||
if (m_portCombo->findText(QStringLiteral("/dev/ttyUSB2")) < 0) {
|
refreshPortsButton->setToolTip(QStringLiteral("Seri port listesini yenile"));
|
||||||
m_portCombo->addItem(QStringLiteral("/dev/ttyUSB2"));
|
|
||||||
}
|
|
||||||
m_portCombo->setCurrentText(QStringLiteral("/dev/ttyUSB2"));
|
|
||||||
|
|
||||||
m_baudCombo = new QComboBox(this);
|
m_baudCombo = new QComboBox(this);
|
||||||
for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) {
|
for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) {
|
||||||
m_baudCombo->addItem(QString::number(baud));
|
m_baudCombo->addItem(QString::number(baud));
|
||||||
}
|
}
|
||||||
m_baudCombo->setCurrentText(QStringLiteral("115200"));
|
m_baudCombo->setCurrentText(QStringLiteral("460800"));
|
||||||
|
|
||||||
m_maxPointsSpin = new QSpinBox(this);
|
m_maxPointsSpin = new QSpinBox(this);
|
||||||
m_maxPointsSpin->setObjectName(QStringLiteral("maxPointsInput"));
|
m_maxPointsSpin->setObjectName(QStringLiteral("maxPointsInput"));
|
||||||
@@ -698,6 +1116,14 @@ void MainWindow::buildUi()
|
|||||||
m_connectButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
|
m_connectButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
|
||||||
m_clearButton = new QPushButton(QStringLiteral("Temizle"), this);
|
m_clearButton = new QPushButton(QStringLiteral("Temizle"), this);
|
||||||
m_clearButton->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
|
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 = new QCheckBox(QStringLiteral("Kamerayi takip et"), this);
|
||||||
m_autoFollowCheck->setChecked(true);
|
m_autoFollowCheck->setChecked(true);
|
||||||
@@ -711,11 +1137,14 @@ void MainWindow::buildUi()
|
|||||||
|
|
||||||
toolbarLayout->addWidget(new QLabel(QStringLiteral("Port"), this));
|
toolbarLayout->addWidget(new QLabel(QStringLiteral("Port"), this));
|
||||||
toolbarLayout->addWidget(m_portCombo);
|
toolbarLayout->addWidget(m_portCombo);
|
||||||
|
toolbarLayout->addWidget(refreshPortsButton);
|
||||||
toolbarLayout->addWidget(new QLabel(QStringLiteral("Baud"), this));
|
toolbarLayout->addWidget(new QLabel(QStringLiteral("Baud"), this));
|
||||||
toolbarLayout->addWidget(m_baudCombo);
|
toolbarLayout->addWidget(m_baudCombo);
|
||||||
toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this));
|
toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this));
|
||||||
toolbarLayout->addWidget(m_maxPointsSpin);
|
toolbarLayout->addWidget(m_maxPointsSpin);
|
||||||
toolbarLayout->addWidget(m_connectButton);
|
toolbarLayout->addWidget(m_connectButton);
|
||||||
|
toolbarLayout->addWidget(m_openLogButton);
|
||||||
|
toolbarLayout->addWidget(m_replayButton);
|
||||||
toolbarLayout->addWidget(m_clearButton);
|
toolbarLayout->addWidget(m_clearButton);
|
||||||
toolbarLayout->addWidget(m_autoFollowCheck);
|
toolbarLayout->addWidget(m_autoFollowCheck);
|
||||||
toolbarLayout->addWidget(zoomOutButton);
|
toolbarLayout->addWidget(zoomOutButton);
|
||||||
@@ -728,7 +1157,25 @@ void MainWindow::buildUi()
|
|||||||
auto *viewTabs = new QTabWidget(this);
|
auto *viewTabs = new QTabWidget(this);
|
||||||
viewTabs->setObjectName(QStringLiteral("mainViewTabs"));
|
viewTabs->setObjectName(QStringLiteral("mainViewTabs"));
|
||||||
viewTabs->addTab(m_map, QStringLiteral("2B Harita"));
|
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);
|
auto *sidePanel = new QWidget(this);
|
||||||
sidePanel->setMinimumWidth(390);
|
sidePanel->setMinimumWidth(390);
|
||||||
@@ -932,6 +1379,9 @@ void MainWindow::buildUi()
|
|||||||
|
|
||||||
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial);
|
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial);
|
||||||
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
|
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
|
||||||
|
connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog);
|
||||||
|
connect(m_replayButton, &QPushButton::clicked, this, &MainWindow::openUartReplay);
|
||||||
|
connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts);
|
||||||
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
|
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||||
&MainWindow::trimToMaxPoints);
|
&MainWindow::trimToMaxPoints);
|
||||||
connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow);
|
connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow);
|
||||||
@@ -965,15 +1415,25 @@ void MainWindow::buildUi()
|
|||||||
void MainWindow::updateConnectionControls()
|
void MainWindow::updateConnectionControls()
|
||||||
{
|
{
|
||||||
const bool open = m_serial->isOpen();
|
const bool open = m_serial->isOpen();
|
||||||
|
const bool replaying = m_replay->isRunning();
|
||||||
m_connectButton->setText(open ? QStringLiteral("Durdur") : QStringLiteral("Dinle"));
|
m_connectButton->setText(open ? QStringLiteral("Durdur") : QStringLiteral("Dinle"));
|
||||||
m_connectButton->setIcon(style()->standardIcon(open ? QStyle::SP_MediaStop : QStyle::SP_MediaPlay));
|
m_connectButton->setIcon(style()->standardIcon(open ? QStyle::SP_MediaStop : QStyle::SP_MediaPlay));
|
||||||
m_portCombo->setEnabled(!open);
|
m_connectButton->setEnabled(!replaying);
|
||||||
m_baudCombo->setEnabled(!open);
|
m_portCombo->setEnabled(!open && !replaying);
|
||||||
|
m_baudCombo->setEnabled(!open && !replaying);
|
||||||
|
m_openLogButton->setEnabled(!replaying);
|
||||||
|
m_replayButton->setText(replaying ? QStringLiteral("Replay Durdur")
|
||||||
|
: QStringLiteral("UART Replay"));
|
||||||
|
m_replayButton->setIcon(style()->standardIcon(
|
||||||
|
replaying ? QStyle::SP_MediaStop : QStyle::SP_MediaPlay));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
|
void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
|
||||||
{
|
{
|
||||||
TelemetryPoint point;
|
TelemetryPoint point;
|
||||||
|
point.source = telemetry.source;
|
||||||
|
point.fusionMode = telemetry.fusionMode;
|
||||||
|
point.gpsAgeMs = telemetry.gpsAgeMs;
|
||||||
point.latitude = *telemetry.latitude;
|
point.latitude = *telemetry.latitude;
|
||||||
point.longitude = *telemetry.longitude;
|
point.longitude = *telemetry.longitude;
|
||||||
point.latitudeText = telemetry.latitudeText;
|
point.latitudeText = telemetry.latitudeText;
|
||||||
@@ -985,8 +1445,21 @@ void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
|
|||||||
point.rawLine = telemetry.rawLine;
|
point.rawLine = telemetry.rawLine;
|
||||||
point.receivedAt = QDateTime::currentDateTimeUtc();
|
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_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()) {
|
if (m_locationTimeoutTimer != nullptr && m_serial->isOpen()) {
|
||||||
m_locationTimeoutTimer->start();
|
m_locationTimeoutTimer->start();
|
||||||
}
|
}
|
||||||
@@ -1028,7 +1501,8 @@ void MainWindow::updateDopLabels()
|
|||||||
|
|
||||||
void MainWindow::updateAnalysis()
|
void MainWindow::updateAnalysis()
|
||||||
{
|
{
|
||||||
const LocationAnalysis analysis = analyzeLocations(m_points);
|
const QVector<TelemetryPoint> analysisPoints = preferredAnalysisPoints(m_points);
|
||||||
|
const LocationAnalysis analysis = analyzeLocations(analysisPoints);
|
||||||
if (!analysis.valid) {
|
if (!analysis.valid) {
|
||||||
m_cepLabel->setText(QStringLiteral("-"));
|
m_cepLabel->setText(QStringLiteral("-"));
|
||||||
m_meanLocationLabel->setText(QStringLiteral("-"));
|
m_meanLocationLabel->setText(QStringLiteral("-"));
|
||||||
@@ -1051,11 +1525,11 @@ void MainWindow::updateAnalysis()
|
|||||||
m_map->setAverageMarker(analysis.averageLat, analysis.averageLon,
|
m_map->setAverageMarker(analysis.averageLat, analysis.averageLon,
|
||||||
analysis.rmsScatterMeters);
|
analysis.rmsScatterMeters);
|
||||||
|
|
||||||
const double cep50 = calculateCep50(m_points, analysis.averageLat,
|
const double cep50 = calculateCep50(analysisPoints, analysis.averageLat,
|
||||||
analysis.averageLon);
|
analysis.averageLon);
|
||||||
m_cepLabel->setText(QStringLiteral("%1 (P50, N=%2)")
|
m_cepLabel->setText(QStringLiteral("%1 (P50, N=%2)")
|
||||||
.arg(metersText(cep50))
|
.arg(metersText(cep50))
|
||||||
.arg(m_points.size()));
|
.arg(analysisPoints.size()));
|
||||||
m_map->setCepMarker(analysis.averageLat, analysis.averageLon, cep50);
|
m_map->setCepMarker(analysis.averageLat, analysis.averageLon, cep50);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1077,7 +1551,14 @@ void MainWindow::mergePendingTelemetry(const ParsedTelemetry &telemetry)
|
|||||||
commitPendingTelemetry();
|
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()) {
|
if (telemetry.latitude.has_value()) {
|
||||||
|
m_pendingTelemetry.source = telemetry.source;
|
||||||
m_pendingTelemetry.latitude = telemetry.latitude;
|
m_pendingTelemetry.latitude = telemetry.latitude;
|
||||||
m_pendingTelemetry.latitudeText = telemetry.latitudeText;
|
m_pendingTelemetry.latitudeText = telemetry.latitudeText;
|
||||||
}
|
}
|
||||||
@@ -1157,5 +1638,5 @@ int MainWindow::selectedBaudRate() const
|
|||||||
{
|
{
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
const int baudRate = m_baudCombo->currentText().toInt(&ok);
|
const int baudRate = m_baudCombo->currentText().toInt(&ok);
|
||||||
return ok ? baudRate : 115200;
|
return ok ? baudRate : 460800;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,18 +18,27 @@ class QPushButton;
|
|||||||
class QSpinBox;
|
class QSpinBox;
|
||||||
class QTableView;
|
class QTableView;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
|
class QCloseEvent;
|
||||||
|
class QFile;
|
||||||
|
|
||||||
class MapWidget;
|
class MapWidget;
|
||||||
class GlobeWidget;
|
class GlobeWidget;
|
||||||
class RinexNavigation;
|
class RinexNavigation;
|
||||||
class SerialReader;
|
class SerialReader;
|
||||||
class TelemetryTableModel;
|
class TelemetryTableModel;
|
||||||
|
class UartReplay;
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
class MainWindow : public QMainWindow {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MainWindow(QWidget *parent = nullptr);
|
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:
|
private slots:
|
||||||
void toggleSerial();
|
void toggleSerial();
|
||||||
@@ -41,6 +50,12 @@ private slots:
|
|||||||
void trimToMaxPoints();
|
void trimToMaxPoints();
|
||||||
void updateReferenceLocation();
|
void updateReferenceLocation();
|
||||||
void flushUiUpdates();
|
void flushUiUpdates();
|
||||||
|
void openTelemetryLog();
|
||||||
|
void openUartReplay();
|
||||||
|
void stopUartReplay();
|
||||||
|
void refreshSerialPorts();
|
||||||
|
bool startUartLogging(const QString &port, int baudRate);
|
||||||
|
void stopUartLogging();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildUi();
|
void buildUi();
|
||||||
@@ -57,9 +72,11 @@ private:
|
|||||||
void scheduleTelemetryRefresh();
|
void scheduleTelemetryRefresh();
|
||||||
void refreshTelemetryViews();
|
void refreshTelemetryViews();
|
||||||
void flushPendingLogLines();
|
void flushPendingLogLines();
|
||||||
|
void appendUartLogLine(const QString &line);
|
||||||
int selectedBaudRate() const;
|
int selectedBaudRate() const;
|
||||||
|
|
||||||
SerialReader *m_serial = nullptr;
|
SerialReader *m_serial = nullptr;
|
||||||
|
UartReplay *m_replay = nullptr;
|
||||||
MapWidget *m_map = nullptr;
|
MapWidget *m_map = nullptr;
|
||||||
GlobeWidget *m_globe = nullptr;
|
GlobeWidget *m_globe = nullptr;
|
||||||
RinexNavigation *m_rinex = nullptr;
|
RinexNavigation *m_rinex = nullptr;
|
||||||
@@ -70,6 +87,9 @@ private:
|
|||||||
QLineEdit *m_referenceLonEdit = nullptr;
|
QLineEdit *m_referenceLonEdit = nullptr;
|
||||||
QPushButton *m_connectButton = nullptr;
|
QPushButton *m_connectButton = nullptr;
|
||||||
QPushButton *m_clearButton = nullptr;
|
QPushButton *m_clearButton = nullptr;
|
||||||
|
QPushButton *m_openLogButton = nullptr;
|
||||||
|
QPushButton *m_replayButton = nullptr;
|
||||||
|
QPushButton *m_refreshRinexButton = nullptr;
|
||||||
QCheckBox *m_autoFollowCheck = nullptr;
|
QCheckBox *m_autoFollowCheck = nullptr;
|
||||||
QCheckBox *m_showTelemetryCheck = nullptr;
|
QCheckBox *m_showTelemetryCheck = nullptr;
|
||||||
QCheckBox *m_showRecentPointsCheck = nullptr;
|
QCheckBox *m_showRecentPointsCheck = nullptr;
|
||||||
@@ -93,6 +113,8 @@ private:
|
|||||||
QPlainTextEdit *m_log = nullptr;
|
QPlainTextEdit *m_log = nullptr;
|
||||||
QTimer *m_locationTimeoutTimer = nullptr;
|
QTimer *m_locationTimeoutTimer = nullptr;
|
||||||
QTimer *m_uiRefreshTimer = nullptr;
|
QTimer *m_uiRefreshTimer = nullptr;
|
||||||
|
QFile *m_uartLogFile = nullptr;
|
||||||
|
QString m_uartLogPath;
|
||||||
|
|
||||||
QVector<TelemetryPoint> m_points;
|
QVector<TelemetryPoint> m_points;
|
||||||
QStringList m_pendingLogLines;
|
QStringList m_pendingLogLines;
|
||||||
|
|||||||
+131
-27
@@ -52,6 +52,20 @@ double metersPerPixel(double latitude, int zoom)
|
|||||||
return latitudeScale * 2.0 * Pi * WebMercatorRadiusMeters / scale;
|
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
|
} // namespace
|
||||||
|
|
||||||
MapWidget::MapWidget(QWidget *parent)
|
MapWidget::MapWidget(QWidget *parent)
|
||||||
@@ -90,9 +104,9 @@ void MapWidget::setPoints(const QVector<TelemetryPoint> &points)
|
|||||||
m_points = points;
|
m_points = points;
|
||||||
|
|
||||||
if (!m_points.isEmpty() && m_autoFollow) {
|
if (!m_points.isEmpty() && m_autoFollow) {
|
||||||
const TelemetryPoint &last = m_points.last();
|
const TelemetryPoint ¤t = m_points[preferredCurrentPointIndex(m_points)];
|
||||||
m_centerLat = last.latitude;
|
m_centerLat = current.latitude;
|
||||||
m_centerLon = last.longitude;
|
m_centerLon = current.longitude;
|
||||||
if (wasEmpty) {
|
if (wasEmpty) {
|
||||||
m_zoom = MaxZoom;
|
m_zoom = MaxZoom;
|
||||||
}
|
}
|
||||||
@@ -108,6 +122,41 @@ void MapWidget::clearPoints()
|
|||||||
update();
|
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)
|
void MapWidget::setAverageMarker(double latitude, double longitude, double radiusMeters)
|
||||||
{
|
{
|
||||||
m_hasAverageMarker = true;
|
m_hasAverageMarker = true;
|
||||||
@@ -156,6 +205,11 @@ void MapWidget::clearCepMarker()
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPointF MapWidget::centerCoordinate() const
|
||||||
|
{
|
||||||
|
return QPointF(m_centerLat, m_centerLon);
|
||||||
|
}
|
||||||
|
|
||||||
void MapWidget::setAutoFollow(bool enabled)
|
void MapWidget::setAutoFollow(bool enabled)
|
||||||
{
|
{
|
||||||
if (m_autoFollow == enabled) {
|
if (m_autoFollow == enabled) {
|
||||||
@@ -164,9 +218,9 @@ void MapWidget::setAutoFollow(bool enabled)
|
|||||||
|
|
||||||
m_autoFollow = enabled;
|
m_autoFollow = enabled;
|
||||||
if (m_autoFollow && !m_points.isEmpty()) {
|
if (m_autoFollow && !m_points.isEmpty()) {
|
||||||
const TelemetryPoint &last = m_points.last();
|
const TelemetryPoint ¤t = m_points[preferredCurrentPointIndex(m_points)];
|
||||||
m_centerLat = last.latitude;
|
m_centerLat = current.latitude;
|
||||||
m_centerLon = last.longitude;
|
m_centerLon = current.longitude;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
emit autoFollowChanged(m_autoFollow);
|
emit autoFollowChanged(m_autoFollow);
|
||||||
@@ -216,6 +270,7 @@ void MapWidget::paintEvent(QPaintEvent *)
|
|||||||
painter.fillRect(rect(), QColor(219, 225, 220));
|
painter.fillRect(rect(), QColor(219, 225, 220));
|
||||||
drawTiles(painter);
|
drawTiles(painter);
|
||||||
drawTelemetry(painter);
|
drawTelemetry(painter);
|
||||||
|
drawLegend(painter);
|
||||||
|
|
||||||
if (m_points.isEmpty()) {
|
if (m_points.isEmpty()) {
|
||||||
drawEmptyState(painter);
|
drawEmptyState(painter);
|
||||||
@@ -387,36 +442,53 @@ void MapWidget::drawTelemetry(QPainter &painter)
|
|||||||
|
|
||||||
drawCepOverlay(painter);
|
drawCepOverlay(painter);
|
||||||
|
|
||||||
QPainterPath path;
|
const auto drawSourcePath = [this, &painter](LocationSource source, const QColor &color) {
|
||||||
for (int i = 0; i < m_points.size(); ++i) {
|
QPainterPath path;
|
||||||
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
|
bool havePoint = false;
|
||||||
if (i == 0) {
|
int pointCount = 0;
|
||||||
path.moveTo(screen);
|
for (const TelemetryPoint &point : m_points) {
|
||||||
} else {
|
if (point.source != source) {
|
||||||
path.lineTo(screen);
|
continue;
|
||||||
|
}
|
||||||
|
const QPointF screen = coordinateToScreen(point.latitude, point.longitude);
|
||||||
|
havePoint ? path.lineTo(screen) : path.moveTo(screen);
|
||||||
|
havePoint = true;
|
||||||
|
++pointCount;
|
||||||
}
|
}
|
||||||
}
|
if (m_showRecentPoints && pointCount > 1) {
|
||||||
|
QPen pathPen(color, 3.0);
|
||||||
if (m_showRecentPoints && m_points.size() > 1) {
|
pathPen.setCapStyle(Qt::RoundCap);
|
||||||
QPen pathPen(QColor(210, 105, 30), 3.0);
|
pathPen.setJoinStyle(Qt::RoundJoin);
|
||||||
pathPen.setCapStyle(Qt::RoundCap);
|
painter.setPen(pathPen);
|
||||||
pathPen.setJoinStyle(Qt::RoundJoin);
|
painter.drawPath(path);
|
||||||
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);
|
drawAverageOverlay(painter);
|
||||||
drawCepMarker(painter);
|
drawCepMarker(painter);
|
||||||
|
|
||||||
const bool drawNumbers = m_points.size() <= 150;
|
const bool drawNumbers = m_points.size() <= 150;
|
||||||
const int firstPoint = m_showRecentPoints ? 0 : m_points.size() - 1;
|
const int currentPointIndex = preferredCurrentPointIndex(m_points);
|
||||||
for (int i = firstPoint; i < m_points.size(); ++i) {
|
const int firstPoint = m_showRecentPoints ? 0 : currentPointIndex;
|
||||||
const bool isLast = i == m_points.size() - 1;
|
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 QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
|
||||||
const double radius = isLast ? 8.0 : 5.0;
|
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.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);
|
painter.drawEllipse(screen, radius, radius);
|
||||||
|
|
||||||
if (drawNumbers) {
|
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);
|
const QPointF lastScreen = coordinateToScreen(last.latitude, last.longitude);
|
||||||
QStringList lines;
|
QStringList lines;
|
||||||
const QString latitudeText = last.latitudeText.isEmpty()
|
const QString latitudeText = last.latitudeText.isEmpty()
|
||||||
@@ -436,6 +508,7 @@ void MapWidget::drawTelemetry(QPainter &painter)
|
|||||||
? QString::number(last.longitude, 'f', 15) : last.longitudeText;
|
? QString::number(last.longitude, 'f', 15) : last.longitudeText;
|
||||||
lines << QStringLiteral("Lat: %1").arg(latitudeText);
|
lines << QStringLiteral("Lat: %1").arg(latitudeText);
|
||||||
lines << QStringLiteral("Long: %1").arg(longitudeText);
|
lines << QStringLiteral("Long: %1").arg(longitudeText);
|
||||||
|
lines << QStringLiteral("Kaynak: %1").arg(locationSourceText(last.source));
|
||||||
if (last.hheight.has_value()) {
|
if (last.hheight.has_value()) {
|
||||||
lines << QStringLiteral("HHeight: %1").arg(*last.hheight, 0, 'f', 2);
|
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);
|
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)
|
void MapWidget::drawCepOverlay(QPainter &painter)
|
||||||
{
|
{
|
||||||
if (!m_hasCepMarker || !m_cepRadiusMeters.has_value() || *m_cepRadiusMeters <= 0.0) {
|
if (!m_hasCepMarker || !m_cepRadiusMeters.has_value() || *m_cepRadiusMeters <= 0.0) {
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ public:
|
|||||||
explicit MapWidget(QWidget *parent = nullptr);
|
explicit MapWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
void setPoints(const QVector<TelemetryPoint> &points);
|
void setPoints(const QVector<TelemetryPoint> &points);
|
||||||
|
void fitToPoints();
|
||||||
void clearPoints();
|
void clearPoints();
|
||||||
void setAverageMarker(double latitude, double longitude, double radiusMeters);
|
void setAverageMarker(double latitude, double longitude, double radiusMeters);
|
||||||
void clearAverageMarker();
|
void clearAverageMarker();
|
||||||
void setCepMarker(double latitude, double longitude,
|
void setCepMarker(double latitude, double longitude,
|
||||||
const std::optional<double> &radiusMeters);
|
const std::optional<double> &radiusMeters);
|
||||||
void clearCepMarker();
|
void clearCepMarker();
|
||||||
|
[[nodiscard]] QPointF centerCoordinate() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void setAutoFollow(bool enabled);
|
void setAutoFollow(bool enabled);
|
||||||
@@ -56,6 +58,7 @@ private:
|
|||||||
|
|
||||||
void drawTiles(QPainter &painter);
|
void drawTiles(QPainter &painter);
|
||||||
void drawTelemetry(QPainter &painter);
|
void drawTelemetry(QPainter &painter);
|
||||||
|
void drawLegend(QPainter &painter);
|
||||||
void drawAverageOverlay(QPainter &painter);
|
void drawAverageOverlay(QPainter &painter);
|
||||||
void drawCepOverlay(QPainter &painter);
|
void drawCepOverlay(QPainter &painter);
|
||||||
void drawCepMarker(QPainter &painter);
|
void drawCepMarker(QPainter &painter);
|
||||||
|
|||||||
+135
-100
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QHash>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -22,29 +24,6 @@ constexpr double HalfGpsWeek = GpsWeekSeconds / 2.0;
|
|||||||
constexpr double CurrentGpsUtcOffsetSeconds = 18.0;
|
constexpr double CurrentGpsUtcOffsetSeconds = 18.0;
|
||||||
constexpr int PositionUpdateIntervalMs = 60000;
|
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)
|
double parseField(const QString &line, int field)
|
||||||
{
|
{
|
||||||
QString text = line.mid(4 + field * 19, 19).trimmed();
|
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);
|
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
|
} // namespace
|
||||||
|
|
||||||
RinexNavigation::RinexNavigation(QObject *parent)
|
RinexNavigation::RinexNavigation(QObject *parent)
|
||||||
@@ -150,10 +86,10 @@ RinexNavigation::RinexNavigation(QObject *parent)
|
|||||||
void RinexNavigation::refresh()
|
void RinexNavigation::refresh()
|
||||||
{
|
{
|
||||||
if (m_loading) return;
|
if (m_loading) return;
|
||||||
if (m_latestRinex.isEmpty()) {
|
if (m_ephemerides.isEmpty()) {
|
||||||
loadCache();
|
loadCache();
|
||||||
}
|
}
|
||||||
m_loading = true;
|
setLoading(true);
|
||||||
emit statusChanged(QStringLiteral("Guncel RINEX efemerisi indiriliyor..."), false);
|
emit statusChanged(QStringLiteral("Guncel RINEX efemerisi indiriliyor..."), false);
|
||||||
requestDay(QDateTime::currentDateTimeUtc().date(), 0);
|
requestDay(QDateTime::currentDateTimeUtc().date(), 0);
|
||||||
}
|
}
|
||||||
@@ -181,8 +117,13 @@ void RinexNavigation::requestDay(const QDate &date, int fallbackIndex)
|
|||||||
if (fallbackIndex < 2) {
|
if (fallbackIndex < 2) {
|
||||||
requestDay(date.addDays(-1), fallbackIndex + 1);
|
requestDay(date.addDays(-1), fallbackIndex + 1);
|
||||||
} else {
|
} else {
|
||||||
m_loading = false;
|
setLoading(false);
|
||||||
if (!loadCache()) {
|
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);
|
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);
|
const QByteArray rinex = gunzip(compressed);
|
||||||
QString error;
|
QString error;
|
||||||
const QVector<SatellitePosition> positions = parseGpsNavigation(
|
QVector<Ephemeris> ephemerides = parseEphemerides(rinex, &error);
|
||||||
rinex, QDateTime::currentDateTimeUtc(), &error);
|
const QDateTime now = QDateTime::currentDateTimeUtc();
|
||||||
|
const QVector<SatellitePosition> positions = propagateEphemerides(
|
||||||
|
selectEphemerides(ephemerides, now), now);
|
||||||
if (positions.isEmpty()) {
|
if (positions.isEmpty()) {
|
||||||
if (fallbackIndex < 2) {
|
if (fallbackIndex < 2) {
|
||||||
requestDay(date.addDays(-1), fallbackIndex + 1);
|
requestDay(date.addDays(-1), fallbackIndex + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_loading = false;
|
setLoading(false);
|
||||||
emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true);
|
emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -212,11 +155,14 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &
|
|||||||
QFile cache(cachePath());
|
QFile cache(cachePath());
|
||||||
QDir().mkpath(QFileInfo(cache).absolutePath());
|
QDir().mkpath(QFileInfo(cache).absolutePath());
|
||||||
if (cache.open(QIODevice::WriteOnly)) cache.write(rinex);
|
if (cache.open(QIODevice::WriteOnly)) cache.write(rinex);
|
||||||
m_latestRinex = rinex;
|
m_ephemerides = std::move(ephemerides);
|
||||||
m_loading = false;
|
setLoading(false);
|
||||||
publishCurrentMotion();
|
publishCurrentMotion();
|
||||||
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 (UTC)")
|
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 · %3")
|
||||||
.arg(positions.size()).arg(date.toString(QStringLiteral("dd.MM.yyyy"))), false);
|
.arg(positions.size())
|
||||||
|
.arg(date.toString(QStringLiteral("dd.MM.yyyy")))
|
||||||
|
.arg(now.toLocalTime().toString(
|
||||||
|
QStringLiteral("HH:mm:ss t"))), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RinexNavigation::loadCache()
|
bool RinexNavigation::loadCache()
|
||||||
@@ -224,12 +170,17 @@ bool RinexNavigation::loadCache()
|
|||||||
QFile cache(cachePath());
|
QFile cache(cachePath());
|
||||||
if (!cache.open(QIODevice::ReadOnly)) return false;
|
if (!cache.open(QIODevice::ReadOnly)) return false;
|
||||||
QString error;
|
QString error;
|
||||||
m_latestRinex = cache.readAll();
|
QVector<Ephemeris> ephemerides = parseEphemerides(cache.readAll(), &error);
|
||||||
const QVector<SatellitePosition> positions = parseGpsNavigation(
|
const QDateTime now = QDateTime::currentDateTimeUtc();
|
||||||
m_latestRinex, QDateTime::currentDateTimeUtc(), &error);
|
const QVector<SatellitePosition> positions = propagateEphemerides(
|
||||||
|
selectEphemerides(ephemerides, now), now);
|
||||||
if (positions.isEmpty()) return false;
|
if (positions.isEmpty()) return false;
|
||||||
|
m_ephemerides = std::move(ephemerides);
|
||||||
publishCurrentMotion();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,28 +192,36 @@ QString RinexNavigation::cachePath() const
|
|||||||
|
|
||||||
void RinexNavigation::updateCurrentPositions()
|
void RinexNavigation::updateCurrentPositions()
|
||||||
{
|
{
|
||||||
if (m_latestRinex.isEmpty()) return;
|
if (m_ephemerides.isEmpty()) return;
|
||||||
publishCurrentMotion();
|
publishCurrentMotion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RinexNavigation::publishCurrentMotion()
|
void RinexNavigation::publishCurrentMotion()
|
||||||
{
|
{
|
||||||
if (m_latestRinex.isEmpty()) return;
|
if (m_ephemerides.isEmpty()) return;
|
||||||
|
|
||||||
const QDateTime now = QDateTime::currentDateTimeUtc();
|
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;
|
if (current.isEmpty()) return;
|
||||||
|
|
||||||
const QVector<SatellitePosition> next = parseGpsNavigation(
|
const QVector<SatellitePosition> next = propagateEphemerides(
|
||||||
m_latestRinex, now.addMSecs(PositionUpdateIntervalMs));
|
selected, now.addMSecs(PositionUpdateIntervalMs));
|
||||||
emit satellitesUpdated(current);
|
emit satellitesUpdated(current);
|
||||||
if (!next.isEmpty()) {
|
if (!next.isEmpty()) {
|
||||||
emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs);
|
emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
|
void RinexNavigation::setLoading(bool loading)
|
||||||
const QByteArray &rinex, const QDateTime &whenUtc, QString *error)
|
{
|
||||||
|
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 QString text = QString::fromLatin1(rinex);
|
||||||
const QStringList lines = text.split('\n');
|
const QStringList lines = text.split('\n');
|
||||||
@@ -274,8 +233,8 @@ QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
|
|||||||
}
|
}
|
||||||
++index;
|
++index;
|
||||||
|
|
||||||
QHash<QString, Ephemeris> closest;
|
QVector<Ephemeris> result;
|
||||||
QHash<QString, qint64> closestAge;
|
result.reserve(512);
|
||||||
while (index < lines.size()) {
|
while (index < lines.size()) {
|
||||||
const QString first = lines[index];
|
const QString first = lines[index];
|
||||||
if (first.size() < 3) { ++index; continue; }
|
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.cus, eph.sqrtA, eph.toe, eph.cic, eph.omega0, eph.cis,
|
||||||
eph.i0, eph.crc, eph.omega, eph.omegaDot, eph.idot})
|
eph.i0, eph.crc, eph.omega, eph.omegaDot, eph.idot})
|
||||||
&& eph.sqrtA > 4000.0 && eph.gpsWeek > 0) {
|
&& eph.sqrtA > 4000.0 && eph.gpsWeek > 0) {
|
||||||
const qint64 age = std::abs(toc.secsTo(whenUtc));
|
result.push_back(eph);
|
||||||
if (!closestAge.contains(eph.prn) || age < closestAge[eph.prn]) {
|
|
||||||
closest[eph.prn] = eph;
|
|
||||||
closestAge[eph.prn] = age;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
index += 8;
|
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");
|
if (result.isEmpty() && error) *error = QStringLiteral("GPS efemerisi bulunamadi");
|
||||||
return result;
|
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
@@ -26,17 +26,49 @@ signals:
|
|||||||
const QVector<SatellitePosition> &next,
|
const QVector<SatellitePosition> &next,
|
||||||
int durationMs);
|
int durationMs);
|
||||||
void statusChanged(const QString &status, bool error);
|
void statusChanged(const QString &status, bool error);
|
||||||
|
void loadingChanged(bool loading);
|
||||||
|
|
||||||
private:
|
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 requestDay(const QDate &date, int fallbackIndex);
|
||||||
void handleDownload(const QByteArray &compressed, const QDate &date, int fallbackIndex);
|
void handleDownload(const QByteArray &compressed, const QDate &date, int fallbackIndex);
|
||||||
bool loadCache();
|
bool loadCache();
|
||||||
QString cachePath() const;
|
QString cachePath() const;
|
||||||
void updateCurrentPositions();
|
void updateCurrentPositions();
|
||||||
void publishCurrentMotion();
|
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;
|
QNetworkAccessManager m_network;
|
||||||
QTimer m_positionTimer;
|
QTimer m_positionTimer;
|
||||||
QByteArray m_latestRinex;
|
QVector<Ephemeris> m_ephemerides;
|
||||||
bool m_loading = false;
|
bool m_loading = false;
|
||||||
};
|
};
|
||||||
|
|||||||
+57
-21
@@ -10,38 +10,70 @@
|
|||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
#include <IOKit/serial/ioss.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr qsizetype MaxBytesPerReadCycle = 64 * 1024;
|
constexpr qsizetype MaxBytesPerReadCycle = 64 * 1024;
|
||||||
|
|
||||||
speed_t baudToSpeed(int baudRate)
|
bool applyBaudRate(termios &options, int baudRate)
|
||||||
{
|
{
|
||||||
|
speed_t speed = 0;
|
||||||
switch (baudRate) {
|
switch (baudRate) {
|
||||||
case 1200:
|
case 1200:
|
||||||
return B1200;
|
speed = B1200;
|
||||||
|
break;
|
||||||
case 2400:
|
case 2400:
|
||||||
return B2400;
|
speed = B2400;
|
||||||
|
break;
|
||||||
case 4800:
|
case 4800:
|
||||||
return B4800;
|
speed = B4800;
|
||||||
|
break;
|
||||||
case 9600:
|
case 9600:
|
||||||
return B9600;
|
speed = B9600;
|
||||||
|
break;
|
||||||
case 19200:
|
case 19200:
|
||||||
return B19200;
|
speed = B19200;
|
||||||
|
break;
|
||||||
case 38400:
|
case 38400:
|
||||||
return B38400;
|
speed = B38400;
|
||||||
|
break;
|
||||||
case 57600:
|
case 57600:
|
||||||
return B57600;
|
speed = B57600;
|
||||||
|
break;
|
||||||
case 115200:
|
case 115200:
|
||||||
return B115200;
|
speed = B115200;
|
||||||
|
break;
|
||||||
|
#ifdef B230400
|
||||||
case 230400:
|
case 230400:
|
||||||
return B230400;
|
speed = B230400;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
#ifdef B460800
|
||||||
case 460800:
|
case 460800:
|
||||||
return B460800;
|
speed = B460800;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
#ifdef B921600
|
||||||
case 921600:
|
case 921600:
|
||||||
return B921600;
|
speed = B921600;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
default:
|
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
|
} // namespace
|
||||||
@@ -176,12 +208,6 @@ void SerialReader::flushPartialLine()
|
|||||||
|
|
||||||
bool SerialReader::configurePort(int baudRate)
|
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 {};
|
termios options {};
|
||||||
if (::tcgetattr(m_fd, &options) != 0) {
|
if (::tcgetattr(m_fd, &options) != 0) {
|
||||||
emitSystemError(QStringLiteral("Port ayarlari okunamadi"));
|
emitSystemError(QStringLiteral("Port ayarlari okunamadi"));
|
||||||
@@ -189,8 +215,10 @@ bool SerialReader::configurePort(int baudRate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
::cfmakeraw(&options);
|
::cfmakeraw(&options);
|
||||||
::cfsetispeed(&options, speed);
|
if (!applyBaudRate(options, baudRate)) {
|
||||||
::cfsetospeed(&options, speed);
|
emit errorOccurred(QStringLiteral("Bu sistemde desteklenmeyen baud rate: %1").arg(baudRate));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
options.c_cflag |= CLOCAL | CREAD;
|
options.c_cflag |= CLOCAL | CREAD;
|
||||||
options.c_cflag &= ~HUPCL;
|
options.c_cflag &= ~HUPCL;
|
||||||
@@ -206,6 +234,14 @@ bool SerialReader::configurePort(int baudRate)
|
|||||||
return false;
|
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);
|
::tcflush(m_fd, TCIFLUSH);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,34 @@
|
|||||||
|
|
||||||
#include <optional>
|
#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 {
|
struct DopValues {
|
||||||
std::optional<double> gdop;
|
std::optional<double> gdop;
|
||||||
std::optional<double> pdop;
|
std::optional<double> pdop;
|
||||||
@@ -20,6 +48,9 @@ struct DopValues {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct TelemetryPoint {
|
struct TelemetryPoint {
|
||||||
|
LocationSource source = LocationSource::Generic;
|
||||||
|
FusionMode fusionMode = FusionMode::Unknown;
|
||||||
|
std::optional<qint64> gpsAgeMs;
|
||||||
double latitude = 0.0;
|
double latitude = 0.0;
|
||||||
double longitude = 0.0;
|
double longitude = 0.0;
|
||||||
QString latitudeText;
|
QString latitudeText;
|
||||||
@@ -32,6 +63,9 @@ struct TelemetryPoint {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct ParsedTelemetry {
|
struct ParsedTelemetry {
|
||||||
|
LocationSource source = LocationSource::Generic;
|
||||||
|
FusionMode fusionMode = FusionMode::Unknown;
|
||||||
|
std::optional<qint64> gpsAgeMs;
|
||||||
std::optional<double> latitude;
|
std::optional<double> latitude;
|
||||||
std::optional<double> longitude;
|
std::optional<double> longitude;
|
||||||
QString latitudeText;
|
QString latitudeText;
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
#include "UartReplay.h"
|
||||||
|
|
||||||
|
#include "LocationParser.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
UartReplay::UartReplay(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_timer(new QTimer(this))
|
||||||
|
{
|
||||||
|
m_timer->setSingleShot(true);
|
||||||
|
connect(m_timer, &QTimer::timeout, this, &UartReplay::emitNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UartReplay::isRunning() const
|
||||||
|
{
|
||||||
|
return m_running;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UartReplay::locationCount() const
|
||||||
|
{
|
||||||
|
return m_locationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UartReplay::start(const QString &filePath, int gpsIntervalMs, int sensorIntervalMs)
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
|
||||||
|
if (gpsIntervalMs < 1 || sensorIntervalMs < 1) {
|
||||||
|
emit errorOccurred(QStringLiteral("Replay araliklari en az 1 ms olmalidir"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
emit errorOccurred(QStringLiteral("UART replay logu acilamadi: %1")
|
||||||
|
.arg(file.errorString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 nextGpsMs = 0;
|
||||||
|
qint64 nextSensorMs = 0;
|
||||||
|
qint64 lastDueMs = 0;
|
||||||
|
bool pendingLatitude = false;
|
||||||
|
bool pendingLongitude = false;
|
||||||
|
LocationSource pendingSource = LocationSource::Generic;
|
||||||
|
const QList<QByteArray> rawLines = file.readAll().split('\n');
|
||||||
|
m_lines.reserve(rawLines.size());
|
||||||
|
|
||||||
|
for (const QByteArray &rawLine : rawLines) {
|
||||||
|
const QString line = removeRecorderTimestamp(QString::fromUtf8(rawLine));
|
||||||
|
if (line.isEmpty() || line.startsWith(QLatin1Char('#'))
|
||||||
|
|| line.startsWith(QStringLiteral("[SESSION START]"))
|
||||||
|
|| line.startsWith(QStringLiteral("[SESSION END]"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream stream = classifyLine(line);
|
||||||
|
const std::optional<ParsedTelemetry> telemetry = LocationParser::parseLine(line);
|
||||||
|
if (telemetry.has_value() && telemetry->hasLocation()) {
|
||||||
|
pendingLatitude = false;
|
||||||
|
pendingLongitude = false;
|
||||||
|
pendingSource = LocationSource::Generic;
|
||||||
|
} else if (telemetry.has_value()) {
|
||||||
|
if (telemetry->latitude.has_value()) {
|
||||||
|
if (pendingLatitude) {
|
||||||
|
pendingLongitude = false;
|
||||||
|
}
|
||||||
|
pendingLatitude = true;
|
||||||
|
pendingSource = telemetry->source;
|
||||||
|
}
|
||||||
|
if (telemetry->longitude.has_value()) {
|
||||||
|
pendingLongitude = true;
|
||||||
|
if (telemetry->source != LocationSource::Generic) {
|
||||||
|
pendingSource = telemetry->source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pendingLatitude && pendingLongitude) {
|
||||||
|
stream = pendingSource == LocationSource::ImuCorrected
|
||||||
|
|| pendingSource == LocationSource::Trajectory
|
||||||
|
? Stream::Sensor : Stream::Gps;
|
||||||
|
pendingLatitude = false;
|
||||||
|
pendingLongitude = false;
|
||||||
|
pendingSource = LocationSource::Generic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qint64 dueMs = lastDueMs;
|
||||||
|
if (stream == Stream::Gps) {
|
||||||
|
dueMs = std::max(lastDueMs, nextGpsMs);
|
||||||
|
nextGpsMs = dueMs + gpsIntervalMs;
|
||||||
|
++m_locationCount;
|
||||||
|
} else if (stream == Stream::Sensor) {
|
||||||
|
dueMs = std::max(lastDueMs, nextSensorMs);
|
||||||
|
nextSensorMs = dueMs + sensorIntervalMs;
|
||||||
|
++m_locationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplayLine replayLine;
|
||||||
|
replayLine.text = line;
|
||||||
|
replayLine.delayBeforeMs = static_cast<int>(std::min<qint64>(
|
||||||
|
dueMs - lastDueMs, std::numeric_limits<int>::max()));
|
||||||
|
m_lines.push_back(std::move(replayLine));
|
||||||
|
lastDueMs = dueMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_locationCount == 0) {
|
||||||
|
m_lines.clear();
|
||||||
|
emit errorOccurred(QStringLiteral(
|
||||||
|
"UART replay logunda GPS veya fused konum kaydi bulunamadi"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_running = true;
|
||||||
|
emit replayStarted(m_lines.size(), m_locationCount);
|
||||||
|
m_timer->start(0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UartReplay::stop()
|
||||||
|
{
|
||||||
|
if (!m_running && m_lines.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_timer->stop();
|
||||||
|
m_running = false;
|
||||||
|
m_lines.clear();
|
||||||
|
m_index = 0;
|
||||||
|
m_locationCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UartReplay::emitNext()
|
||||||
|
{
|
||||||
|
if (!m_running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int MaxImmediateLines = 250;
|
||||||
|
int emitted = 0;
|
||||||
|
while (m_index < m_lines.size() && emitted < MaxImmediateLines) {
|
||||||
|
const ReplayLine &line = m_lines.at(m_index);
|
||||||
|
if (line.delayBeforeMs > 0) {
|
||||||
|
if (emitted > 0) {
|
||||||
|
emit progressChanged(m_index, m_lines.size());
|
||||||
|
}
|
||||||
|
const int delay = line.delayBeforeMs;
|
||||||
|
m_lines[m_index].delayBeforeMs = 0;
|
||||||
|
m_timer->start(delay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit lineReady(line.text);
|
||||||
|
++m_index;
|
||||||
|
++emitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit progressChanged(m_index, m_lines.size());
|
||||||
|
if (m_index >= m_lines.size()) {
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
m_timer->start(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString UartReplay::removeRecorderTimestamp(const QString &line)
|
||||||
|
{
|
||||||
|
QString result = line.trimmed();
|
||||||
|
static const QRegularExpression timestampPrefix(
|
||||||
|
QStringLiteral(R"(^\[\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[\.,]\d+)?\]\s*)"));
|
||||||
|
result.remove(timestampPrefix);
|
||||||
|
return result.trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
UartReplay::Stream UartReplay::classifyLine(const QString &line)
|
||||||
|
{
|
||||||
|
const std::optional<ParsedTelemetry> telemetry = LocationParser::parseLine(line);
|
||||||
|
if (!telemetry.has_value() || !telemetry->hasLocation()) {
|
||||||
|
return Stream::Diagnostic;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (telemetry->source) {
|
||||||
|
case LocationSource::Gps:
|
||||||
|
return Stream::Gps;
|
||||||
|
case LocationSource::ImuCorrected:
|
||||||
|
case LocationSource::Trajectory:
|
||||||
|
return Stream::Sensor;
|
||||||
|
case LocationSource::Generic:
|
||||||
|
default:
|
||||||
|
// Older firmware used a [FUSION] record without an explicit source.
|
||||||
|
return line.startsWith(QStringLiteral("[FUSION]"), Qt::CaseInsensitive)
|
||||||
|
? Stream::Sensor : Stream::Gps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UartReplay::finish()
|
||||||
|
{
|
||||||
|
m_timer->stop();
|
||||||
|
m_running = false;
|
||||||
|
m_lines.clear();
|
||||||
|
m_index = 0;
|
||||||
|
m_locationCount = 0;
|
||||||
|
emit replayFinished();
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
class QTimer;
|
||||||
|
|
||||||
|
class UartReplay final : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit UartReplay(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] bool isRunning() const;
|
||||||
|
[[nodiscard]] int locationCount() const;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
bool start(const QString &filePath, int gpsIntervalMs = 200,
|
||||||
|
int sensorIntervalMs = 50);
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void lineReady(const QString &line);
|
||||||
|
void replayStarted(int lineCount, int locationCount);
|
||||||
|
void progressChanged(int linesProcessed, int lineCount);
|
||||||
|
void replayFinished();
|
||||||
|
void errorOccurred(const QString &message);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void emitNext();
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class Stream {
|
||||||
|
Diagnostic,
|
||||||
|
Gps,
|
||||||
|
Sensor
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReplayLine {
|
||||||
|
QString text;
|
||||||
|
int delayBeforeMs = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
static QString removeRecorderTimestamp(const QString &line);
|
||||||
|
static Stream classifyLine(const QString &line);
|
||||||
|
void finish();
|
||||||
|
|
||||||
|
QTimer *m_timer = nullptr;
|
||||||
|
QVector<ReplayLine> m_lines;
|
||||||
|
int m_index = 0;
|
||||||
|
int m_locationCount = 0;
|
||||||
|
bool m_running = false;
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
@@ -9,6 +10,9 @@ int main(int argc, char *argv[])
|
|||||||
QApplication::setOrganizationName(QStringLiteral("rocket"));
|
QApplication::setOrganizationName(QStringLiteral("rocket"));
|
||||||
|
|
||||||
MainWindow window;
|
MainWindow window;
|
||||||
|
if (argc == 3 && QString::fromLocal8Bit(argv[1]) == QStringLiteral("--replay")) {
|
||||||
|
window.loadTelemetryLog(QFileInfo(QString::fromLocal8Bit(argv[2])).absoluteFilePath());
|
||||||
|
}
|
||||||
window.show();
|
window.show();
|
||||||
|
|
||||||
return QApplication::exec();
|
return QApplication::exec();
|
||||||
|
|||||||
+112
-4
@@ -1,17 +1,24 @@
|
|||||||
#include "../src/MainWindow.h"
|
#include "../src/MainWindow.h"
|
||||||
|
#include "../src/GlobeWidget.h"
|
||||||
#include "../src/MapWidget.h"
|
#include "../src/MapWidget.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QDoubleSpinBox>
|
#include <QDoubleSpinBox>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QMetaObject>
|
#include <QMetaObject>
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QTableView>
|
#include <QTableView>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
|
#include <QTemporaryFile>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
@@ -36,7 +43,35 @@ void flushUpdates(MainWindow &window)
|
|||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
|
qputenv("LOCATION_PLOTTER_PORT", "/dev/test-usb-uart");
|
||||||
MainWindow window;
|
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 *table = window.findChild<QTableView *>();
|
||||||
auto *referenceLatInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLatInput"));
|
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 *showRecentPoints = window.findChild<QCheckBox *>(QStringLiteral("showRecentPointsCheck"));
|
||||||
auto *showAverage = window.findChild<QCheckBox *>(QStringLiteral("showAverageCheck"));
|
auto *showAverage = window.findChild<QCheckBox *>(QStringLiteral("showAverageCheck"));
|
||||||
auto *portSelector = window.findChild<QComboBox *>(QStringLiteral("portSelector"));
|
auto *portSelector = window.findChild<QComboBox *>(QStringLiteral("portSelector"));
|
||||||
|
auto *refreshRinexButton =
|
||||||
|
window.findChild<QPushButton *>(QStringLiteral("refreshRinexButton"));
|
||||||
auto *map = window.findChild<MapWidget *>();
|
auto *map = window.findChild<MapWidget *>();
|
||||||
auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs"));
|
auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs"));
|
||||||
auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter"));
|
auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter"));
|
||||||
@@ -66,10 +103,14 @@ int main(int argc, char **argv)
|
|||||||
assert(showRecentPoints != nullptr);
|
assert(showRecentPoints != nullptr);
|
||||||
assert(showAverage != nullptr);
|
assert(showAverage != nullptr);
|
||||||
assert(portSelector != 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->count() >= 1);
|
||||||
|
assert(portSelector->currentText() == QStringLiteral("/dev/test-usb-uart"));
|
||||||
for (int i = 0; i < portSelector->count(); ++i) {
|
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(!portSelector->itemText(i).trimmed().isEmpty());
|
||||||
}
|
}
|
||||||
assert(map != nullptr);
|
assert(map != nullptr);
|
||||||
@@ -78,6 +119,15 @@ int main(int argc, char **argv)
|
|||||||
assert(sideSplitter != nullptr);
|
assert(sideSplitter != nullptr);
|
||||||
assert(sideSplitter->orientation() == Qt::Vertical);
|
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,
|
assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection,
|
||||||
Q_ARG(double, 40.25), Q_ARG(double, 29.5)));
|
Q_ARG(double, 40.25), Q_ARG(double, 29.5)));
|
||||||
assert(referenceLatInput->text() == QStringLiteral("40.250000000000000"));
|
assert(referenceLatInput->text() == QStringLiteral("40.250000000000000"));
|
||||||
@@ -99,11 +149,48 @@ int main(int argc, char **argv)
|
|||||||
sendLine(window, QStringLiteral("NAV solve done"));
|
sendLine(window, QStringLiteral("NAV solve done"));
|
||||||
flushUpdates(window);
|
flushUpdates(window);
|
||||||
assert(table->model()->rowCount() == 1);
|
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()
|
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"));
|
== 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"));
|
referenceLatInput->setText(QStringLiteral("40.78925034214206846173"));
|
||||||
referenceLonInput->setText(QStringLiteral("29.46277530841727809729"));
|
referenceLonInput->setText(QStringLiteral("29.46277530841727809729"));
|
||||||
const bool referenceUpdated = QMetaObject::invokeMethod(
|
const bool referenceUpdated = QMetaObject::invokeMethod(
|
||||||
@@ -144,6 +231,27 @@ int main(int argc, char **argv)
|
|||||||
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
|
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
|
||||||
maxPointsInput->setValue(10);
|
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");
|
const QByteArray screenshotPath = qgetenv("LOCATION_PLOTTER_TEST_SCREENSHOT");
|
||||||
if (!screenshotPath.isEmpty()) {
|
if (!screenshotPath.isEmpty()) {
|
||||||
sendLine(window, QStringLiteral("NAV solve start sats:5"));
|
sendLine(window, QStringLiteral("NAV solve start sats:5"));
|
||||||
|
|||||||
+120
-2
@@ -9,9 +9,39 @@
|
|||||||
|
|
||||||
namespace {
|
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
|
} // namespace
|
||||||
@@ -156,6 +186,25 @@ int main(int argc, char **argv)
|
|||||||
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G32")}));
|
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(
|
const auto parsed = LocationParser::parseLine(QStringLiteral(
|
||||||
"[OK] [NAV] DOP GDOP=7.0907891622 PDOP=5.6333862175 "
|
"[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));
|
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");
|
const QString rinexPath = qEnvironmentVariable("LOCATION_PLOTTER_TEST_RINEX");
|
||||||
if (!rinexPath.isEmpty()) {
|
if (!rinexPath.isEmpty()) {
|
||||||
QFile file(rinexPath);
|
QFile file(rinexPath);
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#include "../src/UartReplay.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QEventLoop>
|
||||||
|
#include <QTemporaryFile>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
|
||||||
|
QTemporaryFile log;
|
||||||
|
assert(log.open());
|
||||||
|
log.write(
|
||||||
|
"[2026-08-12 10:00:00.000] [SESSION START] port=/dev/test baud=460800\n"
|
||||||
|
"[2026-08-12 10:00:00.001] [LOCATION] source=gps lat=40.0 lon=29.0\n"
|
||||||
|
"diagnostic line without timestamp\n"
|
||||||
|
"[LOCATION] source=imu_corrected lat=40.1 lon=29.1\n"
|
||||||
|
"[FUSION] lat=40.2 lon=29.2 height=10\n"
|
||||||
|
"[LOCATION] source=gps lat=40.3 lon=29.3\n"
|
||||||
|
"[2026-08-12 10:00:00.999] [SESSION END]\n");
|
||||||
|
log.flush();
|
||||||
|
|
||||||
|
UartReplay replay;
|
||||||
|
QStringList received;
|
||||||
|
QVector<qint64> receivedAt;
|
||||||
|
QElapsedTimer elapsed;
|
||||||
|
QEventLoop loop;
|
||||||
|
QObject::connect(&replay, &UartReplay::lineReady, &loop, [&](const QString &line) {
|
||||||
|
received.push_back(line);
|
||||||
|
receivedAt.push_back(elapsed.elapsed());
|
||||||
|
});
|
||||||
|
QObject::connect(&replay, &UartReplay::replayFinished, &loop, &QEventLoop::quit);
|
||||||
|
QTimer::singleShot(2000, &loop, &QEventLoop::quit);
|
||||||
|
|
||||||
|
elapsed.start();
|
||||||
|
assert(replay.start(log.fileName(), 20, 10));
|
||||||
|
assert(replay.locationCount() == 4);
|
||||||
|
loop.exec();
|
||||||
|
|
||||||
|
assert(!replay.isRunning());
|
||||||
|
assert(received.size() == 5);
|
||||||
|
assert(received.first().startsWith(QStringLiteral("[LOCATION] source=gps")));
|
||||||
|
assert(received.at(1) == QStringLiteral("diagnostic line without timestamp"));
|
||||||
|
assert(received.at(3).startsWith(QStringLiteral("[FUSION]")));
|
||||||
|
assert(receivedAt.last() >= 15);
|
||||||
|
|
||||||
|
QTemporaryFile splitLog;
|
||||||
|
assert(splitLog.open());
|
||||||
|
splitLog.write("latitude: 40.25\nlongitude: 29.50\n");
|
||||||
|
splitLog.flush();
|
||||||
|
received.clear();
|
||||||
|
receivedAt.clear();
|
||||||
|
elapsed.restart();
|
||||||
|
assert(replay.start(splitLog.fileName(), 1, 1));
|
||||||
|
assert(replay.locationCount() == 1);
|
||||||
|
loop.exec();
|
||||||
|
assert(received.size() == 2);
|
||||||
|
|
||||||
|
QTemporaryFile invalidLog;
|
||||||
|
assert(invalidLog.open());
|
||||||
|
invalidLog.write("only diagnostics\n");
|
||||||
|
invalidLog.flush();
|
||||||
|
assert(!replay.start(invalidLog.fileName(), 200, 50));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user