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__)))
|
_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
_gui = 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',
|
BAUD_RATES = ['4800', '9600', '19200', '38400', '57600', '115200',
|
||||||
'230400', '460800', '921600']
|
'230400', '460800', '921600']
|
||||||
|
|
||||||
|
_connection_counter = itertools.count(1)
|
||||||
|
|
||||||
|
|
||||||
class _StatusDot(QLabel):
|
class _StatusDot(QLabel):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -45,10 +47,11 @@ class ConnectionRow(QWidget):
|
|||||||
layout.setContentsMargins(4, 2, 4, 2)
|
layout.setContentsMargins(4, 2, 4, 2)
|
||||||
layout.setSpacing(6)
|
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:"))
|
layout.addWidget(QLabel("ID:"))
|
||||||
self._id_edit = QLineEdit()
|
self._id_edit = QLineEdit()
|
||||||
self._id_edit.setPlaceholderText("receiver-id")
|
self._id_edit.setReadOnly(True)
|
||||||
self._id_edit.setFixedWidth(110)
|
self._id_edit.setFixedWidth(110)
|
||||||
layout.addWidget(self._id_edit)
|
layout.addWidget(self._id_edit)
|
||||||
|
|
||||||
@@ -125,6 +128,11 @@ class ConnectionRow(QWidget):
|
|||||||
layout.addWidget(self._stack)
|
layout.addWidget(self._stack)
|
||||||
layout.addStretch()
|
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 ─────────────────────────────────────────────────
|
# ── Status / buttons ─────────────────────────────────────────────────
|
||||||
self._dot = _StatusDot()
|
self._dot = _StatusDot()
|
||||||
layout.addWidget(self._dot)
|
layout.addWidget(self._dot)
|
||||||
@@ -141,6 +149,8 @@ class ConnectionRow(QWidget):
|
|||||||
|
|
||||||
if config:
|
if config:
|
||||||
self.apply_config(config)
|
self.apply_config(config)
|
||||||
|
else:
|
||||||
|
self._id_edit.setText(f"conn-{next(_connection_counter)}")
|
||||||
|
|
||||||
# ── File browse ──────────────────────────────────────────────────────────
|
# ── File browse ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -199,10 +209,17 @@ class ConnectionRow(QWidget):
|
|||||||
self._connected = connected
|
self._connected = connected
|
||||||
self._dot.set_connected(connected)
|
self._dot.set_connected(connected)
|
||||||
self._conn_btn.setText("Disconnect" if connected else "Connect")
|
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._port_spin, self._ser_combo, self._baud_combo,
|
||||||
self._file_edit, self._delay_spin]:
|
self._file_edit, self._delay_spin]:
|
||||||
w.setEnabled(not connected)
|
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 ────────────────────────────────────────────────────────────────
|
# ── Slots ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -287,6 +304,7 @@ class ConnectionTab(QWidget):
|
|||||||
outer.addLayout(bottom)
|
outer.addLayout(bottom)
|
||||||
|
|
||||||
model.connection_changed.connect(self._on_connection_changed)
|
model.connection_changed.connect(self._on_connection_changed)
|
||||||
|
model.version_update.connect(self._on_version_update)
|
||||||
|
|
||||||
# ── History combo ─────────────────────────────────────────────────────────
|
# ── History combo ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -332,3 +350,9 @@ class ConnectionTab(QWidget):
|
|||||||
break
|
break
|
||||||
if connected:
|
if connected:
|
||||||
self._refresh_history_combo()
|
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 dataclasses import dataclass
|
||||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal
|
from PyQt5.QtCore import QObject, QTimer, pyqtSignal
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@ from nmea.packetizer import NmeaPacketizer
|
|||||||
from ubx.packetizer import UbxPacketizer
|
from ubx.packetizer import UbxPacketizer
|
||||||
from backend.network_backend import NetworkBackend
|
from backend.network_backend import NetworkBackend
|
||||||
from ubx.ubx import frame_create
|
from ubx.ubx import frame_create
|
||||||
|
from ubx.query import UbxQuery
|
||||||
|
from ubx.messages import Ver
|
||||||
|
|
||||||
GNSS_NAMES = {
|
GNSS_NAMES = {
|
||||||
0: 'GPS', 1: 'SBAS', 2: 'Galileo', 3: 'BeiDou',
|
0: 'GPS', 1: 'SBAS', 2: 'Galileo', 3: 'BeiDou',
|
||||||
@@ -35,6 +37,14 @@ LRU_PATH = os.path.expanduser('~/.nmea_client_gui.json')
|
|||||||
LRU_MAX = 20
|
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
|
@dataclass
|
||||||
class SatelliteData:
|
class SatelliteData:
|
||||||
gnss_id: int
|
gnss_id: int
|
||||||
@@ -85,12 +95,15 @@ class _ReceiverState:
|
|||||||
self.backend = None
|
self.backend = None
|
||||||
self.transceiver = None
|
self.transceiver = None
|
||||||
self.poll_timer = None
|
self.poll_timer = None
|
||||||
|
self.device_id: str = ''
|
||||||
|
self.sw_version: str = ''
|
||||||
|
|
||||||
|
|
||||||
class ReceiverManager(QObject):
|
class ReceiverManager(QObject):
|
||||||
satellite_update = pyqtSignal(str, list) # rid, [SatelliteData]
|
satellite_update = pyqtSignal(str, list) # rid, [SatelliteData]
|
||||||
pdop_update = pyqtSignal(str, float, float) # rid, pdop, timestamp
|
pdop_update = pyqtSignal(str, float, float) # rid, pdop, timestamp
|
||||||
connection_changed = pyqtSignal(str, bool) # rid, connected
|
connection_changed = pyqtSignal(str, bool) # rid, connected
|
||||||
|
version_update = pyqtSignal(str, str, str) # rid, device_id, sw_version
|
||||||
receiver_added = pyqtSignal(str)
|
receiver_added = pyqtSignal(str)
|
||||||
receiver_removed = pyqtSignal(str)
|
receiver_removed = pyqtSignal(str)
|
||||||
_connection_lost = pyqtSignal(str) # internal: backend thread -> GUI thread
|
_connection_lost = pyqtSignal(str) # internal: backend thread -> GUI thread
|
||||||
@@ -101,6 +114,7 @@ class ReceiverManager(QObject):
|
|||||||
self._configs: dict = {}
|
self._configs: dict = {}
|
||||||
self._lru: list = self._load_lru()
|
self._lru: list = self._load_lru()
|
||||||
self._connection_lost.connect(self._handle_connection_lost)
|
self._connection_lost.connect(self._handle_connection_lost)
|
||||||
|
self.version_update.connect(self._on_version)
|
||||||
|
|
||||||
# ── LRU ──────────────────────────────────────────────────────────────────
|
# ── LRU ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -197,6 +211,11 @@ class ReceiverManager(QObject):
|
|||||||
GsaQtListener(rid, lambda p, t, r=rid: self._on_pdop(r, p, t))
|
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.connect()
|
||||||
backend.start()
|
backend.start()
|
||||||
|
|
||||||
@@ -211,9 +230,18 @@ class ReceiverManager(QObject):
|
|||||||
timer.start()
|
timer.start()
|
||||||
state.poll_timer = timer
|
state.poll_timer = timer
|
||||||
|
|
||||||
|
threading.Thread(target=self._poll_version, args=(rid, ver_query),
|
||||||
|
daemon=True).start()
|
||||||
|
|
||||||
self._push_lru(config)
|
self._push_lru(config)
|
||||||
self.connection_changed.emit(rid, True)
|
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):
|
def _handle_connection_lost(self, rid: str):
|
||||||
state = self._states.get(rid)
|
state = self._states.get(rid)
|
||||||
if not state or not state.connected:
|
if not state or not state.connected:
|
||||||
@@ -237,7 +265,10 @@ class ReceiverManager(QObject):
|
|||||||
state.backend = None
|
state.backend = None
|
||||||
state.transceiver = None
|
state.transceiver = None
|
||||||
state.satellites.clear()
|
state.satellites.clear()
|
||||||
|
state.device_id = ''
|
||||||
|
state.sw_version = ''
|
||||||
self.connection_changed.emit(rid, False)
|
self.connection_changed.emit(rid, False)
|
||||||
|
self.version_update.emit(rid, '', '')
|
||||||
|
|
||||||
def _poll_ubx(self, rid: str):
|
def _poll_ubx(self, rid: str):
|
||||||
state = self._states.get(rid)
|
state = self._states.get(rid)
|
||||||
@@ -263,6 +294,14 @@ class ReceiverManager(QObject):
|
|||||||
def get_config(self, rid: str):
|
def get_config(self, rid: str):
|
||||||
return self._configs.get(rid)
|
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:
|
def get_satellites(self, rids: list) -> list:
|
||||||
result = []
|
result = []
|
||||||
for rid in rids:
|
for rid in rids:
|
||||||
@@ -288,3 +327,9 @@ class ReceiverManager(QObject):
|
|||||||
return
|
return
|
||||||
state.pdop = pdop
|
state.pdop = pdop
|
||||||
self.pdop_update.emit(rid, pdop, ts)
|
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.receiver_removed.connect(self._on_receiver_removed)
|
||||||
model.satellite_update.connect(self._on_sat_update)
|
model.satellite_update.connect(self._on_sat_update)
|
||||||
model.pdop_update.connect(self._on_pdop_update)
|
model.pdop_update.connect(self._on_pdop_update)
|
||||||
|
model.version_update.connect(self._on_version_update)
|
||||||
|
|
||||||
# ── UI ────────────────────────────────────────────────────────────────────
|
# ── UI ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -425,7 +426,7 @@ class SinglePlotWidget(QWidget):
|
|||||||
for rid in selected:
|
for rid in selected:
|
||||||
ts_dq, v_dq = self._data.get(rid, ([], []))
|
ts_dq, v_dq = self._data.get(rid, ([], []))
|
||||||
if ts_dq:
|
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()
|
w, h = self._canvas.width(), self._canvas.height()
|
||||||
if w > 0 and h > 0:
|
if w > 0 and h > 0:
|
||||||
self._thread.submit(snapshot, self._y_cb.currentText(), w, h)
|
self._thread.submit(snapshot, self._y_cb.currentText(), w, h)
|
||||||
@@ -485,6 +486,15 @@ class SinglePlotWidget(QWidget):
|
|||||||
self._rebuild_val_combo()
|
self._rebuild_val_combo()
|
||||||
self._submit_render()
|
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):
|
def _on_sat_update(self, rid: str, sats: list):
|
||||||
selected = set(self._selected_rids())
|
selected = set(self._selected_rids())
|
||||||
if rid not in selected:
|
if rid not in selected:
|
||||||
|
|||||||
+18
-1
@@ -34,6 +34,7 @@ class SkyTab(QWidget):
|
|||||||
self._pdop: dict = {} # rid -> float
|
self._pdop: dict = {} # rid -> float
|
||||||
self._info_cards: dict = {} # rid -> QFrame
|
self._info_cards: dict = {} # rid -> QFrame
|
||||||
self._info_labels: dict = {} # rid -> (lbl_pdop, lbl_visible, lbl_used)
|
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._trail: dict = {} # (rid, gnss_id, sv_id) -> deque[(elev, azim)]
|
||||||
self._hi_timers: dict = {} # (rid, gnss_id, sv_id) -> QTimer
|
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.receiver_removed.connect(self._on_receiver_removed)
|
||||||
model.satellite_update.connect(self._on_sat_update)
|
model.satellite_update.connect(self._on_sat_update)
|
||||||
model.pdop_update.connect(self._on_pdop_update)
|
model.pdop_update.connect(self._on_pdop_update)
|
||||||
|
model.version_update.connect(self._on_version_update)
|
||||||
|
|
||||||
# ── Source list management ────────────────────────────────────────────────
|
# ── Source list management ────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -136,6 +138,7 @@ class SkyTab(QWidget):
|
|||||||
cl.addWidget(w)
|
cl.addWidget(w)
|
||||||
self._info_cards[rid] = card
|
self._info_cards[rid] = card
|
||||||
self._info_labels[rid] = (lbl_pdop, lbl_visible, lbl_used)
|
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)
|
self._info_layout.insertWidget(self._info_layout.count() - 1, card)
|
||||||
|
|
||||||
def _on_receiver_removed(self, rid: str):
|
def _on_receiver_removed(self, rid: str):
|
||||||
@@ -151,6 +154,20 @@ class SkyTab(QWidget):
|
|||||||
self._info_layout.removeWidget(card)
|
self._info_layout.removeWidget(card)
|
||||||
card.deleteLater()
|
card.deleteLater()
|
||||||
self._info_labels.pop(rid, None)
|
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()
|
self._refresh_immediate()
|
||||||
|
|
||||||
def _selected_rids(self) -> set:
|
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, 3, cell(f"{sat.azimuth:.0f}"))
|
||||||
self._table.setItem(row, 4, cell(f"{sat.cno:.1f}"))
|
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, 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 = QTableWidgetItem("")
|
||||||
msg_item.setTextAlignment(Qt.AlignCenter)
|
msg_item.setTextAlignment(Qt.AlignCenter)
|
||||||
msg_item.setBackground(brush)
|
msg_item.setBackground(brush)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import time
|
|||||||
import json
|
import json
|
||||||
import signal
|
import signal
|
||||||
import argparse
|
import argparse
|
||||||
|
import threading
|
||||||
|
|
||||||
from backend.network_backend import NetworkBackend
|
from backend.network_backend import NetworkBackend
|
||||||
from backend.serial_backend import SerialBackend
|
from backend.serial_backend import SerialBackend
|
||||||
@@ -16,8 +17,9 @@ from nmea.messages import Gll, Gsa, Gsv, Gga, Rmc
|
|||||||
from ubx.ubx import frame_create
|
from ubx.ubx import frame_create
|
||||||
from ubx.msg_types import *
|
from ubx.msg_types import *
|
||||||
from ubx.packetizer import UbxPacketizer
|
from ubx.packetizer import UbxPacketizer
|
||||||
from ubx.messages import Sig, Sat, RawX, UbxMsg
|
from ubx.messages import Sig, Sat, RawX, Ver, UbxMsg
|
||||||
from ubx.listener import UbxListener
|
from ubx.listener import UbxListener
|
||||||
|
from ubx.query import UbxQuery
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -87,7 +89,8 @@ def _make_listeners(name: str):
|
|||||||
|
|
||||||
|
|
||||||
def wire_source(name: str, backend, log_dir: str | None,
|
def wire_source(name: str, backend, log_dir: str | None,
|
||||||
loggers: list, transceivers: dict, pacer_delay: float = 0.0):
|
loggers: list, transceivers: dict, ver_queries: dict,
|
||||||
|
pacer_delay: float = 0.0):
|
||||||
xcvr = Transceiver(name)
|
xcvr = Transceiver(name)
|
||||||
backend.register_xcvr(xcvr)
|
backend.register_xcvr(xcvr)
|
||||||
transceivers[name] = xcvr
|
transceivers[name] = xcvr
|
||||||
@@ -104,6 +107,10 @@ def wire_source(name: str, backend, log_dir: str | None,
|
|||||||
|
|
||||||
nmea_rx, ubx_rx = _make_listeners(name)
|
nmea_rx, ubx_rx = _make_listeners(name)
|
||||||
|
|
||||||
|
ver_query = UbxQuery(xcvr, Ver())
|
||||||
|
ubx_rx.append(ver_query)
|
||||||
|
ver_queries[name] = ver_query
|
||||||
|
|
||||||
if pacer_delay > 0:
|
if pacer_delay > 0:
|
||||||
nmea_pacer = ReplayPacer(pacer_delay)
|
nmea_pacer = ReplayPacer(pacer_delay)
|
||||||
ubx_pacer = ReplayPacer(pacer_delay)
|
ubx_pacer = ReplayPacer(pacer_delay)
|
||||||
@@ -157,6 +164,7 @@ if __name__ == '__main__':
|
|||||||
backends = []
|
backends = []
|
||||||
loggers = []
|
loggers = []
|
||||||
transceivers = {}
|
transceivers = {}
|
||||||
|
ver_queries = {}
|
||||||
|
|
||||||
# Network sources → one shared NetworkBackend
|
# Network sources → one shared NetworkBackend
|
||||||
net_sources = [s for s in sources if s['type'] == 'network']
|
net_sources = [s for s in sources if s['type'] == 'network']
|
||||||
@@ -165,13 +173,14 @@ if __name__ == '__main__':
|
|||||||
for s in net_sources])
|
for s in net_sources])
|
||||||
backends.append(net_be)
|
backends.append(net_be)
|
||||||
for s in net_sources:
|
for s in net_sources:
|
||||||
wire_source(s['name'], net_be, log_dir, loggers, transceivers)
|
wire_source(s['name'], net_be, log_dir, loggers, transceivers, ver_queries)
|
||||||
|
|
||||||
# Serial sources → one SerialBackend each
|
# Serial sources → one SerialBackend each
|
||||||
for s in [s for s in sources if s['type'] == 'serial']:
|
serial_sources = [s for s in sources if s['type'] == 'serial']
|
||||||
|
for s in serial_sources:
|
||||||
ser_be = SerialBackend(s['device'], s.get('baudrate', 115200))
|
ser_be = SerialBackend(s['device'], s.get('baudrate', 115200))
|
||||||
backends.append(ser_be)
|
backends.append(ser_be)
|
||||||
wire_source(s['name'], ser_be, log_dir, loggers, transceivers)
|
wire_source(s['name'], ser_be, log_dir, loggers, transceivers, ver_queries)
|
||||||
|
|
||||||
# File sources → one shared FileBackend (no secondary logging)
|
# File sources → one shared FileBackend (no secondary logging)
|
||||||
file_sources = [s for s in sources if s['type'] == 'file']
|
file_sources = [s for s in sources if s['type'] == 'file']
|
||||||
@@ -182,7 +191,8 @@ if __name__ == '__main__':
|
|||||||
name = source_id_from_path(s['path'])
|
name = source_id_from_path(s['path'])
|
||||||
if name:
|
if name:
|
||||||
wire_source(name, file_be, log_dir=None, loggers=loggers,
|
wire_source(name, file_be, log_dir=None, loggers=loggers,
|
||||||
transceivers=transceivers, pacer_delay=delay)
|
transceivers=transceivers, ver_queries=ver_queries,
|
||||||
|
pacer_delay=delay)
|
||||||
|
|
||||||
signal.signal(signal.SIGINT,
|
signal.signal(signal.SIGINT,
|
||||||
lambda sig, frm: handler(sig, frm, backends, loggers))
|
lambda sig, frm: handler(sig, frm, backends, loggers))
|
||||||
@@ -191,6 +201,21 @@ if __name__ == '__main__':
|
|||||||
be.connect()
|
be.connect()
|
||||||
be.start()
|
be.start()
|
||||||
|
|
||||||
|
# Poll MON-VER once per live (network/serial) source, off the main thread
|
||||||
|
# since UbxQuery.poll() blocks until the response frame arrives.
|
||||||
|
def report_version(name: str, query: UbxQuery):
|
||||||
|
ver = query.poll()
|
||||||
|
if ver is None:
|
||||||
|
return
|
||||||
|
ext = '; '.join(e['extension'] for e in ver['extensions'])
|
||||||
|
print(f"{name}: device hw={ver['hw_version']} sw={ver['sw_version']}"
|
||||||
|
+ (f" ({ext})" if ext else ""))
|
||||||
|
|
||||||
|
for s in net_sources + serial_sources:
|
||||||
|
name = s['name']
|
||||||
|
threading.Thread(target=report_version, args=(name, ver_queries[name]),
|
||||||
|
daemon=True).start()
|
||||||
|
|
||||||
# Poll loop – only meaningful for network/serial sources
|
# Poll loop – only meaningful for network/serial sources
|
||||||
net_names = [s['name'] for s in net_sources]
|
net_names = [s['name'] for s in net_sources]
|
||||||
if net_names:
|
if net_names:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
from ubx.messages.msg import UbxMsg
|
from ubx.messages.msg import UbxMsg
|
||||||
from ubx.messages.nav import Sig, Sat
|
from ubx.messages.nav import Sig, Sat
|
||||||
from ubx.messages.rxm import RawX
|
from ubx.messages.rxm import RawX
|
||||||
|
from ubx.messages.mon import Ver
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import struct
|
||||||
|
from ubx.messages.msg import UbxMsg
|
||||||
|
|
||||||
|
class Ver(UbxMsg):
|
||||||
|
def __init__(self):
|
||||||
|
UbxMsg.__init__(self, 0x0A, 0x04)
|
||||||
|
|
||||||
|
class Group1:
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes):
|
||||||
|
obj = Ver.Group1()
|
||||||
|
obj.extension = data[0:30].split(b"\x00", 1)[0].decode("ascii", errors="replace")
|
||||||
|
return obj.__dict__
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes):
|
||||||
|
obj = Ver()
|
||||||
|
obj.sw_version = data[0:30].split(b"\x00", 1)[0].decode("ascii", errors="replace")
|
||||||
|
obj.hw_version = data[30:40].split(b"\x00", 1)[0].decode("ascii", errors="replace")
|
||||||
|
obj.extensions = []
|
||||||
|
offset = 40
|
||||||
|
remain = len(data) - offset
|
||||||
|
group_size = 30
|
||||||
|
n = offset
|
||||||
|
while remain >= group_size:
|
||||||
|
extension = cls.Group1.from_bytes(data[n:n + group_size])
|
||||||
|
n += group_size
|
||||||
|
remain -= group_size
|
||||||
|
obj.extensions.append(extension)
|
||||||
|
if remain:
|
||||||
|
print(f"Ver.from_bytes: {remain} trailing bytes unparsed (payload size mismatch)")
|
||||||
|
return obj.__dict__
|
||||||
+8
-13
@@ -3,30 +3,25 @@ from ubx.ubx import frame_parse, frame_create
|
|||||||
from ubx.messages.msg import UbxMsg
|
from ubx.messages.msg import UbxMsg
|
||||||
from transceiver import Transceiver
|
from transceiver import Transceiver
|
||||||
from ubx.listener import UbxListener
|
from ubx.listener import UbxListener
|
||||||
from threading import BoundedSemaphore
|
from threading import Event
|
||||||
|
|
||||||
class UbxQuery(UbxListener):
|
class UbxQuery(UbxListener):
|
||||||
def __init__(self, transceiver: Transceiver, msg: UbxMsg):
|
def __init__(self, transceiver: Transceiver, msg: UbxMsg):
|
||||||
UbxListener.__init__(self, msg)
|
UbxListener.__init__(self, msg)
|
||||||
self.transceiver = transceiver
|
self.transceiver = transceiver
|
||||||
self.sema = BoundedSemaphore()
|
self.result = None
|
||||||
|
self.event = Event()
|
||||||
|
|
||||||
def on_recv(self, msg: MsgContainer):
|
def on_recv(self, msg: MsgContainer):
|
||||||
if self.is_msg(msg):
|
if self.is_msg(msg):
|
||||||
data, hdr = frame_parse(msg.data)
|
data, hdr = frame_parse(msg.data)
|
||||||
msg_parsed = self.msg.from_bytes(data)
|
self.result = self.msg.from_bytes(data)
|
||||||
self.msg = msg_parsed
|
self.event.set()
|
||||||
self.sema.release()
|
|
||||||
|
|
||||||
def poll(self):
|
def poll(self):
|
||||||
|
self.event.clear()
|
||||||
frame = frame_create(self.msg.mclass, self.msg.mid, b'')
|
frame = frame_create(self.msg.mclass, self.msg.mid, b'')
|
||||||
self.transceiver.send(frame)
|
self.transceiver.send(frame)
|
||||||
self.sema.acquire()
|
self.event.wait()
|
||||||
print(self.msg)
|
return self.result
|
||||||
|
|
||||||
def query(self, data: bytes):
|
|
||||||
frame = frame_create(0x01, 0x35, b'')
|
|
||||||
self.transceiver.send(frame)
|
|
||||||
self.sema.acquire()
|
|
||||||
print(self.msg)
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user