3.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
Hardware reference documentation is in readme.md (u-blox datasheets, integration manuals, interface specs, RTK/RTCM references).
Running
python network_backend.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.
Architecture
The design follows a listener/talker pipeline pattern:
NetworkBackend (TCP socket per receiver)
└─> Transceiver (one per receiver, named to match socket)
├─> NmeaPacketizer ──> NmeaListener subclasses (Gga, Gll, Gsa, Gsv, Rmc)
└─> UbxPacketizer ──> UbxListener instances (wrapping UbxMsg subclasses)
Core abstractions (msg/):
MsgContainer— wraps rawbyteswith asource(ATransceiver) and timestampMsgListener— ABC withon_recv(msg)andis_msg(msg)(default: False); only called whenis_msgreturns TrueMsgTalker— holds a list ofMsgListenersinks;call_listeneriterates them and dispatches
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.
NMEA parsing (nmea/):
NmeaPacketizer: buffers bytes and emits aMsgContainerper line (CR/LF delimited)NmeaListener/AsciiListener: filters by regex on the raw bytes; subclasses overrideon_recvto parse CSV fields- Message classes (
nmea/messages/):Gga,Gll,Gsa,Gsv,Rmc— each takes(name, tid)wheretidis the talker ID (e.g.'GN','GP'); use'--'to match any talker
UBX parsing (ubx/):
ubx.py:frame_create(class, id, data)builds a framed UBX packet with Fletcher checksum;frame_parse/frame_parse_hdrdecode received framesUbxPacketizer: buffers bytes, detects sync word0xB5 0x62, reassembles complete frames by checking the length field and verifying checksumUbxMsg(base class inubx/messages/msg.py): holdsmclass/mid; subclasses implementfrom_bytes(data)as a classmethodUbxListener: wraps aUbxMsg, matches by class+id inis_msg, parses and prints inon_recvUbxQuery(ubx/query.py): extendsUbxListenerwith a semaphore-based synchronouspoll()— sends a UBX poll frame and blocks until the response arrives
Implemented UBX messages (ubx/messages/):
nav.py:Sig(0x01/0x43 NAV-SIG),Sat(0x01/0x35 NAV-SAT)rxm.py:RawX(0x02/0x15 RXM-RAWX)
Adding a new UBX message
- Create a subclass of
UbxMsgwith the correctmclass/midand afrom_bytesclassmethod - Instantiate
UbxListener(YourMsg())and register it on theUbxPacketizer
Adding a new NMEA sentence
Subclass NmeaListener with the appropriate msg_type string, override on_recv to split on b',' and populate a Msg dataclass, then register on the NmeaPacketizer.