sky tab: add per-receiver info bar with pDOP, visible and used satellite counts

One card per receiver showing receiver ID, pDOP, visible satellites and
used satellites. Cards are added/removed dynamically with receivers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 12:27:36 +02:00
co-authored by Claude Sonnet 4.6
parent 07444fd299
commit 495f4c15c9
+57 -5
View File
@@ -9,7 +9,7 @@ for _p in [_root, _gui]:
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QHBoxLayout, QVBoxLayout, QLabel, QListWidget, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QListWidget,
QListWidgetItem, QTableWidget, QTableWidgetItem, QHeaderView, QListWidgetItem, QTableWidget, QTableWidgetItem, QHeaderView,
QSplitter, QGroupBox, QSplitter, QGroupBox, QFrame,
) )
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor, QBrush from PyQt5.QtGui import QColor, QBrush
@@ -24,8 +24,11 @@ class SkyTab(QWidget):
def __init__(self, model: ReceiverManager): def __init__(self, model: ReceiverManager):
super().__init__() super().__init__()
self._model = model self._model = model
self._store: dict = {} # (rid, gnss_id, sv_id) -> SatelliteData self._store: dict = {} # (rid, gnss_id, sv_id) -> SatelliteData
self._table_keys: list = [] # current row order as (rid, gnss_id, sv_id) self._table_keys: list = [] # current row order as (rid, gnss_id, sv_id)
self._pdop: dict = {} # rid -> float
self._info_cards: dict = {} # rid -> QFrame
self._info_labels: dict = {} # rid -> (lbl_pdop, lbl_visible, lbl_used)
outer = QVBoxLayout(self) outer = QVBoxLayout(self)
outer.setContentsMargins(4, 4, 4, 4) outer.setContentsMargins(4, 4, 4, 4)
@@ -44,6 +47,16 @@ class SkyTab(QWidget):
src_bl.addWidget(self._src_list) src_bl.addWidget(self._src_list)
outer.addWidget(src_box) outer.addWidget(src_box)
# ── Info bar (one card per receiver) ──────────────────────────────────
info_frame = QFrame()
info_frame.setFrameShape(QFrame.StyledPanel)
info_frame.setMaximumHeight(42)
self._info_layout = QHBoxLayout(info_frame)
self._info_layout.setContentsMargins(6, 2, 6, 2)
self._info_layout.setSpacing(12)
self._info_layout.addStretch()
outer.addWidget(info_frame)
# ── Sky plot + satellite table ───────────────────────────────────────── # ── Sky plot + satellite table ─────────────────────────────────────────
splitter = QSplitter(Qt.Horizontal) splitter = QSplitter(Qt.Horizontal)
@@ -74,6 +87,7 @@ class SkyTab(QWidget):
model.receiver_added.connect(self._on_receiver_added) model.receiver_added.connect(self._on_receiver_added)
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)
# ── Source list management ──────────────────────────────────────────────── # ── Source list management ────────────────────────────────────────────────
@@ -84,6 +98,22 @@ class SkyTab(QWidget):
item.setData(Qt.UserRole, rid) item.setData(Qt.UserRole, rid)
self._src_list.addItem(item) self._src_list.addItem(item)
# Info card for this receiver
card = QFrame()
card.setFrameShape(QFrame.Box)
cl = QHBoxLayout(card)
cl.setContentsMargins(8, 1, 8, 1)
cl.setSpacing(10)
name_lbl = QLabel(f"<b>{rid}</b>")
lbl_pdop = QLabel("pDOP: —")
lbl_visible = QLabel("Visible: —")
lbl_used = QLabel("Used: —")
for w in (name_lbl, lbl_pdop, lbl_visible, lbl_used):
cl.addWidget(w)
self._info_cards[rid] = card
self._info_labels[rid] = (lbl_pdop, lbl_visible, lbl_used)
self._info_layout.insertWidget(self._info_layout.count() - 1, card)
def _on_receiver_removed(self, rid: str): def _on_receiver_removed(self, rid: str):
for i in range(self._src_list.count()): for i in range(self._src_list.count()):
if self._src_list.item(i).data(Qt.UserRole) == rid: if self._src_list.item(i).data(Qt.UserRole) == rid:
@@ -92,6 +122,12 @@ class SkyTab(QWidget):
keys = [k for k in self._store if k[0] == rid] keys = [k for k in self._store if k[0] == rid]
for k in keys: for k in keys:
del self._store[k] del self._store[k]
self._pdop.pop(rid, None)
card = self._info_cards.pop(rid, None)
if card:
self._info_layout.removeWidget(card)
card.deleteLater()
self._info_labels.pop(rid, None)
self._refresh_immediate() self._refresh_immediate()
def _selected_rids(self) -> set: def _selected_rids(self) -> set:
@@ -107,22 +143,38 @@ class SkyTab(QWidget):
return [(k[0], self._store[k]) return [(k[0], self._store[k])
for k in self._store if k[0] in selected] for k in self._store if k[0] in selected]
# ── Info bar ──────────────────────────────────────────────────────────────
def _update_info(self):
for rid, (lbl_pdop, lbl_visible, lbl_used) in self._info_labels.items():
sats = [self._store[k] for k in self._store if k[0] == rid]
n_visible = len(sats)
n_used = sum(1 for s in sats if s.used_in_fix)
pdop = self._pdop.get(rid)
lbl_pdop.setText( f"pDOP: {pdop:.1f}" if pdop is not None else "pDOP: —")
lbl_visible.setText(f"Visible: {n_visible}")
lbl_used.setText( f"Used: {n_used}")
# ── Data update ─────────────────────────────────────────────────────────── # ── Data update ───────────────────────────────────────────────────────────
def _on_sat_update(self, rid: str, sats: list): def _on_sat_update(self, rid: str, sats: list):
for sat in sats: for sat in sats:
self._store[(rid, sat.gnss_id, sat.sv_id)] = sat self._store[(rid, sat.gnss_id, sat.sv_id)] = sat
# Sky widget: always submit latest (render is off-thread, trivially cheap)
self._sky.update_data(self._visible_sats()) self._sky.update_data(self._visible_sats())
# Table: schedule a rebuild if one isn't already pending self._update_info()
if not self._table_timer.isActive(): if not self._table_timer.isActive():
self._table_timer.start() self._table_timer.start()
def _on_pdop_update(self, rid: str, pdop: float, _ts: float):
self._pdop[rid] = pdop
self._update_info()
def _refresh_immediate(self): def _refresh_immediate(self):
"""Called on source-checkbox toggle or receiver removal — rebuild now.""" """Called on source-checkbox toggle or receiver removal — rebuild now."""
self._table_timer.stop() self._table_timer.stop()
visible = self._visible_sats() visible = self._visible_sats()
self._sky.update_data(visible) self._sky.update_data(visible)
self._update_info()
self._rebuild_table(visible) self._rebuild_table(visible)
def _rebuild_table_from_store(self): def _rebuild_table_from_store(self):