Third Commit

Add Ubuntu 22.04/24.04 compatibility and Linux packaging script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
wongyiekheng 2026-03-29 14:32:08 +08:00
parent c92fbe7548
commit 547e8e8665
7 changed files with 285 additions and 7 deletions

View File

@ -232,7 +232,19 @@ def _setup_logging() -> None:
logging.getLogger(__name__).info("App started v%s", config.APP_VERSION) logging.getLogger(__name__).info("App started v%s", config.APP_VERSION)
def _setup_linux_platform() -> None:
"""Ensure correct Qt platform plugin on Linux."""
import os
if sys.platform != "linux" or os.environ.get("QT_QPA_PLATFORM"):
return
if os.environ.get("WAYLAND_DISPLAY"):
os.environ["QT_QPA_PLATFORM"] = "wayland;xcb"
elif os.environ.get("DISPLAY"):
os.environ["QT_QPA_PLATFORM"] = "xcb"
def main() -> None: def main() -> None:
_setup_linux_platform()
_setup_logging() _setup_logging()
QApplication.setStyle("Fusion") QApplication.setStyle("Fusion")
app = QApplication(sys.argv) app = QApplication(sys.argv)

View File

@ -76,8 +76,12 @@ def parse_baud(text: str) -> int:
def app_data_dir() -> Path: def app_data_dir() -> Path:
base = os.environ.get("LOCALAPPDATA") if os.name == "nt":
if base: base = os.environ.get("LOCALAPPDATA")
return Path(base) / "AmebaControlPanel" if base:
# Fallback for non-Windows dev environments. return Path(base) / "AmebaControlPanel"
# Linux/macOS: use XDG_DATA_HOME or default
xdg = os.environ.get("XDG_DATA_HOME")
if xdg:
return Path(xdg) / "AmebaControlPanel"
return Path.home() / ".local" / "share" / "AmebaControlPanel" return Path.home() / ".local" / "share" / "AmebaControlPanel"

View File

@ -62,7 +62,7 @@ class LogManager(QObject):
if not (text.startswith("[") and "]" in text): if not (text.startswith("[") and "]" in text):
break break
prefix = text.split("]", 1)[0] prefix = text.split("]", 1)[0]
if prefix.startswith("[COM") or prefix.startswith("[main"): if prefix.startswith("[COM") or prefix.startswith("[/dev/") or prefix.startswith("[main"):
text = text.split("]", 1)[1].lstrip() text = text.split("]", 1)[1].lstrip()
else: else:
break break

View File

@ -115,7 +115,7 @@ class FlashRunner(QThread):
# Strip optional "[COMxx]" or "[main]" prefixes that follow # Strip optional "[COMxx]" or "[main]" prefixes that follow
while trimmed.startswith("[") and "]" in trimmed: while trimmed.startswith("[") and "]" in trimmed:
prefix = trimmed.split("]", 1)[0] prefix = trimmed.split("]", 1)[0]
if prefix.startswith("[COM") or prefix.startswith("[main"): if prefix.startswith("[COM") or prefix.startswith("[/dev/") or prefix.startswith("[main"):
trimmed = trimmed.split("]", 1)[1].lstrip() trimmed = trimmed.split("]", 1)[1].lstrip()
continue continue
break break

View File

@ -118,7 +118,7 @@ def build_stylesheet(p: Palette) -> str:
QWidget {{ QWidget {{
background-color: {p.bg}; background-color: {p.bg};
color: {p.text}; color: {p.text};
font-family: "Segoe UI", "SF Pro Text", sans-serif; font-family: "Segoe UI", "SF Pro Text", "Ubuntu", "Noto Sans", sans-serif;
font-size: 10pt; font-size: 10pt;
}} }}

View File

@ -1,3 +1,4 @@
import os
import sys import sys
from pathlib import Path from pathlib import Path
@ -10,7 +11,18 @@ def _bootstrap_path() -> None:
sys.path.insert(0, str(root)) sys.path.insert(0, str(root))
def _setup_linux_platform() -> None:
"""Set Qt platform plugin for Linux X11/Wayland compatibility."""
if sys.platform != "linux" or os.environ.get("QT_QPA_PLATFORM"):
return
if os.environ.get("WAYLAND_DISPLAY"):
os.environ["QT_QPA_PLATFORM"] = "wayland;xcb"
elif os.environ.get("DISPLAY"):
os.environ["QT_QPA_PLATFORM"] = "xcb"
_bootstrap_path() _bootstrap_path()
_setup_linux_platform()
from ameba_control_panel.app import main # noqa: E402 from ameba_control_panel.app import main # noqa: E402

250
script/package_bin.py Normal file
View File

