Files
skyplot/plot_tab.py
T
jens f0d49a3a7c - added tabs
- added plot tab
2026-05-18 21:48:04 +02:00

299 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)