Compare commits
2 Commits
c92fbe7548
...
5faf1e6d14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5faf1e6d14 | ||
|
|
547e8e8665 |
21
.gitignore
vendored
21
.gitignore
vendored
@ -1,3 +1,20 @@
|
|||||||
build
|
# Python
|
||||||
dist
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# Build / packaging
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
*.spec
|
*.spec
|
||||||
|
*.prof
|
||||||
|
|
||||||
|
# Virtual env
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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