From ff686b64f7b65ff27b3a029b250eb06482aae64f Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Mon, 25 May 2026 12:47:28 +0200 Subject: [PATCH] gui: add File source type to connection tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ConnectionConfig: new file_path and delay fields - ConnectionRow: File page with path input, Browse button, delay spinner (0–10 s), and auto-fill of receiver-ID from filename - ReceiverManager.connect_receiver: FileBackend wired for 'file' type; UBX poll timer skipped for file sources - FileBackend.register_xcvr: fallback to first unoccupied entry when name does not match filename pattern Co-Authored-By: Claude Sonnet 4.6 --- backend/file_backend.py | 7 ++++- gui/connection_tab.py | 60 ++++++++++++++++++++++++++++++++++++----- gui/data_model.py | 33 ++++++++++++++--------- 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/backend/file_backend.py b/backend/file_backend.py index a751c0e..7fa2076 100644 --- a/backend/file_backend.py +++ b/backend/file_backend.py @@ -35,7 +35,12 @@ class FileBackend(ABackend): e['xcvr'] = xcvr xcvr.on_register(self) return - raise Exception(f"No log file found for source \"{xcvr.name}\"") + for e in self._entries: + if e['xcvr'] is None: + e['xcvr'] = xcvr + xcvr.on_register(self) + return + raise Exception(f"No available log file entry for \"{xcvr.name}\"") def connect(self): for e in self._entries: diff --git a/gui/connection_tab.py b/gui/connection_tab.py index 779f55c..6ff2d14 100644 --- a/gui/connection_tab.py +++ b/gui/connection_tab.py @@ -8,12 +8,13 @@ for _p in [_root, _gui]: from PyQt5.QtWidgets import ( QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, - QLineEdit, QComboBox, QSpinBox, QStackedWidget, QScrollArea, - QFrame, QMessageBox, QSizePolicy, + 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'] @@ -53,7 +54,7 @@ class ConnectionRow(QWidget): # ── Connection type ────────────────────────────────────────────────── self._type_combo = QComboBox() - self._type_combo.addItems(['TCP', 'Serial']) + self._type_combo.addItems(['TCP', 'Serial', 'File']) self._type_combo.currentIndexChanged.connect(self._on_type_changed) layout.addWidget(self._type_combo) @@ -95,6 +96,32 @@ class ConnectionRow(QWidget): 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() @@ -115,6 +142,17 @@ class ConnectionRow(QWidget): 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): @@ -129,7 +167,8 @@ class ConnectionRow(QWidget): 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._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: @@ -137,9 +176,11 @@ class ConnectionRow(QWidget): 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' if self._type_combo.currentIndex() == 0 else 'serial' + t = ['tcp', 'serial', 'file'][self._type_combo.currentIndex()] return ConnectionConfig( receiver_id = self._id_edit.text().strip(), conn_type = t, @@ -147,6 +188,8 @@ class ConnectionRow(QWidget): 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: @@ -157,7 +200,8 @@ class ConnectionRow(QWidget): 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._port_spin, self._ser_combo, self._baud_combo, + self._file_edit, self._delay_spin]: w.setEnabled(not connected) # ── Slots ──────────────────────────────────────────────────────────────── @@ -180,6 +224,10 @@ class ConnectionRow(QWidget): 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 diff --git a/gui/data_model.py b/gui/data_model.py index 8231f98..33d2c5a 100644 --- a/gui/data_model.py +++ b/gui/data_model.py @@ -52,16 +52,20 @@ class SatelliteData: @dataclass class ConnectionConfig: receiver_id: str - conn_type: str = 'tcp' - host: str = '' - port: int = 8721 - serial_port: str = '' - baud_rate: int = 115200 + conn_type: str = 'tcp' + host: str = '' + port: int = 8721 + serial_port: str = '' + baud_rate: int = 115200 + file_path: str = '' + delay: float = 1.0 def display_str(self) -> str: if self.conn_type == 'tcp': return f"{self.receiver_id} [{self.host}:{self.port}]" - return f"{self.receiver_id} [{self.serial_port} @ {self.baud_rate}]" + if self.conn_type == 'serial': + return f"{self.receiver_id} [{self.serial_port} @ {self.baud_rate}]" + return f"{self.receiver_id} [file: {os.path.basename(self.file_path)}]" def to_dict(self): return dataclasses.asdict(self) @@ -148,6 +152,7 @@ class ReceiverManager(QObject): def connect_receiver(self, rid: str): from listeners import NavSatQtListener, GsaQtListener from backend.serial_backend import SerialBackend + from backend.file_backend import FileBackend config = self._configs.get(rid) state = self._states.get(rid) @@ -160,8 +165,10 @@ class ReceiverManager(QObject): backend = NetworkBackend( [{'name': rid, 'host': config.host, 'port': config.port}] ) - else: + elif config.conn_type == 'serial': backend = SerialBackend(config.serial_port, config.baud_rate) + else: + backend = FileBackend([config.file_path], delay=config.delay) backend.register_xcvr(xcvr) @@ -184,12 +191,12 @@ class ReceiverManager(QObject): state.transceiver = xcvr state.connected = True - # Poll UBX messages every second (receiver only sends on request by default) - timer = QTimer(self) - timer.setInterval(1000) - timer.timeout.connect(lambda r=rid: self._poll_ubx(r)) - timer.start() - state.poll_timer = timer + if config.conn_type != 'file': + timer = QTimer(self) + timer.setInterval(1000) + timer.timeout.connect(lambda r=rid: self._poll_ubx(r)) + timer.start() + state.poll_timer = timer self._push_lru(config) self.connection_changed.emit(rid, True)