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>
156 lines
4.5 KiB
Python
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)
|