# components/uart/uart_logger_ui.py # Minimal UART Logger UI (no commands/table) — connect and stream logs. import config.config as config from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QSplitter, QLabel, ) from PyQt6.QtCore import Qt, QTimer from PyQt6.QtCore import QObject, pyqtSignal # project-local deps from components.console.console_ui import console_widget from components.console.console_registry import log_main_console import components.items.elements as elements from components.uart.uart_logic import UART_logic from components.scheduler.coordinator import ( uart_capture_active, uart_capture_remaining_ms, ) class _SafeConsoleProxy(QObject): log_signal = pyqtSignal(str, str) # level, message def __init__(self, console): super().__init__() self.log_signal.connect(console.log) def __call__(self, level, msg): self.log_signal.emit(level, msg) class UartLoggerWidget(QWidget): """ Super-lean UART logger: - Port + serial params - Connect / Disconnect - Auto-start continuous logger on connect - One console - Capture indicator (global coordinator) """ def __init__(self, parent=None): super().__init__(parent) self.uart_logic = UART_logic() self.comboboxes = {} self.connection_status = False # console + have uart_logic log into it self.console = console_widget() # IMPORTANT: keep a strong reference so it isn’t GC’d self._uart_console_proxy = _SafeConsoleProxy(self.console) self.uart_logic.set_logger(self._uart_console_proxy) self.init_ui() self._wire_signals() self._setup_capture_guard() self.disconnected_enable_status() # ---------------- UI ---------------- def init_ui(self): # === Top Control Row === top_controls = QWidget() top_controls_layout = QHBoxLayout(top_controls) top_controls_layout.setContentsMargins(0, 0, 0, 0) top_controls_layout.setSpacing(12) top_controls_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Port + refresh self.comboboxes["port"] = QComboBox() self.comboboxes["port"].addItems(self.uart_logic.get_channels()) top_controls_layout.addWidget( elements.label_and_widget("Port", self.comboboxes["port"]) ) self.button_refresh = elements.create_icon_button( config.REFRESH_BUTTON_ICON_LINK, icon_size=30, border_size=4 ) top_controls_layout.addWidget(self.button_refresh) # Serial params self.comboboxes["baudrate"] = QComboBox() self.comboboxes["baudrate"].addItems(self.uart_logic.get_baud_rates()) top_controls_layout.addWidget( elements.label_and_widget("Baudrate", self.comboboxes["baudrate"]) ) self.comboboxes["data_bits"] = QComboBox() self.comboboxes["data_bits"].addItems(self.uart_logic.get_data_bits()) top_controls_layout.addWidget( elements.label_and_widget("Data Bits", self.comboboxes["data_bits"]) ) self.comboboxes["stop_bits"] = QComboBox() self.comboboxes["stop_bits"].addItems(self.uart_logic.get_stop_bits()) top_controls_layout.addWidget( elements.label_and_widget("Stop Bits", self.comboboxes["stop_bits"]) ) self.comboboxes["parity"] = QComboBox() self.comboboxes["parity"].addItems(self.uart_logic.get_parity()) top_controls_layout.addWidget( elements.label_and_widget("Parity", self.comboboxes["parity"]) ) # Connect / Disconnect self.button_connect = QPushButton("Connect") top_controls_layout.addWidget( elements.label_and_widget("", self.button_connect) ) self.button_disconnect = QPushButton("Disconnect") top_controls_layout.addWidget( elements.label_and_widget("", self.button_disconnect) ) # Capture status label self.label_capture = QLabel("Capture: idle") top_controls_layout.addWidget( elements.label_and_widget("Status", self.label_capture) ) # === Console only === console_stack_widget = QWidget() console_stack_layout = QVBoxLayout(console_stack_widget) console_stack_layout.setContentsMargins(0, 0, 0, 0) console_stack_layout.setSpacing(4) console_stack_layout.addWidget(self.console) # === Splitter (kept for consistency, only console pane) === splitter = QSplitter(Qt.Orientation.Horizontal) splitter.addWidget(console_stack_widget) splitter.setStretchFactor(0, 1) # === Main Layout === main_layout = QVBoxLayout() main_layout.setContentsMargins(4, 4, 4, 4) main_layout.setSpacing(6) main_layout.addWidget(top_controls) main_layout.addWidget(splitter, stretch=1) self.setLayout(main_layout) def _wire_signals(self): self.button_refresh.clicked.connect(self.refresh) self.button_connect.clicked.connect(self.connect) self.button_disconnect.clicked.connect(self.disconnect) # ---------- Capture guard: label updates ---------- def _setup_capture_guard(self): self._guard_timer = QTimer(self) self._guard_timer.setInterval(50) # 20 Hz self._guard_timer.timeout.connect(self._update_capture_label) self._guard_timer.start() def _update_capture_label(self): if uart_capture_active(): rem = uart_capture_remaining_ms() self.label_capture.setText(f"Capture: listening… {rem} ms") else: self.label_capture.setText("Capture: idle") # ---- UI state toggles ---- def disconnected_enable_status(self): for combo in self.comboboxes.values(): if combo.property("inversedGray"): elements.set_enabled_state(False, combo, grayOut=True) else: elements.set_enabled_state(True, combo, grayOut=False) elements.set_enabled_state(False, self.button_disconnect, grayOut=True) elements.set_enabled_state(True, self.button_connect, grayOut=False) def connected_enable_status(self): for combo in self.comboboxes.values(): if combo.property("inversedGray"): elements.set_enabled_state(True, combo, grayOut=False) else: elements.set_enabled_state(False, combo, grayOut=True) elements.set_enabled_state(True, self.button_disconnect, grayOut=False) elements.set_enabled_state(False, self.button_connect, grayOut=True) # ---- Actions ---- def connect(self): log_main_console("info", "🔗 Connecting (logger)…") port = self.comboboxes["port"].currentText() baudrate = int(self.comboboxes["baudrate"].currentText()) data_bits = int(self.comboboxes["data_bits"].currentText()) stop_bits = float(self.comboboxes["stop_bits"].currentText()) parity_label = self.comboboxes["parity"].currentText() # "Even", "None", "Odd" parity_short = parity_label[0].upper() # E/N/O for your uart_logic ok = self.uart_logic.connect( port=port, baudrate=baudrate, data_bits=data_bits, stop_bits=stop_bits, parity=parity_short, ) if ok: # auto-start continuous logger (hex output) self.uart_logic.start_logger(hex_output=True) self.connected_enable_status() self.connection_status = True else: elements.flash_button( self.button_connect, flash_style="background-color: red;" ) def disconnect(self): log_main_console("info", "🔌 Disconnecting (logger)…") if self.uart_logic.disconnect(): self.disconnected_enable_status() self.connection_status = False self.refresh(silent=True) self.uart_logic.stop_logger() else: elements.flash_button( self.button_disconnect, flash_style="background-color: red;" ) def refresh(self, silent: bool = False): log_main_console("info", "🔄 Refreshing ports…") self.comboboxes["port"].clear() ports = self.uart_logic.get_channels() if ports: self.comboboxes["port"].addItems(ports) if not silent: elements.flash_button(self.button_refresh) log_main_console("success", "✅ Ports refreshed.") else: elements.flash_button( self.button_refresh, flash_style="background-color: red;" ) log_main_console("warning", "⚠️ No UART ports found.") # helpful for diagnostics def get_current_config(self): return {key: cb.currentText() for key, cb in self.comboboxes.items()}