Major refactor of Ameba Control Panel v3.1.0: - Three-column layout: icon sidebar, config+history, log view - Dracula PRO theme with light/dark toggle - DTR/RTS GPIO control (replaces ASCII commands) - Multi-CDC firmware support for AmebaSmart control device - Dynamic DUT tabs with +/- management - NN Model flash image support - Settings dialog (Font, Serial, Flash, Command tabs) - Background port scanning, debounced session store - Adaptive log flush rate, format cache optimization - Smooth sidebar animation, deferred startup - pytest test framework with session/log/settings tests - Thread safety fixes: _alive guards, parented timers, safe baud parsing - Find highlight: needle-only highlighting with focused match color - Partial line buffering for table output - PyInstaller packaging with version stamp and module exclusions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
158 lines
6.9 KiB
Python
158 lines
6.9 KiB
Python
from __future__ import annotations
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtGui import QIntValidator
|
|
from PySide6.QtWidgets import (
|
|
QComboBox,
|
|
QDialog,
|
|
QFormLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QPushButton,
|
|
QTabWidget,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from ameba_control_panel.services.settings_service import Settings
|
|
|
|
|
|
def _create_int_input(value: int, lo: int, hi: int, suffix: str = "") -> QLineEdit:
|
|
"""Plain text input with integer validation."""
|
|
display = f"{value}{suffix}" if suffix else str(value)
|
|
edit = QLineEdit(str(value))
|
|
edit.setValidator(QIntValidator(lo, hi))
|
|
edit.setPlaceholderText(f"{lo} - {hi}")
|
|
edit.setFixedWidth(140)
|
|
return edit
|
|
|
|
|
|
def _int_value(edit: QLineEdit) -> int:
|
|
"""Get integer from a _num input, or 0 if invalid."""
|
|
try:
|
|
return int(edit.text())
|
|
except (ValueError, TypeError):
|
|
return 0
|
|
|
|
|
|
class SettingsDialog(QDialog):
|
|
def __init__(self, settings: Settings, parent=None) -> None:
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Settings")
|
|
self.setMinimumWidth(480)
|
|
self._settings = settings
|
|
|
|
layout = QVBoxLayout(self)
|
|
tabs = QTabWidget()
|
|
layout.addWidget(tabs)
|
|
|
|
# ── Font tab ──────────────────────────────────────
|
|
font_tab = QWidget()
|
|
font_form = QFormLayout(font_tab)
|
|
|
|
from PySide6.QtGui import QFontDatabase
|
|
self._font_family = QComboBox()
|
|
self._font_family.setEditable(False)
|
|
self._font_family.setMaxVisibleItems(10)
|
|
for fam in sorted(QFontDatabase.families()):
|
|
self._font_family.addItem(fam)
|
|
self._font_family.setCurrentText(settings.font_family)
|
|
self._font_family.currentTextChanged.connect(lambda v: self._auto_save("font_family", v))
|
|
font_form.addRow("Log font family", self._font_family)
|
|
|
|
self._font_size = _create_int_input(settings.font_size, 6, 24)
|
|
self._font_size.editingFinished.connect(lambda: self._auto_save("font_size", _int_value(self._font_size)))
|
|
font_form.addRow("Log font size (pt)", self._font_size)
|
|
|
|
tabs.addTab(font_tab, "Font")
|
|
|
|
# ── Serial & Log tab ─────────────────────────────
|
|
serial_tab = QWidget()
|
|
serial_form = QFormLayout(serial_tab)
|
|
|
|
self._default_baud = _create_int_input(settings.default_baud, 9600, 6_000_000)
|
|
self._default_baud.editingFinished.connect(lambda: self._auto_save("default_baud", _int_value(self._default_baud)))
|
|
serial_form.addRow("Default baud (bps)", self._default_baud)
|
|
|
|
self._partial_hold = _create_int_input(settings.partial_line_hold_ms, 50, 2000)
|
|
self._partial_hold.editingFinished.connect(lambda: self._auto_save("partial_line_hold_ms", _int_value(self._partial_hold)))
|
|
serial_form.addRow("Partial line hold (ms)", self._partial_hold)
|
|
|
|
self._scan_interval = _create_int_input(settings.port_scan_interval_sec, 1, 60)
|
|
self._scan_interval.editingFinished.connect(lambda: self._auto_save("port_scan_interval_sec", _int_value(self._scan_interval)))
|
|
serial_form.addRow("Port scan interval (sec)", self._scan_interval)
|
|
|
|
self._log_tail = _create_int_input(settings.log_tail_lines, 1_000, 1_000_000)
|
|
self._log_tail.editingFinished.connect(lambda: self._auto_save("log_tail_lines", _int_value(self._log_tail)))
|
|
serial_form.addRow("Log buffer (lines)", self._log_tail)
|
|
|
|
self._log_archive = _create_int_input(settings.get("log_archive_max"), 10_000, 2_000_000)
|
|
self._log_archive.editingFinished.connect(lambda: self._auto_save("log_archive_max", _int_value(self._log_archive)))
|
|
serial_form.addRow("Log archive max (lines)", self._log_archive)
|
|
|
|
self._flush_interval = _create_int_input(settings.get("log_flush_interval_ms"), 10, 500)
|
|
self._flush_interval.editingFinished.connect(lambda: self._auto_save("log_flush_interval_ms", _int_value(self._flush_interval)))
|
|
serial_form.addRow("Log flush interval (ms)", self._flush_interval)
|
|
|
|
self._flush_batch = _create_int_input(settings.get("log_flush_batch_limit"), 50, 1000)
|
|
self._flush_batch.editingFinished.connect(lambda: self._auto_save("log_flush_batch_limit", _int_value(self._flush_batch)))
|
|
serial_form.addRow("Log flush batch (lines)", self._flush_batch)
|
|
|
|
tabs.addTab(serial_tab, "Serial")
|
|
|
|
# ── Flash tab ─────────────────────────────────────
|
|
flash_tab = QWidget()
|
|
flash_form = QFormLayout(flash_tab)
|
|
|
|
for label, key in [("Boot start addr", "default_boot_start"), ("Boot end addr", "default_boot_end"),
|
|
("App start addr", "default_app_start"), ("App end addr", "default_app_end"),
|
|
("NN start addr", "default_nn_start"), ("NN end addr", "default_nn_end")]:
|
|
edit = QLineEdit(settings.get(key) or "")
|
|
edit.setFixedWidth(140)
|
|
edit.editingFinished.connect(lambda e=edit, k=key: self._auto_save(k, e.text()))
|
|
flash_form.addRow(label, edit)
|
|
|
|
tabs.addTab(flash_tab, "Flash")
|
|
|
|
# ── Command tab ───────────────────────────────────
|
|
cmd_tab = QWidget()
|
|
cmd_form = QFormLayout(cmd_tab)
|
|
|
|
self._cmd_delay = _create_int_input(settings.cmd_delay_ms, 0, 60_000)
|
|
self._cmd_delay.editingFinished.connect(lambda: self._auto_save("cmd_delay_ms", _int_value(self._cmd_delay)))
|
|
cmd_form.addRow("Per-command delay (ms)", self._cmd_delay)
|
|
|
|
self._char_delay = _create_int_input(settings.char_delay_ms, 0, 5_000)
|
|
self._char_delay.editingFinished.connect(lambda: self._auto_save("char_delay_ms", _int_value(self._char_delay)))
|
|
cmd_form.addRow("Per-char delay (ms)", self._char_delay)
|
|
|
|
self._history_max = _create_int_input(settings.history_max_entries, 10, 10_000)
|
|
self._history_max.editingFinished.connect(lambda: self._auto_save("history_max_entries", _int_value(self._history_max)))
|
|
cmd_form.addRow("History max entries", self._history_max)
|
|
|
|
tabs.addTab(cmd_tab, "Command")
|
|
|
|
btn_row = QHBoxLayout()
|
|
apply_btn = QPushButton("Apply")
|
|
apply_btn.clicked.connect(self._on_apply)
|
|
close_btn = QPushButton("Close")
|
|
close_btn.clicked.connect(self.accept)
|
|
btn_row.addStretch()
|
|
btn_row.addWidget(apply_btn)
|
|
btn_row.addWidget(close_btn)
|
|
layout.addLayout(btn_row)
|
|
|
|
self._apply_callback = None
|
|
|
|
def set_apply_callback(self, callback) -> None:
|
|
self._apply_callback = callback
|
|
|
|
def _on_apply(self) -> None:
|
|
if self._apply_callback:
|
|
self._apply_callback()
|
|
|
|
def _auto_save(self, key: str, value) -> None:
|
|
self._settings.set(key, value)
|
|
self._settings.save()
|