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>
120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from ameba_control_panel.config import app_data_dir
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DEFAULTS = {
|
|
# Font
|
|
"font_family": "JetBrains Mono",
|
|
"font_size": 10,
|
|
"ui_font_size": 10,
|
|
# Serial & Log
|
|
"default_baud": 1_500_000,
|
|
"partial_line_hold_ms": 300,
|
|
"port_scan_interval_sec": 5,
|
|
"log_tail_lines": 100_000,
|
|
"log_archive_max": 500_000,
|
|
"log_flush_interval_ms": 30,
|
|
"log_flush_batch_limit": 200,
|
|
# Flash
|
|
"default_rdev_path": "",
|
|
"default_floader_path": "",
|
|
"default_boot_start": "0x08000000",
|
|
"default_boot_end": "0x08040000",
|
|
"default_app_start": "0x08040000",
|
|
"default_app_end": "0x08440000",
|
|
"default_nn_start": "0x088A3000",
|
|
"default_nn_end": "0x08EB2FFF",
|
|
# Command
|
|
"cmd_delay_ms": 50,
|
|
"char_delay_ms": 0,
|
|
"history_max_entries": 500,
|
|
}
|
|
|
|
|
|
class Settings:
|
|
def __init__(self) -> None:
|
|
self._path = app_data_dir() / "settings.json"
|
|
self._data: dict = {}
|
|
self._load()
|
|
|
|
def _load(self) -> None:
|
|
try:
|
|
self._data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
except FileNotFoundError:
|
|
self._data = {}
|
|
except Exception:
|
|
logger.warning("Corrupt settings file, using defaults: %s", self._path)
|
|
self._data = {}
|
|
|
|
def save(self) -> None:
|
|
try:
|
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = self._path.with_suffix(".json.tmp")
|
|
tmp.write_text(json.dumps(self._data, indent=2), encoding="utf-8")
|
|
tmp.replace(self._path)
|
|
except Exception:
|
|
logger.exception("Failed to write settings: %s", self._path)
|
|
|
|
def get(self, key: str):
|
|
return self._data.get(key, _DEFAULTS.get(key))
|
|
|
|
def set(self, key: str, value) -> None:
|
|
self._data[key] = value
|
|
|
|
# Convenience properties for frequently accessed settings
|
|
@property
|
|
def font_family(self) -> str: return self.get("font_family")
|
|
@font_family.setter
|
|
def font_family(self, v: str) -> None: self.set("font_family", v)
|
|
|
|
@property
|
|
def font_size(self) -> int: return self.get("font_size")
|
|
@font_size.setter
|
|
def font_size(self, v: int) -> None: self.set("font_size", v)
|
|
|
|
@property
|
|
def ui_font_size(self) -> int: return self.get("ui_font_size")
|
|
@ui_font_size.setter
|
|
def ui_font_size(self, v: int) -> None: self.set("ui_font_size", v)
|
|
|
|
@property
|
|
def default_baud(self) -> int: return self.get("default_baud")
|
|
@default_baud.setter
|
|
def default_baud(self, v: int) -> None: self.set("default_baud", v)
|
|
|
|
@property
|
|
def partial_line_hold_ms(self) -> int: return self.get("partial_line_hold_ms")
|
|
@partial_line_hold_ms.setter
|
|
def partial_line_hold_ms(self, v: int) -> None: self.set("partial_line_hold_ms", v)
|
|
|
|
@property
|
|
def port_scan_interval_sec(self) -> int: return self.get("port_scan_interval_sec")
|
|
@port_scan_interval_sec.setter
|
|
def port_scan_interval_sec(self, v: int) -> None: self.set("port_scan_interval_sec", v)
|
|
|
|
@property
|
|
def log_tail_lines(self) -> int: return self.get("log_tail_lines")
|
|
@log_tail_lines.setter
|
|
def log_tail_lines(self, v: int) -> None: self.set("log_tail_lines", v)
|
|
|
|
@property
|
|
def cmd_delay_ms(self) -> int: return self.get("cmd_delay_ms")
|
|
@cmd_delay_ms.setter
|
|
def cmd_delay_ms(self, v: int) -> None: self.set("cmd_delay_ms", v)
|
|
|
|
@property
|
|
def char_delay_ms(self) -> int: return self.get("char_delay_ms")
|
|
@char_delay_ms.setter
|
|
def char_delay_ms(self, v: int) -> None: self.set("char_delay_ms", v)
|
|
|
|
@property
|
|
def history_max_entries(self) -> int: return self.get("history_max_entries")
|
|
@history_max_entries.setter
|
|
def history_max_entries(self, v: int) -> None: self.set("history_max_entries", v)
|