@ -0,0 +1,250 @@
"""Build Ameba Control Panel Linux binary with PyInstaller (onedir)."""
from __future__ import annotations
import argparse
import os
import shutil
import sys
from pathlib import Path
def _add_data(src: Path, dest: str) -> str:
return f"{src}:{dest}"
_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",
]
_DESKTOP_ENTRY = """\
[Desktop Entry]
Name=Ameba Control Panel
Comment=Realtek Ameba device control panel for serial communication, flashing, and debugging
Exec={exec_path}
Icon={icon_path}
Terminal=false
Type=Application
Categories=Development;Electronics;
StartupWMClass=AmebaControlPanel
"""
_UDEV_RULES = """\
# Ameba Control Panel - USB serial device permissions
# Install: sudo cp 99-ameba-serial.rules /etc/udev/rules.d/
# Reload: sudo udevadm control --reload-rules && sudo udevadm trigger
# FTDI devices (FT232, FT2232, etc.)
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", MODE="0666"
# Silicon Labs CP210x
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", MODE="0666"
# WCH CH340/CH341
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", MODE="0666"
# Prolific PL2303
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", MODE="0666"
# Realtek devices
SUBSYSTEM=="tty", ATTRS{idVendor}=="0bda", MODE="0666"
# SEGGER J-Link
SUBSYSTEM=="tty", ATTRS{idVendor}=="1366", MODE="0666"
# STMicroelectronics STLink
SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", MODE="0666"
# Generic USB CDC ACM devices (covers most USB-to-serial adapters)
SUBSYSTEM=="tty", KERNEL=="ttyACM[0-9]*", MODE="0666"
SUBSYSTEM=="tty", KERNEL=="ttyUSB[0-9]*", MODE="0666"
"""
_LAUNCH_SCRIPT = """\
#!/bin/bash
# Ameba Control Panel launcher
# Handles X11/Wayland compatibility
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Detect display server and set Qt platform if needed
if [ -n "$WAYLAND_DISPLAY" ]; then
# Wayland session — let Qt use wayland or xcb fallback
export QT_QPA_PLATFORM="${{QT_QPA_PLATFORM:-wayland;xcb}}"
elif [ -n "$DISPLAY" ]; then
# X11 session
export QT_QPA_PLATFORM="${{QT_QPA_PLATFORM:-xcb}}"
fi
# Ensure bundled libs are found
export LD_LIBRARY_PATH="$SCRIPT_DIR:$LD_LIBRARY_PATH"
exec "$SCRIPT_DIR/AmebaControlPanel" "$@"
"""
def _generate_support_files(dist_dir: Path) -> None:
"""Generate udev rules, desktop entry, and launcher script."""
app_dir = dist_dir / "AmebaControlPanel"
app_dir.mkdir(parents=True, exist_ok=True)
# Udev rules
rules_path = app_dir / "99-ameba-serial.rules"
rules_path.write_text(_UDEV_RULES, encoding="utf-8")
print(f" udev rules: {rules_path}")
# Launcher script
launcher = app_dir / "ameba-control-panel.sh"
launcher.write_text(_LAUNCH_SCRIPT, encoding="utf-8")
launcher.chmod(0o755)
print(f" launcher: {launcher}")
# Desktop entry
desktop = app_dir / "ameba-control-panel.desktop"
desktop.write_text(
_DESKTOP_ENTRY.format(
exec_path=str(app_dir / "ameba-control-panel.sh"),
icon_path=str(app_dir / "icon.png") if (app_dir / "icon.png").exists() else "",
),
encoding="utf-8",
)
print(f" desktop: {desktop}")
# Install helper
install_script = app_dir / "install.sh"
install_script.write_text(
"#!/bin/bash\n"
"set -e\n"
'SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"\n'
"\n"
"echo 'Installing udev rules for serial port access...'\n"
'sudo cp "$SCRIPT_DIR/99-ameba-serial.rules" /etc/udev/rules.d/\n'
"sudo udevadm control --reload-rules\n"
"sudo udevadm trigger\n"
"echo 'Udev rules installed.'\n"
"\n"
"echo 'Installing desktop entry...'\n"
'DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"\n'
'mkdir -p "$DESKTOP_DIR"\n'
'sed "s|Exec=.*|Exec=$SCRIPT_DIR/ameba-control-panel.sh|" '
'"$SCRIPT_DIR/ameba-control-panel.desktop" > "$DESKTOP_DIR/ameba-control-panel.desktop"\n'
"echo 'Desktop entry installed.'\n"
"\n"
"echo 'Done. You may need to log out and back in for serial port permissions to take effect.'\n",
encoding="utf-8",
)
install_script.chmod(0o755)
print(f" installer: {install_script}")
def build(*, icon: 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")
args = [
"--noconfirm",
"--clean",
"--onedir",
"--name=AmebaControlPanel",
f"--distpath={root / 'dist'}",
f"--workpath={root / 'build'}",
f"--specpath={root}",
"--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)
args.append(str(entry))
print("Building with PyInstaller...")
pyinstaller.run(args)
# Copy icon if provided
dist_app = root / "dist" / "AmebaControlPanel"
if icon and Path(icon).exists() and dist_app.exists():
shutil.copy2(icon, dist_app / "icon.png")
print("\nGenerating support files...")
_generate_support_files(root / "dist")
print(f"\nBuild complete: {dist_app}")
print("\nTo install on target system:")
print(f" 1. Copy {dist_app} to the target machine")
print(f" 2. Run: {dist_app}/install.sh")
print(f" 3. Launch: {dist_app}/ameba-control-panel.sh")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Build Ameba Control Panel for Linux")
parser.add_argument("--icon", help="Path to .png icon file")
opts = parser.parse_args()
build(icon=opts.icon)