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)
|
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)
|
||||||
|
|||||||
@ -76,8 +76,12 @@ def parse_baud(text: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def app_data_dir() -> Path:
|
def app_data_dir() -> Path:
|
||||||
|
if os.name == "nt":
|
||||||
base = os.environ.get("LOCALAPPDATA")
|
base = os.environ.get("LOCALAPPDATA")
|
||||||
if base:
|
if base:
|
||||||
return Path(base) / "AmebaControlPanel"
|
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"
|
return Path.home() / ".local" / "share" / "AmebaControlPanel"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|||||||
@ -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
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