Files
NmeaClient/gui/connection_tab.py
T
jensandClaude Sonnet 4.6 cd5508c4d0 gui: fix reconnect after disconnect on same row
disconnect_receiver left the rid in _states, causing the duplicate-ID
check to fire on reconnect. Use remove_receiver instead (which cleans
up _states/_configs) and reset _rid to None.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:23:18 +02:00

335 lines
13 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 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, QDoubleSpinBox, QStackedWidget,
QScrollArea, QFrame, QMessageBox, QSizePolicy, QFileDialog,
)
from PyQt5.QtCore import pyqtSignal, Qt
from data_model import ReceiverManager, ConnectionConfig
from backend.file_backend import source_id_from_path
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', 'File'])
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)
# File page
file_w = QWidget()
file_l = QHBoxLayout(file_w)
file_l.setContentsMargins(0, 0, 0, 0)
file_l.setSpacing(4)
file_l.addWidget(QLabel("Path:"))
self._file_edit = QLineEdit()
self._file_edit.setPlaceholderText("log file path…")
self._file_edit.setMinimumWidth(200)
self._file_edit.textChanged.connect(self._on_file_path_changed)
file_l.addWidget(self._file_edit)
browse_btn = QPushButton("Browse")
browse_btn.setFixedWidth(60)
browse_btn.clicked.connect(self._browse_file)
file_l.addWidget(browse_btn)
file_l.addWidget(QLabel("Delay:"))
self._delay_spin = QDoubleSpinBox()
self._delay_spin.setRange(0.0, 10.0)
self._delay_spin.setSingleStep(0.1)
self._delay_spin.setDecimals(1)
self._delay_spin.setValue(1.0)
self._delay_spin.setSuffix(" s")
self._delay_spin.setFixedWidth(72)
file_l.addWidget(self._delay_spin)
self._stack.addWidget(file_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)
# ── File browse ──────────────────────────────────────────────────────────
def _browse_file(self):
path, _ = QFileDialog.getOpenFileName(
self, "Select log file", "", "Log files (*.log);;All files (*)")
if path:
self._file_edit.setText(path)
def _on_file_path_changed(self, path: str):
self._id_edit.setText(source_id_from_path(path) or '')
# ── 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(
{'tcp': 0, 'serial': 1, 'file': 2}.get(config.conn_type, 0))
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))
self._file_edit.setText(config.file_path)
self._delay_spin.setValue(config.delay)
def get_config(self) -> ConnectionConfig:
t = ['tcp', 'serial', 'file'][self._type_combo.currentIndex()]
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()),
file_path = self._file_edit.text().strip(),
delay = self._delay_spin.value(),
)
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,
self._file_edit, self._delay_spin]:
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.remove_receiver(self._rid)
self._rid = None
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
if config.conn_type == 'file' and not config.file_path:
QMessageBox.warning(self, "GNSS Monitor",
"Bitte eine Log-Datei auswählen.")
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()