283 lines
11 KiB
Python
283 lines
11 KiB
Python
import sys, os
|
||
|
||
_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
_gui = os.path.dirname(os.path.abspath(__file__))
|
||
for _p in [_root, _gui]:
|
||
if _p not in sys.path:
|
||
sys.path.insert(0, _p)
|
||
|
||
from PyQt5.QtWidgets import (
|
||
QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLabel,
|
||
QLineEdit, QComboBox, QSpinBox, QStackedWidget, QScrollArea,
|
||
QFrame, QMessageBox, QSizePolicy,
|
||
)
|
||
from PyQt5.QtCore import pyqtSignal, Qt
|
||
|
||
from data_model import ReceiverManager, ConnectionConfig
|
||
|
||
BAUD_RATES = ['4800', '9600', '19200', '38400', '57600', '115200',
|
||
'230400', '460800', '921600']
|
||
|
||
|
||
class _StatusDot(QLabel):
|
||
def __init__(self):
|
||
super().__init__("●")
|
||
self.setFixedWidth(20)
|
||
self.setAlignment(Qt.AlignCenter)
|
||
self.set_connected(False)
|
||
|
||
def set_connected(self, connected: bool):
|
||
color = '#4CAF50' if connected else '#9E9E9E'
|
||
self.setStyleSheet(f"color: {color}; font-size: 16px;")
|
||
|
||
|
||
class ConnectionRow(QWidget):
|
||
remove_requested = pyqtSignal(object)
|
||
|
||
def __init__(self, model: ReceiverManager, config: ConnectionConfig = None):
|
||
super().__init__()
|
||
self._model = model
|
||
self._rid: str = None
|
||
self._connected = False
|
||
|
||
layout = QHBoxLayout(self)
|
||
layout.setContentsMargins(4, 2, 4, 2)
|
||
layout.setSpacing(6)
|
||
|
||
# ── History dropdown ─────────────────────────────────────────────────
|
||
self._hist_combo = QComboBox()
|
||
self._hist_combo.setPlaceholderText("History ▾")
|
||
self._hist_combo.setMinimumWidth(150)
|
||
self._hist_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
||
self._hist_combo.currentIndexChanged.connect(self._load_history)
|
||
layout.addWidget(self._hist_combo)
|
||
|
||
sep = QFrame()
|
||
sep.setFrameShape(QFrame.VLine)
|
||
sep.setFrameShadow(QFrame.Sunken)
|
||
layout.addWidget(sep)
|
||
|
||
# ── Receiver ID ──────────────────────────────────────────────────────
|
||
layout.addWidget(QLabel("ID:"))
|
||
self._id_edit = QLineEdit()
|
||
self._id_edit.setPlaceholderText("receiver-id")
|
||
self._id_edit.setFixedWidth(110)
|
||
layout.addWidget(self._id_edit)
|
||
|
||
# ── Connection type ──────────────────────────────────────────────────
|
||
self._type_combo = QComboBox()
|
||
self._type_combo.addItems(['TCP', 'Serial'])
|
||
self._type_combo.currentIndexChanged.connect(self._on_type_changed)
|
||
layout.addWidget(self._type_combo)
|
||
|
||
# ── Stacked: TCP page / Serial page ─────────────────────────────────
|
||
self._stack = QStackedWidget()
|
||
|
||
# TCP page
|
||
tcp_w = QWidget()
|
||
tcp_l = QHBoxLayout(tcp_w)
|
||
tcp_l.setContentsMargins(0, 0, 0, 0)
|
||
tcp_l.setSpacing(4)
|
||
tcp_l.addWidget(QLabel("Host:"))
|
||
self._host_edit = QLineEdit("192.168.1.1")
|
||
self._host_edit.setFixedWidth(130)
|
||
tcp_l.addWidget(self._host_edit)
|
||
tcp_l.addWidget(QLabel("Port:"))
|
||
self._port_spin = QSpinBox()
|
||
self._port_spin.setRange(1, 65535)
|
||
self._port_spin.setValue(8721)
|
||
self._port_spin.setFixedWidth(75)
|
||
tcp_l.addWidget(self._port_spin)
|
||
self._stack.addWidget(tcp_w)
|
||
|
||
# Serial page
|
||
ser_w = QWidget()
|
||
ser_l = QHBoxLayout(ser_w)
|
||
ser_l.setContentsMargins(0, 0, 0, 0)
|
||
ser_l.setSpacing(4)
|
||
ser_l.addWidget(QLabel("Port:"))
|
||
self._ser_combo = QComboBox()
|
||
self._ser_combo.setEditable(True)
|
||
self._ser_combo.setMinimumWidth(130)
|
||
self._populate_serial_ports()
|
||
ser_l.addWidget(self._ser_combo)
|
||
ser_l.addWidget(QLabel("Baud:"))
|
||
self._baud_combo = QComboBox()
|
||
self._baud_combo.addItems(BAUD_RATES)
|
||
self._baud_combo.setCurrentText('115200')
|
||
ser_l.addWidget(self._baud_combo)
|
||
self._stack.addWidget(ser_w)
|
||
|
||
layout.addWidget(self._stack)
|
||
layout.addStretch()
|
||
|
||
# ── Status / buttons ─────────────────────────────────────────────────
|
||
self._dot = _StatusDot()
|
||
layout.addWidget(self._dot)
|
||
|
||
self._conn_btn = QPushButton("Connect")
|
||
self._conn_btn.setFixedWidth(95)
|
||
self._conn_btn.clicked.connect(self._toggle)
|
||
layout.addWidget(self._conn_btn)
|
||
|
||
rm_btn = QPushButton("Remove")
|
||
rm_btn.setFixedWidth(70)
|
||
rm_btn.clicked.connect(lambda: self.remove_requested.emit(self))
|
||
layout.addWidget(rm_btn)
|
||
|
||
if config:
|
||
self.apply_config(config)
|
||
self.refresh_history()
|
||
|
||
# ── Serial port detection ────────────────────────────────────────────────
|
||
|
||
def _populate_serial_ports(self):
|
||
try:
|
||
import serial.tools.list_ports
|
||
ports = [p.device for p in serial.tools.list_ports.comports()]
|
||
except Exception:
|
||
ports = []
|
||
self._ser_combo.addItems(ports or ['/dev/ttyUSB0', 'COM1'])
|
||
|
||
# ── Config helpers ───────────────────────────────────────────────────────
|
||
|
||
def apply_config(self, config: ConnectionConfig):
|
||
self._id_edit.setText(config.receiver_id)
|
||
self._type_combo.setCurrentIndex(0 if config.conn_type == 'tcp' else 1)
|
||
self._host_edit.setText(config.host)
|
||
self._port_spin.setValue(config.port)
|
||
if config.serial_port:
|
||
idx = self._ser_combo.findText(config.serial_port)
|
||
self._ser_combo.setCurrentIndex(idx) if idx >= 0 \
|
||
else self._ser_combo.setCurrentText(config.serial_port)
|
||
self._baud_combo.setCurrentText(str(config.baud_rate))
|
||
|
||
def get_config(self) -> ConnectionConfig:
|
||
t = 'tcp' if self._type_combo.currentIndex() == 0 else 'serial'
|
||
return ConnectionConfig(
|
||
receiver_id = self._id_edit.text().strip(),
|
||
conn_type = t,
|
||
host = self._host_edit.text().strip(),
|
||
port = self._port_spin.value(),
|
||
serial_port = self._ser_combo.currentText().strip(),
|
||
baud_rate = int(self._baud_combo.currentText()),
|
||
)
|
||
|
||
def get_receiver_id(self) -> str:
|
||
return self._rid
|
||
|
||
def refresh_history(self):
|
||
self._hist_combo.blockSignals(True)
|
||
self._hist_combo.clear()
|
||
self._hist_combo.addItem("History ▾")
|
||
for cfg in self._model.get_lru():
|
||
self._hist_combo.addItem(cfg.display_str(), userData=cfg)
|
||
self._hist_combo.blockSignals(False)
|
||
|
||
def set_connected(self, connected: bool):
|
||
self._connected = connected
|
||
self._dot.set_connected(connected)
|
||
self._conn_btn.setText("Disconnect" if connected else "Connect")
|
||
for w in [self._id_edit, self._type_combo, self._host_edit,
|
||
self._port_spin, self._ser_combo, self._baud_combo]:
|
||
w.setEnabled(not connected)
|
||
|
||
# ── Slots ────────────────────────────────────────────────────────────────
|
||
|
||
def _on_type_changed(self, idx: int):
|
||
self._stack.setCurrentIndex(idx)
|
||
|
||
def _load_history(self, idx: int):
|
||
if idx <= 0:
|
||
return
|
||
cfg = self._hist_combo.itemData(idx)
|
||
if cfg:
|
||
self.apply_config(cfg)
|
||
self._hist_combo.blockSignals(True)
|
||
self._hist_combo.setCurrentIndex(0)
|
||
self._hist_combo.blockSignals(False)
|
||
|
||
def _toggle(self):
|
||
if self._connected:
|
||
if self._rid:
|
||
self._model.disconnect_receiver(self._rid)
|
||
return
|
||
|
||
config = self.get_config()
|
||
if not config.receiver_id:
|
||
QMessageBox.warning(self, "GNSS Monitor",
|
||
"Receiver-ID darf nicht leer sein.")
|
||
return
|
||
if config.receiver_id in self._model.get_receiver_ids():
|
||
QMessageBox.warning(self, "GNSS Monitor",
|
||
f"ID '{config.receiver_id}' wird bereits verwendet.")
|
||
return
|
||
|
||
self._model.add_receiver(config)
|
||
self._rid = config.receiver_id
|
||
try:
|
||
self._model.connect_receiver(config.receiver_id)
|
||
except Exception as exc:
|
||
QMessageBox.critical(self, "Verbindungsfehler", str(exc))
|
||
self._model.remove_receiver(config.receiver_id)
|
||
self._rid = None
|
||
|
||
|
||
# ── Connection tab ────────────────────────────────────────────────────────────
|
||
|
||
class ConnectionTab(QWidget):
|
||
def __init__(self, model: ReceiverManager):
|
||
super().__init__()
|
||
self._model = model
|
||
self._rows: list = []
|
||
|
||
outer = QVBoxLayout(self)
|
||
outer.setContentsMargins(8, 8, 8, 8)
|
||
outer.setSpacing(6)
|
||
|
||
# Scroll area
|
||
self._scroll = QScrollArea()
|
||
self._scroll.setWidgetResizable(True)
|
||
self._scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||
|
||
self._inner = QWidget()
|
||
self._vbox = QVBoxLayout(self._inner)
|
||
self._vbox.setContentsMargins(0, 0, 0, 0)
|
||
self._vbox.setSpacing(2)
|
||
self._vbox.addStretch()
|
||
|
||
self._scroll.setWidget(self._inner)
|
||
outer.addWidget(self._scroll)
|
||
|
||
add_btn = QPushButton("+ Add Connection")
|
||
add_btn.setMaximumWidth(180)
|
||
add_btn.clicked.connect(lambda: self._add_row())
|
||
outer.addWidget(add_btn, alignment=Qt.AlignLeft)
|
||
|
||
model.connection_changed.connect(self._on_connection_changed)
|
||
|
||
def _add_row(self, config: ConnectionConfig = None):
|
||
row = ConnectionRow(self._model, config)
|
||
row.remove_requested.connect(self._remove_row)
|
||
# Insert before the trailing stretch
|
||
self._vbox.insertWidget(self._vbox.count() - 1, row)
|
||
self._rows.append(row)
|
||
|
||
def _remove_row(self, row: ConnectionRow):
|
||
rid = row.get_receiver_id()
|
||
if rid:
|
||
self._model.remove_receiver(rid)
|
||
self._vbox.removeWidget(row)
|
||
row.deleteLater()
|
||
if row in self._rows:
|
||
self._rows.remove(row)
|
||
|
||
def _on_connection_changed(self, rid: str, connected: bool):
|
||
for row in self._rows:
|
||
if row.get_receiver_id() == rid:
|
||
row.set_connected(connected)
|
||
if connected:
|
||
for r in self._rows:
|
||
r.refresh_history()
|
||
break
|