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) # ── 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) # ── 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 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 _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 with rows ───────────────────────────────────────────── 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) # ── Bottom bar: history combo (left) + Add button (right) ───────────── bottom = QHBoxLayout() bottom.setSpacing(8) self._hist_combo = QComboBox() self._hist_combo.setPlaceholderText("Select from history…") self._hist_combo.setMinimumWidth(260) self._hist_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self._refresh_history_combo() bottom.addWidget(self._hist_combo) add_btn = QPushButton("+ Add") add_btn.setFixedWidth(90) add_btn.clicked.connect(self._on_add) bottom.addWidget(add_btn) bottom.addStretch() outer.addLayout(bottom) model.connection_changed.connect(self._on_connection_changed) # ── History combo ───────────────────────────────────────────────────────── def _refresh_history_combo(self): self._hist_combo.blockSignals(True) current = self._hist_combo.currentData() self._hist_combo.clear() for cfg in self._model.get_lru(): self._hist_combo.addItem(cfg.display_str(), userData=cfg) # restore selection if still present if current: idx = self._hist_combo.findText(current.display_str()) if idx >= 0: self._hist_combo.setCurrentIndex(idx) self._hist_combo.blockSignals(False) # ── Row management ──────────────────────────────────────────────────────── def _on_add(self): cfg = self._hist_combo.currentData() # None when placeholder shown self._add_row(cfg) self._hist_combo.setCurrentIndex(-1) # reset to placeholder def _add_row(self, config: ConnectionConfig = None): row = ConnectionRow(self._model, config) row.remove_requested.connect(self._remove_row) 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) break if connected: self._refresh_history_combo()