From 495f4c15c9103374c4313cd4ededfc34185e4ce6 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sun, 24 May 2026 12:27:36 +0200 Subject: [PATCH] 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 --- gui/sky_tab.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/gui/sky_tab.py b/gui/sky_tab.py index b790c82..9ac4115 100644 --- a/gui/sky_tab.py +++ b/gui/sky_tab.py @@ -9,7 +9,7 @@ for _p in [_root, _gui]: from PyQt5.QtWidgets import ( QWidget, QHBoxLayout, QVBoxLayout, QLabel, QListWidget, QListWidgetItem, QTableWidget, QTableWidgetItem, QHeaderView, - QSplitter, QGroupBox, + QSplitter, QGroupBox, QFrame, ) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QColor, QBrush @@ -24,8 +24,11 @@ class SkyTab(QWidget): def __init__(self, model: ReceiverManager): super().__init__() self._model = model - self._store: dict = {} # (rid, gnss_id, sv_id) -> SatelliteData - self._table_keys: list = [] # current row order as (rid, gnss_id, sv_id) + self._store: dict = {} # (rid, gnss_id, sv_id) -> SatelliteData + 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.setContentsMargins(4, 4, 4, 4) @@ -44,6 +47,16 @@ class SkyTab(QWidget): src_bl.addWidget(self._src_list) 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 ───────────────────────────────────────── splitter = QSplitter(Qt.Horizontal) @@ -74,6 +87,7 @@ class SkyTab(QWidget): model.receiver_added.connect(self._on_receiver_added) model.receiver_removed.connect(self._on_receiver_removed) model.satellite_update.connect(self._on_sat_update) + model.pdop_update.connect(self._on_pdop_update) # ── Source list management ──────────────────────────────────────────────── @@ -84,6 +98,22 @@ class SkyTab(QWidget): item.setData(Qt.UserRole, rid) 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"{rid}") + 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): for i in range(self._src_list.count()): 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] for k in keys: 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() def _selected_rids(self) -> set: @@ -107,22 +143,38 @@ class SkyTab(QWidget): return [(k[0], self._store[k]) 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 ─────────────────────────────────────────────────────────── def _on_sat_update(self, rid: str, sats: list): for sat in sats: 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()) - # Table: schedule a rebuild if one isn't already pending + self._update_info() if not self._table_timer.isActive(): 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): """Called on source-checkbox toggle or receiver removal — rebuild now.""" self._table_timer.stop() visible = self._visible_sats() self._sky.update_data(visible) + self._update_info() self._rebuild_table(visible) def _rebuild_table_from_store(self):