wongyiekheng c92fbe7548 Second Commit
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>
2026-03-29 13:01:12 +08:00

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