114 lines
4.9 KiB
Markdown
114 lines
4.9 KiB
Markdown
# GNSS Monitor GUI — Conversation Recap
|
||
|
||
Date: 2026-05-24
|
||
|
||
## Goal
|
||
|
||
Build a PyQt5 frontend (`gui/`) on top of the existing nmea_client pipeline
|
||
(NetworkBackend / Transceiver / NmeaPacketizer / UbxPacketizer) with three tabs:
|
||
Connection, Sky, Plot.
|
||
|
||
---
|
||
|
||
## Files Created
|
||
|
||
| File | Purpose |
|
||
|---|---|
|
||
| `gui/main.py` | QApplication, MainWindow, QTabWidget with 3 tabs |
|
||
| `gui/data_model.py` | ReceiverManager (QObject), SatelliteData, ConnectionConfig, GNSS constants, LRU persistence |
|
||
| `gui/listeners.py` | NavSatQtListener (UBX NAV-SAT → SatelliteData), GsaQtListener (NMEA GSA → pDOP) |
|
||
| `gui/backends.py` | SerialBackend (ABackend over pyserial) |
|
||
| `gui/connection_tab.py` | ConnectionTab + ConnectionRow, TCP/serial config, LRU history combo |
|
||
| `gui/sky_widget.py` | SkyPlotWidget — off-thread QPainter renderer via SkyRenderThread |
|
||
| `gui/sky_tab.py` | SkyTab — sky plot + satellite table + source selector + info bar + trails |
|
||
| `gui/plot_widget.py` | SinglePlotWidget — custom QPainter canvas, 6 y-axis modes, settings persistence |
|
||
| `gui/plot_tab.py` | PlotTab — 8 SinglePlotWidgets in a 2-column scrollable grid |
|
||
|
||
---
|
||
|
||
## Connection Tab
|
||
|
||
- Each row: ID field, TCP/Serial switcher (QStackedWidget), host/port or serial/baud
|
||
fields, status dot, Connect/Disconnect, Remove
|
||
- LRU history combo bottom-left, Add button next to it — user picks from history then
|
||
hits Add
|
||
- Host field accepts both IP addresses and hostnames
|
||
- LRU persisted to `~/.nmea_client_gui.json` (max 20 entries)
|
||
|
||
---
|
||
|
||
## Sky Tab
|
||
|
||
- **Sky plot**: polar projection, concentric rings at 0°/30°/60°, cardinal labels,
|
||
satellites sized by C/N₀, filled = used in fix, dashed outline = not used
|
||
- **Off-thread rendering**: `SkyRenderThread` (QThread + Queue(maxsize=1)) renders to
|
||
QImage; paintEvent does only `drawImage` — main thread never blocks
|
||
- **Trails**: per-satellite `deque(maxlen=86400)` (24 h at 1 Hz); global Show Trails
|
||
checkbox + depth spinbox (0–24 h); subsampled to ≤120 points in render thread;
|
||
alpha fades 15→200 oldest→newest
|
||
- **Satellite table**: columns GNSS / SV / Elev° / Az° / C/N₀ / Fix / Receiver / Msg;
|
||
sorted by GNSS then SV; in-place cell updates preserve scroll position; full rebuild
|
||
only when satellite set changes
|
||
- **Msg column**: fixed 36 px width; shows `✓` for `_MSG_HIGHLIGHT_MS = 10 000` ms after
|
||
a message is received; clears and evicts satellite from store/sky when timer fires
|
||
(not seen for 10 s)
|
||
- **Info bar**: one card per receiver showing pDOP / Visible / Used counts
|
||
- **Source selector**: per-receiver checkboxes to include/exclude from plot and table
|
||
|
||
---
|
||
|
||
## Plot Tab
|
||
|
||
- 8 independent plots, 2-column scrollable layout
|
||
- **Y-axis modes**: C/N₀, Azimuth, Elevation (satellite-specific); pDOP, Visible Sats,
|
||
Used Sats (global/per-receiver)
|
||
- **Off-thread rendering**: same QThread+QImage pattern as sky widget
|
||
- **Custom QPainter canvas**: grid, axis ticks, curves, legend — no pyqtgraph dependency
|
||
- **`_nice_ticks(lo, hi)`**: `math.floor` for first tick (≤ lo), `math.ceil` for last
|
||
tick (≥ hi), with 5 % padding
|
||
- **Settings persistence**: y-axis mode, satellite selection, point count saved to
|
||
`~/.nmea_client_plots.json` keyed by plot index 0–7
|
||
- **Right-click context menu**: Save as PNG, Copy to clipboard
|
||
- All GNSS sources default to checked (enabled)
|
||
|
||
---
|
||
|
||
## Key Architectural Decisions
|
||
|
||
- **Qt cross-thread safety**: listener callbacks emit signals from backend threads; Qt
|
||
queues delivery to main thread automatically — no explicit locking needed
|
||
- **Off-thread rendering pattern**: `QPainter` on `QImage` is thread-safe;
|
||
`Queue(maxsize=1)` ensures only the latest data is rendered, stale frames discarded
|
||
- `_GNSS_QCOLORS` dict pre-built in main thread; render thread reads it as read-only
|
||
- **In-place table cell updates**: `_table_keys` list tracks row identity; `setText` only
|
||
when structure unchanged; `setRowCount`/`setSortingEnabled` avoided to preserve scroll
|
||
position
|
||
|
||
---
|
||
|
||
## Notable Fixes & Iterations
|
||
|
||
| Issue | Fix |
|
||
|---|---|
|
||
| Sky plot slow, table scrolling laggy | QTimer throttle (500 ms) + in-place cell updates + `viewport().setUpdatesEnabled(False/True)` |
|
||
| Y-axis clipping data at top | `math.floor` for first tick; loop extended to cover `ceil` of max |
|
||
| Plot satellite combo always empty | Source checkboxes defaulted to `Qt.Checked` |
|
||
| Msg column showing two sub-columns | `QHeaderView.Fixed` + explicit `setColumnWidth` |
|
||
| Row background highlight rejected | Replaced with dedicated Msg column + `✓` symbol |
|
||
| Host field rejecting hostnames | `QLineEdit` with `setPlaceholderText` instead of IP-only validator |
|
||
|
||
---
|
||
|
||
## GNSS Systems Supported
|
||
|
||
| ID | Name | Short | Color |
|
||
|---|---|---|---|
|
||
| 0 | GPS | G | #2196F3 blue |
|
||
| 1 | SBAS | S | #9E9E9E gray |
|
||
| 2 | Galileo | E | #4CAF50 green |
|
||
| 3 | BeiDou | C | #FF9800 orange |
|
||
| 4 | IMES | I | #607D8B blue-gray |
|
||
| 5 | QZSS | J | #9C27B0 purple |
|
||
| 6 | GLONASS | R | #F44336 red |
|
||
| 7 | NavIC | N | #F4A0A0 rose |
|