- added tabs

- added plot tab
This commit is contained in:
2026-05-18 21:48:04 +02:00
parent 25d6d3dc13
commit f0d49a3a7c
3 changed files with 383 additions and 2 deletions
+298
View File
@@ -0,0 +1,298 @@
import pyqtgraph as pg
from pyqtgraph import PlotWidget
from datetime import datetime
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QComboBox, QLabel
from PyQt5.QtCore import Qt, QSettings, pyqtSignal
from defines import GNSS_NAMES
pg.setConfigOption("background", "#0d1b2a")
pg.setConfigOption("foreground", "#cccccc")
PLOT_Y_FIELDS_TOP = ["pdop", "cno", "azim", "elev"]
PLOT_Y_LABELS_TOP = {
"pdop": "PDOP",
"cno": "C/N₀ (dB)",
"azim": "Azimuth (°)",
"elev": "Elevation (°)",
}
MAX_HISTORY = 300
CURVE_PALETTE = [
"#1E90FF", "#00C853", "#E53935", "#FFA500",
"#9C27B0", "#00BCD4", "#FF4081", "#FFD600",
]
# ---------------------------------------------------------------------------
# Persistenz-Hilfsklasse für Plot-Einstellungen
# ---------------------------------------------------------------------------
class PlotSettingsStore:
"""
Speichert/lädt die Einstellungen aller SinglePlotPanels via QSettings.
Format pro Panel:
plot_settings/panel_<n>/field → str (z.B. "cno")
plot_settings/panel_<n>/sv → str (z.B. "GPS-12" oder "Alle")
"""
_KEY = "plot_settings"
def __init__(self):
self._cfg = QSettings("gnss_skyplot", "connections")
def save(self, panels: list) -> None:
self._cfg.beginGroup(self._KEY)
self._cfg.remove("") # Gruppe leeren
for idx, panel in enumerate(panels):
self._cfg.setValue(f"panel_{idx}/field", panel.current_field())
self._cfg.setValue(f"panel_{idx}/sv", panel.current_sv())
self._cfg.endGroup()
self._cfg.sync()
def load(self, panels: list) -> None:
self._cfg.beginGroup(self._KEY)
for idx, panel in enumerate(panels):
field = self._cfg.value(f"panel_{idx}/field", "cno")
sv = self._cfg.value(f"panel_{idx}/sv", "Alle")
panel.restore(field, sv)
self._cfg.endGroup()
# ---------------------------------------------------------------------------
# Einzelner Plot-Panel
# ---------------------------------------------------------------------------
class SinglePlotPanel(QWidget):
"""Ein einzelner 2D-Plot mit Y-Auswahl (Feld + SVid)."""
# Signal: Einstellung wurde geändert → PlotTab speichert
settings_changed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._history: dict[str, dict] = {} # key → {x:[], y:[], active: bool}
self._curves: dict[str, pg.PlotDataItem] = {}
self._field = "cno"
self._svid_filter = "Alle"
self._build_ui()
# ── UI ────────────────────────────────────────────────────────────────
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(4, 4, 4, 4)
layout.setSpacing(4)
# Steuerleiste
ctrl = QHBoxLayout()
ctrl.addWidget(QLabel("Y:"))
self._y_combo = QComboBox()
self._y_combo.setFixedWidth(120)
for key in PLOT_Y_FIELDS_TOP:
self._y_combo.addItem(PLOT_Y_LABELS_TOP[key], key)
self._y_combo.currentIndexChanged.connect(self._on_field_changed)
ctrl.addWidget(self._y_combo)
ctrl.addWidget(QLabel("SV:"))
self._sv_combo = QComboBox()
self._sv_combo.setFixedWidth(110)
self._sv_combo.addItem("Alle", "Alle")
self._sv_combo.currentIndexChanged.connect(self._on_sv_changed)
ctrl.addWidget(self._sv_combo)
ctrl.addStretch()
layout.addLayout(ctrl)
# Plot-Widget
self._plot = PlotWidget()
self._plot.showGrid(x=True, y=True, alpha=0.3)
self._plot.setLabel("left", PLOT_Y_LABELS_TOP[self._field])
self._plot.setLabel("bottom", "Zeit")
self._plot.addLegend(offset=(5, 5))
self._plot.setAxisItems({"bottom": pg.DateAxisItem(orientation="bottom")})
layout.addWidget(self._plot)
# ── Öffentliche Getter für Persistenz ─────────────────────────────────
def current_field(self) -> str:
return self._field
def current_sv(self) -> str:
return self._svid_filter
# ── Einstellungen wiederherstellen ────────────────────────────────────
def restore(self, field: str, sv: str) -> None:
# Y-Combo
idx = self._y_combo.findData(field)
if idx >= 0:
self._y_combo.blockSignals(True)
self._y_combo.setCurrentIndex(idx)
self._y_combo.blockSignals(False)
self._field = field
self._plot.setLabel("left", PLOT_Y_LABELS_TOP.get(field, field))
# SV-Combo: Der SV-Wert wird gesetzt, sobald die Combo befüllt ist.
# Wir merken ihn vor, _update_sv_combo() wendet ihn an.
self._svid_filter = sv
# ── Slots ─────────────────────────────────────────────────────────────
def _on_field_changed(self, _idx: int):
self._field = self._y_combo.currentData()
self._plot.setLabel("left", PLOT_Y_LABELS_TOP[self._field])
self._history.clear()
self._curves.clear()
self._plot.clear()
self._plot.addLegend(offset=(5, 5))
# SV-Combo zurücksetzen
self._sv_combo.blockSignals(True)
self._sv_combo.clear()
self._sv_combo.addItem("Alle", "Alle")
self._sv_combo.blockSignals(False)
self._svid_filter = "Alle"
self.settings_changed.emit()
def _on_sv_changed(self, _idx: int):
self._svid_filter = self._sv_combo.currentData() or "Alle"
self._redraw()
self.settings_changed.emit()
# ── Daten-Update ──────────────────────────────────────────────────────
def update_data(self, sats: list[dict], pdop: float | None, ts: float):
active_keys: set[str] = set()
if self._field == "pdop":
active_keys.add("PDOP")
self._push("PDOP", ts, pdop if pdop is not None else float("nan"))
else:
for sat in sats:
key = f"{GNSS_NAMES.get(sat['gnssId'], '?')}-{sat['svId']}"
active_keys.add(key)
self._push(key, ts, float(sat.get(self._field, 0)))
# Markiere inaktive SVs
for key, h in self._history.items():
h["active"] = key in active_keys
self._update_sv_combo()
self._redraw()
def _push(self, key: str, ts: float, val: float):
if key not in self._history:
self._history[key] = {"x": [], "y": [], "active": True}
h = self._history[key]
h["x"].append(ts)
h["y"].append(val)
h["active"] = True
if len(h["x"]) > MAX_HISTORY:
h["x"] = h["x"][-MAX_HISTORY:]
h["y"] = h["y"][-MAX_HISTORY:]
def _update_sv_combo(self):
known = {self._sv_combo.itemData(i)
for i in range(self._sv_combo.count())}
self._sv_combo.blockSignals(True)
for key in self._history:
if key not in known:
self._sv_combo.addItem(key, key)
# Gespeicherten Filter anwenden, falls SV jetzt bekannt ist
if self._svid_filter != "Alle":
idx = self._sv_combo.findData(self._svid_filter)
if idx >= 0 and self._sv_combo.currentIndex() != idx:
self._sv_combo.setCurrentIndex(idx)
self._sv_combo.blockSignals(False)
# ── Zeichnen ──────────────────────────────────────────────────────────
def _redraw(self):
show_keys = (
list(self._history.keys())
if self._svid_filter == "Alle"
else ([self._svid_filter] if self._svid_filter in self._history else [])
)
# Kurven entfernen, die nicht mehr im Filter sind
for key in list(self._curves.keys()):
if key not in show_keys:
self._plot.removeItem(self._curves.pop(key))
for idx, key in enumerate(show_keys):
if key not in self._history:
continue
h = self._history[key]
color = CURVE_PALETTE[idx % len(CURVE_PALETTE)]
active = h.get("active", True)
if active:
pen = pg.mkPen(color=color, width=1, style=Qt.SolidLine)
else:
# Inaktiver SV: gestrichelte Linie bei y=0
pen = pg.mkPen(color=color, width=1, style=Qt.DashLine)
x_data = h["x"] if h["x"] else [datetime.now().timestamp()]
y_data = [0.0] * len(x_data)
if key in self._curves:
self._curves[key].setData(x_data, y_data)
self._curves[key].setPen(pen)
else:
self._curves[key] = self._plot.plot(
x_data, y_data, pen=pen, name=f"{key} (weg)"
)
continue
if key not in self._curves:
self._curves[key] = self._plot.plot(
h["x"], h["y"], pen=pen, name=key
)
else:
self._curves[key].setData(h["x"], h["y"])
self._curves[key].setPen(pen)
# ---------------------------------------------------------------------------
# Plot-Tab (2 × 4 Grid, scrollbar)
# ---------------------------------------------------------------------------
class PlotTab(QWidget):
NUM_PLOTS = 8
def __init__(self, parent=None):
super().__init__(parent)
self._panels: list[SinglePlotPanel] = []
self._store = PlotSettingsStore()
self._build_ui()
self._store.load(self._panels) # Einstellungen wiederherstellen
def _build_ui(self):
from PyQt5.QtWidgets import QScrollArea, QGridLayout
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setStyleSheet("QScrollArea { border: none; }")
grid_widget = QWidget()
grid = QGridLayout(grid_widget)
grid.setSpacing(6)
grid.setContentsMargins(6, 6, 6, 6)
for i in range(self.NUM_PLOTS):
panel = SinglePlotPanel()
panel.settings_changed.connect(self._save_settings)
self._panels.append(panel)
row, col = divmod(i, 2)
grid.addWidget(panel, row, col)
scroll.setWidget(grid_widget)
root = QVBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
root.addWidget(scroll)
def _save_settings(self):
self._store.save(self._panels)
def update_data(self, sats: list[dict], pdop: float | None = None):
ts = datetime.now().timestamp()
for panel in self._panels:
panel.update_data(sats, pdop, ts)