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