add UBX-MON-VER support and surface device identification in CLI/GUI

Add the Ver message parser and a fixed UbxQuery (was unused and had a
broken semaphore/state bug) to poll MON-VER once on connect. The CLI
prints hw/sw version and extensions; the GUI polls it per receiver,
pre-fills the read-only ID field with a connection counter and
rewrites it from the MOD= extension once the device answers, and
propagates the resolved display name to the Sky and Plot tabs so they
no longer show the stale placeholder ID.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XM4FNUjhu9x8df5Dp1hHvd
This commit is contained in:
2026-07-16 20:22:25 +02:00
co-authored by Claude Sonnet 5
parent d506f9afe0
commit 2dba3a9a2f
8 changed files with 176 additions and 27 deletions
+46 -1
View File
@@ -1,4 +1,4 @@
import sys, os, json, dataclasses, time
import sys, os, json, dataclasses, time, threading
from dataclasses import dataclass
from PyQt5.QtCore import QObject, QTimer, pyqtSignal
@@ -13,6 +13,8 @@ from nmea.packetizer import NmeaPacketizer
from ubx.packetizer import UbxPacketizer
from backend.network_backend import NetworkBackend
from ubx.ubx import frame_create
from ubx.query import UbxQuery
from ubx.messages import Ver
GNSS_NAMES = {
0: 'GPS', 1: 'SBAS', 2: 'Galileo', 3: 'BeiDou',
@@ -35,6 +37,14 @@ LRU_PATH = os.path.expanduser('~/.nmea_client_gui.json')
LRU_MAX = 20
def _device_id_from_ver(ver: dict) -> str:
for ext in ver.get('extensions', []):
s = ext.get('extension', '')
if s.startswith('MOD='):
return s[len('MOD='):]
return ver.get('hw_version', '')
@dataclass
class SatelliteData:
gnss_id: int
@@ -85,12 +95,15 @@ class _ReceiverState:
self.backend = None
self.transceiver = None
self.poll_timer = None
self.device_id: str = ''
self.sw_version: str = ''
class ReceiverManager(QObject):
satellite_update = pyqtSignal(str, list) # rid, [SatelliteData]
pdop_update = pyqtSignal(str, float, float) # rid, pdop, timestamp
connection_changed = pyqtSignal(str, bool) # rid, connected
version_update = pyqtSignal(str, str, str) # rid, device_id, sw_version
receiver_added = pyqtSignal(str)
receiver_removed = pyqtSignal(str)
_connection_lost = pyqtSignal(str) # internal: backend thread -> GUI thread
@@ -101,6 +114,7 @@ class ReceiverManager(QObject):
self._configs: dict = {}
self._lru: list = self._load_lru()
self._connection_lost.connect(self._handle_connection_lost)
self.version_update.connect(self._on_version)
# ── LRU ──────────────────────────────────────────────────────────────────
@@ -197,6 +211,11 @@ class ReceiverManager(QObject):
GsaQtListener(rid, lambda p, t, r=rid: self._on_pdop(r, p, t))
)
ver_query = None
if config.conn_type != 'file':
ver_query = UbxQuery(xcvr, Ver())
ubx_sink.register_listener(ver_query)
backend.connect()
backend.start()
@@ -211,9 +230,18 @@ class ReceiverManager(QObject):
timer.start()
state.poll_timer = timer
threading.Thread(target=self._poll_version, args=(rid, ver_query),
daemon=True).start()
self._push_lru(config)
self.connection_changed.emit(rid, True)
def _poll_version(self, rid: str, query: UbxQuery):
ver = query.poll()
if ver is None:
return
self.version_update.emit(rid, _device_id_from_ver(ver), ver['sw_version'])
def _handle_connection_lost(self, rid: str):
state = self._states.get(rid)
if not state or not state.connected:
@@ -237,7 +265,10 @@ class ReceiverManager(QObject):
state.backend = None
state.transceiver = None
state.satellites.clear()
state.device_id = ''
state.sw_version = ''
self.connection_changed.emit(rid, False)
self.version_update.emit(rid, '', '')
def _poll_ubx(self, rid: str):
state = self._states.get(rid)
@@ -263,6 +294,14 @@ class ReceiverManager(QObject):
def get_config(self, rid: str):
return self._configs.get(rid)
def get_version_info(self, rid: str):
s = self._states.get(rid)
return (s.device_id, s.sw_version) if s else ('', '')
def display_name(self, rid: str) -> str:
s = self._states.get(rid)
return (s.device_id if s and s.device_id else rid)
def get_satellites(self, rids: list) -> list:
result = []
for rid in rids:
@@ -288,3 +327,9 @@ class ReceiverManager(QObject):
return
state.pdop = pdop
self.pdop_update.emit(rid, pdop, ts)
def _on_version(self, rid: str, device_id: str, sw_version: str):
state = self._states.get(rid)
if state:
state.device_id = device_id
state.sw_version = sw_version