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

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)