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

156 lines
4.5 KiB
Python

"""Build Ameba Control Panel executable with PyInstaller."""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
def _add_data(src: Path, dest: str) -> str:
sep = ";" if os.name == "nt" else ":"
return f"{src}{sep}{dest}"
def _write_version_file(root: Path, version: str) -> Path:
parts = (version.split(".") + ["0", "0", "0"])[:4]
csv = ", ".join(parts)
dot = ".".join(parts)
out = root / "build" / "version_info.txt"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(f"""\
VSVersionInfo(
ffi=FixedFileInfo(filevers=({csv}), prodvers=({csv})),
kids=[
StringFileInfo([StringTable('040904B0', [
StringStruct('CompanyName', 'Realtek'),
StringStruct('FileDescription', 'Ameba Control Panel'),
StringStruct('FileVersion', '{dot}'),
StringStruct('ProductName', 'Ameba Control Panel'),
StringStruct('ProductVersion', '{dot}'),
])]),
VarFileInfo([VarStruct('Translation', [0x0409, 0x04B0])])
]
)
""", encoding="utf-8")
return out
_EXCLUDES = [
"tkinter",
"matplotlib",
"numpy",
"scipy",
"pandas",
"PIL",
"PySide6.QtWebEngine",
"PySide6.QtWebEngineCore",
"PySide6.QtWebEngineWidgets",
"PySide6.Qt3DCore",
"PySide6.Qt3DRender",
"PySide6.Qt3DInput",
"PySide6.Qt3DAnimation",
"PySide6.Qt3DExtras",
"PySide6.Qt3DLogic",
"PySide6.QtCharts",
"PySide6.QtDataVisualization",
"PySide6.QtMultimedia",
"PySide6.QtMultimediaWidgets",
"PySide6.QtQuick",
"PySide6.QtQuick3D",
"PySide6.QtQuickWidgets",
"PySide6.QtQml",
"PySide6.QtRemoteObjects",
"PySide6.QtSensors",
"PySide6.QtSerialBus",
"PySide6.QtBluetooth",
"PySide6.QtNfc",
"PySide6.QtPositioning",
"PySide6.QtLocation",
"PySide6.QtTest",
"PySide6.QtPdf",
"PySide6.QtPdfWidgets",
"PySide6.QtSvgWidgets",
"PySide6.QtNetworkAuth",
"PySide6.QtDesigner",
"PySide6.QtHelp",
"PySide6.QtOpenGL",
"PySide6.QtOpenGLWidgets",
"PySide6.QtSpatialAudio",
"PySide6.QtStateMachine",
"PySide6.QtTextToSpeech",
"PySide6.QtHttpServer",
"PySide6.QtGraphs",
]
def build(*, onefile: bool, icon: str | None = None, splash: str | None = None) -> None:
root = Path(__file__).resolve().parent.parent
entry = root / "script" / "auto_run.py"
flash_dir = root / "Flash"
if not entry.exists():
sys.exit(f"Entry script missing: {entry}")
if not flash_dir.exists():
sys.exit(f"Flash folder missing: {flash_dir}")
os.chdir(root)
try:
import PyInstaller.__main__ as pyinstaller
except ImportError:
sys.exit("PyInstaller is not installed. Run: pip install PyInstaller")
sys.path.insert(0, str(root))
from ameba_control_panel.config import APP_VERSION
version_file = _write_version_file(root, APP_VERSION)
args = [
"--noconfirm",
"--clean",
"--windowed",
"--onefile" if onefile else "--onedir",
f"--name=AmebaControlPanel",
f"--distpath={root / 'dist'}",
f"--workpath={root / 'build'}",
f"--specpath={root}",
f"--version-file={version_file}",
"--paths", str(root),
"--collect-all", "PySide6",
"--hidden-import=serial",
"--hidden-import=serial.tools.list_ports",
"--hidden-import=pyDes",
"--hidden-import=colorama",
"--add-data", _add_data(flash_dir, "Flash"),
]
for mod in _EXCLUDES:
args.extend(["--exclude-module", mod])
if icon:
icon_path = Path(icon)
if icon_path.exists():
args.extend(["--icon", str(icon_path)])
else:
print(f"Warning: icon not found: {icon_path}", file=sys.stderr)
if splash:
splash_path = Path(splash)
if splash_path.exists():
args.extend(["--splash", str(splash_path)])
else:
print(f"Warning: splash image not found: {splash_path}", file=sys.stderr)
args.append(str(entry))
pyinstaller.run(args)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Build Ameba Control Panel EXE")
parser.add_argument("--onefile", action="store_true", help="Create single-file exe (slower startup)")
parser.add_argument("--icon", help="Path to .ico file")
parser.add_argument("--splash", help="Path to splash screen image (.png)")
opts = parser.parse_args()
# Default to --onedir for fast startup
build(onefile=opts.onefile, icon=opts.icon, splash=opts.splash)