- added tabs
- added plot tab
This commit is contained in:
+298
@@ -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)
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pyserial
|
pyserial
|
||||||
pyqt5
|
pyqt5
|
||||||
|
pyqtgraph
|
||||||
|
|||||||
+84
-2
@@ -8,7 +8,7 @@ TCP/IP-Verbindungen werden in einer persistenten LRU-Liste (max. 5) gespeichert.
|
|||||||
import sys
|
import sys
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget, QVBoxLayout,
|
QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||||
QHBoxLayout, QLabel, QStatusBar, QSplitter, )
|
QHBoxLayout, QLabel, QStatusBar, QSplitter, QTabWidget)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
from connection_panel import ConnectionPanel
|
from connection_panel import ConnectionPanel
|
||||||
@@ -16,7 +16,7 @@ from gnss_thread import GnssReader
|
|||||||
from sat_table import SatelliteTable
|
from sat_table import SatelliteTable
|
||||||
from sky_plot_widget import SkyPlotWidget
|
from sky_plot_widget import SkyPlotWidget
|
||||||
|
|
||||||
|
from plot_tab import PlotTab
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Hauptfenster
|
# Hauptfenster
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -35,6 +35,73 @@ class MainWindow(QMainWindow):
|
|||||||
root = QVBoxLayout(central)
|
root = QVBoxLayout(central)
|
||||||
root.setContentsMargins(8, 8, 8, 8)
|
root.setContentsMargins(8, 8, 8, 8)
|
||||||
|
|
||||||
|
# Haupt-Tabs
|
||||||
|
self._main_tabs = QTabWidget()
|
||||||
|
self._main_tabs.setStyleSheet("""
|
||||||
|
QTabBar::tab {
|
||||||
|
background: #1a3a5c;
|
||||||
|
color: #cccccc;
|
||||||
|
padding: 6px 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
QTabBar::tab:selected {
|
||||||
|
background: #2a5a8c;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ── Tab 1: Verbindung ─────────────────────────────────────────────────
|
||||||
|
conn_tab = QWidget()
|
||||||
|
conn_layout = QVBoxLayout(conn_tab)
|
||||||
|
conn_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
|
||||||
|
self._conn_panel = ConnectionPanel()
|
||||||
|
self._conn_panel.connect_requested.connect(self._connect)
|
||||||
|
self._conn_panel.disconnect_requested.connect(self._disconnect)
|
||||||
|
conn_layout.addWidget(self._conn_panel)
|
||||||
|
conn_layout.addStretch()
|
||||||
|
|
||||||
|
self._main_tabs.addTab(conn_tab, "🔌 Verbindung")
|
||||||
|
|
||||||
|
# ── Tab 2: Sky View ───────────────────────────────────────────────────
|
||||||
|
view_tab = QWidget()
|
||||||
|
view_layout = QVBoxLayout(view_tab)
|
||||||
|
view_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
|
||||||
|
# ── Tab 3: Plot ───────────────────────────────────────────────────
|
||||||
|
self._plot_tab = PlotTab()
|
||||||
|
self._main_tabs.addTab(self._plot_tab, "📈 Plot")
|
||||||
|
|
||||||
|
# Satellitenanzahl
|
||||||
|
self._sat_label = QLabel("Satelliten: –")
|
||||||
|
self._sat_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
|
self._sat_label.setStyleSheet("font-size: 13px; color: #7ab7d4;")
|
||||||
|
view_layout.addWidget(self._sat_label)
|
||||||
|
|
||||||
|
# Splitter: Sky Plot | Tabelle
|
||||||
|
splitter = QSplitter(Qt.Horizontal)
|
||||||
|
self._sky_plot = SkyPlotWidget()
|
||||||
|
self._table = SatelliteTable()
|
||||||
|
splitter.addWidget(self._sky_plot)
|
||||||
|
splitter.addWidget(self._table)
|
||||||
|
splitter.setSizes([540, 380])
|
||||||
|
view_layout.addWidget(splitter)
|
||||||
|
|
||||||
|
self._main_tabs.addTab(view_tab, "🛰 Sky View")
|
||||||
|
|
||||||
|
root.addWidget(self._main_tabs)
|
||||||
|
|
||||||
|
# Status Bar
|
||||||
|
self._status = QStatusBar()
|
||||||
|
self.setStatusBar(self._status)
|
||||||
|
self._status.showMessage("Nicht verbunden.")
|
||||||
|
|
||||||
|
def __build_ui(self):
|
||||||
|
central = QWidget()
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
root = QVBoxLayout(central)
|
||||||
|
root.setContentsMargins(8, 8, 8, 8)
|
||||||
|
|
||||||
# Obere Leiste
|
# Obere Leiste
|
||||||
top = QHBoxLayout()
|
top = QHBoxLayout()
|
||||||
|
|
||||||
@@ -65,6 +132,20 @@ class MainWindow(QMainWindow):
|
|||||||
self._status.showMessage("Nicht verbunden.")
|
self._status.showMessage("Nicht verbunden.")
|
||||||
|
|
||||||
def _connect(self, factory, label: str):
|
def _connect(self, factory, label: str):
|
||||||
|
self._reader = GnssReader(factory)
|
||||||
|
self._reader.satellites_updated.connect(self._on_satellites)
|
||||||
|
self._reader.error_occurred.connect(self._on_error)
|
||||||
|
self._reader.connected.connect(
|
||||||
|
lambda msg: (
|
||||||
|
self._status.showMessage(f"{msg}: {label}"),
|
||||||
|
self._main_tabs.setCurrentIndex(1), # → Sky View
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._reader.start()
|
||||||
|
self._conn_panel.set_connected(True)
|
||||||
|
self._status.showMessage(f"Verbinde mit {label} …")
|
||||||
|
|
||||||
|
def __connect(self, factory, label: str):
|
||||||
self._reader = GnssReader(factory)
|
self._reader = GnssReader(factory)
|
||||||
self._reader.satellites_updated.connect(self._on_satellites)
|
self._reader.satellites_updated.connect(self._on_satellites)
|
||||||
self._reader.error_occurred.connect(self._on_error)
|
self._reader.error_occurred.connect(self._on_error)
|
||||||
@@ -88,6 +169,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._sat_label.setText(f"Satelliten: {len(visible)} sichtbar, {used} genutzt")
|
self._sat_label.setText(f"Satelliten: {len(visible)} sichtbar, {used} genutzt")
|
||||||
self._sky_plot.update_satellites(visible)
|
self._sky_plot.update_satellites(visible)
|
||||||
self._table.update_satellites(visible)
|
self._table.update_satellites(visible)
|
||||||
|
self._plot_tab.update_data(visible) # ← neu
|
||||||
|
|
||||||
def _on_error(self, msg: str):
|
def _on_error(self, msg: str):
|
||||||
self._status.showMessage(f"Fehler: {msg}")
|
self._status.showMessage(f"Fehler: {msg}")
|
||||||
|
|||||||
Reference in New Issue
Block a user