|
|
# 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()}
|