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:
parent
c92fbe7548
commit
547e8e8665
@ -232,7 +232,19 @@ def _setup_logging() -> None:
|
||||
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:
|
||||
_setup_linux_platform()
|
||||
_setup_logging()
|
||||
QApplication.setStyle("Fusion")
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@ -76,8 +76,12 @@ def parse_baud(text: str) -> int:
|
||||
|
||||
|
||||
def app_data_dir() -> Path:
|
||||
if os.name == "nt":
|
||||
base = os.environ.get("LOCALAPPDATA")
|
||||
if base:
|
||||
return Path(base) / "AmebaControlPanel"
|
||||
# Fallback for non-Windows dev environments.
|
||||
# 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"
|
||||
|
||||
@ -62,7 +62,7 @@ class LogManager(QObject):
|
||||
if not (text.startswith("[") and "]" in text):
|
||||
break
|
||||
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()
|
||||
else:
|
||||
break
|
||||
|
||||
@ -115,7 +115,7 @@ class FlashRunner(QThread):
|
||||
# Strip optional "[COMxx]" or "[main]" prefixes that follow
|
||||
while trimmed.startswith("[") and "]" in trimmed:
|
||||
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()
|
||||
continue
|
||||
break
|
||||
|
||||
@ -118,7 +118,7 @@ def build_stylesheet(p: Palette) -> str:
|
||||
QWidget {{
|
||||
background-color: {p.bg};
|
||||
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;
|
||||
}}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@ -10,7 +11,18 @@ def _bootstrap_path() -> None:
|
||||
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()
|
||||
_setup_linux_platform()
|
||||
|
||||
from ameba_control_panel.app import main # noqa: E402
|
||||
|
||||
|
||||
250
script/package_bin.py
Normal file
250
script/package_bin.py
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user