docs: refresh CLAUDE.md to match current codebase
It still described network_backend.py at the repo root as the entry point and said nothing about the GUI, the serial/file backends, or how backend threads now handle parse errors vs. real disconnects. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzmCaNjDb3TAqPpTwunKTY
This commit is contained in:
@@ -4,25 +4,30 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## What this project is
|
## What this project is
|
||||||
|
|
||||||
A Python client for u-blox GNSS receivers (NEO-F9P, ZED-X20P) connected over TCP/IP. It receives and parses both NMEA 0183 and UBX binary protocol messages, and can send UBX poll requests to the receivers.
|
A Python client for u-blox GNSS receivers (NEO-F9P, ZED-X20P) connected over TCP/IP, serial, or replayed from a log file. It receives and parses both NMEA 0183 and UBX binary protocol messages, and can send UBX poll requests to the receivers.
|
||||||
|
|
||||||
Hardware reference documentation is in `readme.md` (u-blox datasheets, integration manuals, interface specs, RTK/RTCM references).
|
Hardware reference documentation and CLI usage examples are in `readme.md` (u-blox datasheets, integration manuals, interface specs, RTK/RTCM references).
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python network_backend.py
|
# CLI — headless, prints parsed messages to stdout
|
||||||
|
python main.py --network neo-f9p 192.168.22.93 8721 \
|
||||||
|
--network zed-x20p 192.168.22.93 8731
|
||||||
|
|
||||||
|
# GUI — PyQt5 app with Connection / Sky / Plot tabs
|
||||||
|
python gui/main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
The entry point in `network_backend.py` connects to two GNSS receivers, wires up packetizers and message listeners, then polls UBX messages every second.
|
`main.py` accepts `--network NAME HOST PORT`, `--serial NAME DEVICE BAUD`, `--file PATH` (repeatable/combinable), `--project FILE` / `--save FILE` for JSON config, `--log-dir DIR` and `--delay SECS` for file replay pacing. See `readme.md` for the full option table and a project-file example.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The design follows a **listener/talker pipeline** pattern:
|
The design follows a **listener/talker pipeline** pattern:
|
||||||
|
|
||||||
```
|
```
|
||||||
NetworkBackend (TCP socket per receiver)
|
ABackend (Network | Serial | File)
|
||||||
└─> Transceiver (one per receiver, named to match socket)
|
└─> Transceiver (one per receiver, named to match the backend's source)
|
||||||
├─> NmeaPacketizer ──> NmeaListener subclasses (Gga, Gll, Gsa, Gsv, Rmc)
|
├─> NmeaPacketizer ──> NmeaListener subclasses (Gga, Gll, Gsa, Gsv, Rmc)
|
||||||
└─> UbxPacketizer ──> UbxListener instances (wrapping UbxMsg subclasses)
|
└─> UbxPacketizer ──> UbxListener instances (wrapping UbxMsg subclasses)
|
||||||
```
|
```
|
||||||
@@ -30,11 +35,16 @@ NetworkBackend (TCP socket per receiver)
|
|||||||
**Core abstractions** (`msg/`):
|
**Core abstractions** (`msg/`):
|
||||||
- `MsgContainer` — wraps raw `bytes` with a `source` (ATransceiver) and timestamp
|
- `MsgContainer` — wraps raw `bytes` with a `source` (ATransceiver) and timestamp
|
||||||
- `MsgListener` — ABC with `on_recv(msg)` and `is_msg(msg)` (default: False); only called when `is_msg` returns True
|
- `MsgListener` — ABC with `on_recv(msg)` and `is_msg(msg)` (default: False); only called when `is_msg` returns True
|
||||||
- `MsgTalker` — holds a list of `MsgListener` sinks; `call_listener` iterates them and dispatches
|
- `MsgTalker` — holds a list of `MsgListener` sinks; `call_listener` iterates them, calling `is_msg`/`on_recv` per sink inside a `try/except` so one misbehaving listener can't kill the caller (important: e.g. `UbxPacketizer.on_recv` only resets its packet buffer *after* `call_listener` returns)
|
||||||
|
- `FrameLogger` — writes every raw frame for a source to a timestamped log file under `log_dir`, for later replay
|
||||||
|
- `ReplayPacer` — sits between a packetizer and its listeners during file replay; sleeps `delay` seconds per complete frame to pace playback
|
||||||
|
|
||||||
**Transceiver** (`transceiver.py`): inherits both `ATransceiver` (MsgListener) and `MsgTalker`. Receives raw bytes from the backend via `on_recv`, wraps them in `MsgContainer`, and fans out to downstream listeners. Sends bytes back via `backend.send`.
|
**Transceiver** (`transceiver.py`): inherits both `ATransceiver` (MsgListener) and `MsgTalker`. Receives raw bytes from the backend via `on_recv`, wraps them in `MsgContainer`, and fans out to downstream listeners. Sends bytes back via `backend.send`.
|
||||||
|
|
||||||
**NetworkBackend** (`network_backend.py`): manages non-blocking TCP sockets via `selectors`. Each connection has a name, address, associated `Transceiver`, and outgoing `Queue`. Call `register_xcvr(xcvr)` to bind a transceiver to its socket by name match, then `connect()` + `start()` to begin the event loop thread.
|
**Backends** (`backend/`), each implementing `ABackend.send(xcvr, data)`:
|
||||||
|
- `NetworkBackend` — non-blocking TCP sockets via `selectors`, one shared selector/thread per backend instance (the CLI wires all `--network` sources into one shared `NetworkBackend`; the GUI creates one `NetworkBackend` per receiver so connections are fully isolated). Call `register_xcvr(xcvr)` to bind a transceiver to its socket by name match, then `connect()` + `start()` to begin the event loop thread. The event loop catches `OSError` (peer closed / connection refused) to cleanly unregister and close that socket without killing the thread, and catches any other `Exception` from downstream processing so a parse error in a listener can't silently kill the reader thread either. Accepts an optional `on_disconnect(name)` callback, invoked when a socket is lost — the GUI uses this to reflect a peer-initiated disconnect in `ReceiverManager` instead of leaving stale "connected" state.
|
||||||
|
- `SerialBackend` — reads/writes a pyserial `Serial` port on a polling thread
|
||||||
|
- `FileBackend` — replays one or more raw log files (as produced by `FrameLogger`) chunk-by-chunk on a thread per file; `source_id_from_path` extracts the receiver name from the `YYYY_MM_DD_HH_MM_SS_<name>.log` filename convention
|
||||||
|
|
||||||
**NMEA parsing** (`nmea/`):
|
**NMEA parsing** (`nmea/`):
|
||||||
- `NmeaPacketizer`: buffers bytes and emits a `MsgContainer` per line (CR/LF delimited)
|
- `NmeaPacketizer`: buffers bytes and emits a `MsgContainer` per line (CR/LF delimited)
|
||||||
@@ -49,8 +59,24 @@ NetworkBackend (TCP socket per receiver)
|
|||||||
- `UbxQuery` (`ubx/query.py`): extends `UbxListener` with a semaphore-based synchronous `poll()` — sends a UBX poll frame and blocks until the response arrives
|
- `UbxQuery` (`ubx/query.py`): extends `UbxListener` with a semaphore-based synchronous `poll()` — sends a UBX poll frame and blocks until the response arrives
|
||||||
|
|
||||||
**Implemented UBX messages** (`ubx/messages/`):
|
**Implemented UBX messages** (`ubx/messages/`):
|
||||||
- `nav.py`: `Sig` (0x01/0x43 NAV-SIG), `Sat` (0x01/0x35 NAV-SAT)
|
- `nav.py`: `Sig` (0x01/0x43 NAV-SIG), `Sat` (0x01/0x35 NAV-SAT) — repeated-group parsers walk `remain >= group_size` (not a bare truthy check) so a payload whose length isn't an exact multiple of the per-item size logs a warning and stops instead of raising `struct.error` on a short slice
|
||||||
- `rxm.py`: `RawX` (0x02/0x15 RXM-RAWX)
|
- `rxm.py`: `RawX` (0x02/0x15 RXM-RAWX) — same defensive group parsing
|
||||||
|
|
||||||
|
## GUI (`gui/`)
|
||||||
|
|
||||||
|
A PyQt5 app (`gui/main.py`) on top of the same msg-pipeline, with Connection / Sky / Plot tabs in a `QTabWidget`.
|
||||||
|
|
||||||
|
- `data_model.py` — `ReceiverManager(QObject)` owns per-receiver state (`_ReceiverState`: backend, transceiver, satellites, pdop, connected flag) and exposes Qt signals (`connection_changed`, `satellite_update`, `pdop_update`, `receiver_added`/`removed`) that GUI widgets connect to. `connect_receiver`/`disconnect_receiver`/`remove_receiver` build the backend + packetizer + listener chain for TCP/Serial/File sources. A peer-initiated disconnect (`NetworkBackend`'s `on_disconnect` callback, fired from the backend thread) is marshaled to the GUI thread via an internal `_connection_lost` signal and handled by tearing the receiver down the same way an explicit disconnect does. `ConnectionConfig` + an LRU history persisted to `~/.nmea_client_gui.json` round out connection setup.
|
||||||
|
- `listeners.py` — `NavSatQtListener` (UBX NAV-SAT → `SatelliteData` list) and `GsaQtListener` (NMEA GSA → pDOP) bridge parsed messages to `ReceiverManager` callbacks.
|
||||||
|
- `connection_tab.py` — `ConnectionTab` + `ConnectionRow`: one row per receiver, TCP (host/port) / Serial (port/baud) / File (path/delay) source types, LRU history combo.
|
||||||
|
- `sky_tab.py` / `sky_widget.py` — polar sky-plot (off-thread `QPainter` rendering, no external plotting deps), satellite table, per-satellite trails, GNSS-source filter checkboxes.
|
||||||
|
- `plot_tab.py` / `plot_widget.py` — 8 independent time-series plots (custom `QPainter` canvas, no pyqtgraph) in a scrollable 2-column grid; settings persisted to `~/.nmea_client_plots.json`.
|
||||||
|
|
||||||
|
Rendering that isn't trivial should stay off the GUI thread (see the sky/plot widgets for the established pattern), and table widgets should update cells in place rather than rebuilding on every message to preserve scroll position.
|
||||||
|
|
||||||
|
## Error-handling philosophy
|
||||||
|
|
||||||
|
Backend-thread code (the socket reader loops) should never die from an application-level parsing bug — only from a genuine, expected `OSError` on the socket itself, which is treated as "this connection is gone" and reported via `on_disconnect` where a caller is listening. Anything else raised by downstream listener/parser code should be caught, logged, and skipped so one malformed or unexpected message doesn't take down the whole read loop.
|
||||||
|
|
||||||
## Adding a new UBX message
|
## Adding a new UBX message
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user