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:
+28
-4
@@ -1,4 +1,4 @@
|
||||
import sys, os
|
||||
import sys, os, itertools
|
||||
|
||||
_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
_gui = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -19,6 +19,8 @@ from backend.file_backend import source_id_from_path
|
||||
BAUD_RATES = ['4800', '9600', '19200', '38400', '57600', '115200',
|
||||
'230400', '460800', '921600']
|
||||
|
||||
_connection_counter = itertools.count(1)
|
||||
|
||||
|
||||
class _StatusDot(QLabel):
|
||||
def __init__(self):
|
||||
@@ -45,10 +47,11 @@ class ConnectionRow(QWidget):
|
||||
layout.setContentsMargins(4, 2, 4, 2)
|
||||
layout.setSpacing(6)
|
||||
|
||||
# ── Receiver ID ──────────────────────────────────────────────────────
|
||||
# ── Receiver ID (read-only: pre-filled with a counter, rewritten from
|
||||
# MON-VER once the device answers, or derived from the file path) ──
|
||||
layout.addWidget(QLabel("ID:"))
|
||||
self._id_edit = QLineEdit()
|
||||
self._id_edit.setPlaceholderText("receiver-id")
|
||||
self._id_edit.setReadOnly(True)
|
||||
self._id_edit.setFixedWidth(110)
|
||||
layout.addWidget(self._id_edit)
|
||||
|
||||
@@ -125,6 +128,11 @@ class ConnectionRow(QWidget):
|
||||
layout.addWidget(self._stack)
|
||||
layout.addStretch()
|
||||
|
||||
# ── Device identification (read-only, from MON-VER on connect) ────────
|
||||
self._version_label = QLabel("")
|
||||
self._version_label.setStyleSheet("color: #888888; font-size: 11px;")
|
||||
layout.addWidget(self._version_label)
|
||||
|
||||
# ── Status / buttons ─────────────────────────────────────────────────
|
||||
self._dot = _StatusDot()
|
||||
layout.addWidget(self._dot)
|
||||
@@ -141,6 +149,8 @@ class ConnectionRow(QWidget):
|
||||
|
||||
if config:
|
||||
self.apply_config(config)
|
||||
else:
|
||||
self._id_edit.setText(f"conn-{next(_connection_counter)}")
|
||||
|
||||
# ── File browse ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -199,10 +209,17 @@ class ConnectionRow(QWidget):
|
||||
self._connected = connected
|
||||
self._dot.set_connected(connected)
|
||||
self._conn_btn.setText("Disconnect" if connected else "Connect")
|
||||
for w in [self._id_edit, self._type_combo, self._host_edit,
|
||||
for w in [self._type_combo, self._host_edit,
|
||||
self._port_spin, self._ser_combo, self._baud_combo,
|
||||
self._file_edit, self._delay_spin]:
|
||||
w.setEnabled(not connected)
|
||||
if not connected:
|
||||
self._version_label.setText("")
|
||||
|
||||
def set_version_info(self, device_id: str, sw_version: str):
|
||||
if device_id:
|
||||
self._id_edit.setText(device_id)
|
||||
self._version_label.setText(sw_version)
|
||||
|
||||
# ── Slots ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -287,6 +304,7 @@ class ConnectionTab(QWidget):
|
||||
outer.addLayout(bottom)
|
||||
|
||||
model.connection_changed.connect(self._on_connection_changed)
|
||||
model.version_update.connect(self._on_version_update)
|
||||
|
||||
# ── History combo ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -332,3 +350,9 @@ class ConnectionTab(QWidget):
|
||||
break
|
||||
if connected:
|
||||
self._refresh_history_combo()
|
||||
|
||||
def _on_version_update(self, rid: str, device_id: str, sw_version: str):
|
||||
for row in self._rows:
|
||||
if row.get_receiver_id() == rid:
|
||||
row.set_version_info(device_id, sw_version)
|
||||
break
|
||||
|
||||
+46
-1
@@ -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
|
||||
|
||||
+11
-1
@@ -316,6 +316,7 @@ class SinglePlotWidget(QWidget):
|
||||
model.receiver_removed.connect(self._on_receiver_removed)
|
||||
model.satellite_update.connect(self._on_sat_update)
|
||||
model.pdop_update.connect(self._on_pdop_update)
|
||||
model.version_update.connect(self._on_version_update)
|
||||
|
||||
# ── UI ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -425,7 +426,7 @@ class SinglePlotWidget(QWidget):
|
||||
for rid in selected:
|
||||
ts_dq, v_dq = self._data.get(rid, ([], []))
|
||||
if ts_dq:
|
||||
snapshot[rid] = (list(ts_dq), list(v_dq))
|
||||
snapshot[self._model.display_name(rid)] = (list(ts_dq), list(v_dq))
|
||||
w, h = self._canvas.width(), self._canvas.height()
|
||||
if w > 0 and h > 0:
|
||||
self._thread.submit(snapshot, self._y_cb.currentText(), w, h)
|
||||
@@ -485,6 +486,15 @@ class SinglePlotWidget(QWidget):
|
||||
self._rebuild_val_combo()
|
||||
self._submit_render()
|
||||
|
||||
def _on_version_update(self, rid: str, device_id: str, sw_version: str):
|
||||
label = self._model.display_name(rid)
|
||||
for i in range(self._src_list.count()):
|
||||
item = self._src_list.item(i)
|
||||
if item.data(Qt.UserRole) == rid:
|
||||
item.setText(f" {label} ")
|
||||
break
|
||||
self._submit_render()
|
||||
|
||||
def _on_sat_update(self, rid: str, sats: list):
|
||||
selected = set(self._selected_rids())
|
||||
if rid not in selected:
|
||||
|
||||
+18
-1
@@ -34,6 +34,7 @@ class SkyTab(QWidget):
|
||||
self._pdop: dict = {} # rid -> float
|
||||
self._info_cards: dict = {} # rid -> QFrame
|
||||
self._info_labels: dict = {} # rid -> (lbl_pdop, lbl_visible, lbl_used)
|
||||
self._name_labels: dict = {} # rid -> QLabel (receiver display name)
|
||||
self._trail: dict = {} # (rid, gnss_id, sv_id) -> deque[(elev, azim)]
|
||||
self._hi_timers: dict = {} # (rid, gnss_id, sv_id) -> QTimer
|
||||
|
||||
@@ -113,6 +114,7 @@ class SkyTab(QWidget):
|
||||
model.receiver_removed.connect(self._on_receiver_removed)
|
||||
model.satellite_update.connect(self._on_sat_update)
|
||||
model.pdop_update.connect(self._on_pdop_update)
|
||||
model.version_update.connect(self._on_version_update)
|
||||
|
||||
# ── Source list management ────────────────────────────────────────────────
|
||||
|
||||
@@ -136,6 +138,7 @@ class SkyTab(QWidget):
|
||||
cl.addWidget(w)
|
||||
self._info_cards[rid] = card
|
||||
self._info_labels[rid] = (lbl_pdop, lbl_visible, lbl_used)
|
||||
self._name_labels[rid] = name_lbl
|
||||
self._info_layout.insertWidget(self._info_layout.count() - 1, card)
|
||||
|
||||
def _on_receiver_removed(self, rid: str):
|
||||
@@ -151,6 +154,20 @@ class SkyTab(QWidget):
|
||||
self._info_layout.removeWidget(card)
|
||||
card.deleteLater()
|
||||
self._info_labels.pop(rid, None)
|
||||
self._name_labels.pop(rid, None)
|
||||
self._refresh_immediate()
|
||||
|
||||
def _on_version_update(self, rid: str, device_id: str, sw_version: str):
|
||||
label = self._model.display_name(rid)
|
||||
for i in range(self._src_list.count()):
|
||||
item = self._src_list.item(i)
|
||||
if item.data(Qt.UserRole) == rid:
|
||||
item.setText(f" {label} ")
|
||||
break
|
||||
lbl = self._name_labels.get(rid)
|
||||
if lbl:
|
||||
lbl.setText(f"<b>{label}</b>")
|
||||
self._table_keys = [] # force a full rebuild so the Receiver column updates
|
||||
self._refresh_immediate()
|
||||
|
||||
def _selected_rids(self) -> set:
|
||||
@@ -297,7 +314,7 @@ class SkyTab(QWidget):
|
||||
self._table.setItem(row, 3, cell(f"{sat.azimuth:.0f}"))
|
||||
self._table.setItem(row, 4, cell(f"{sat.cno:.1f}"))
|
||||
self._table.setItem(row, 5, cell("✓" if sat.used_in_fix else ""))
|
||||
self._table.setItem(row, 6, cell(rid))
|
||||
self._table.setItem(row, 6, cell(self._model.display_name(rid)))
|
||||
msg_item = QTableWidgetItem("")
|
||||
msg_item.setTextAlignment(Qt.AlignCenter)
|
||||
msg_item.setBackground(brush)
|
||||
|
||||
Reference in New Issue
Block a user