diff --git a/conversation_recap.md b/conversation_recap.md new file mode 100644 index 0000000..7d19d74 --- /dev/null +++ b/conversation_recap.md @@ -0,0 +1,113 @@ +# 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 |