# 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 |