完善夹爪界面、跨平台打包与运行日志

This commit is contained in:
cheng
2026-07-15 16:10:31 +08:00
parent 73ce84f586
commit 45630e6d77
13 changed files with 1001 additions and 201 deletions
-19
View File
@@ -1,19 +0,0 @@
@echo off
setlocal
cd /d "%~dp0.."
call conda activate py311
if errorlevel 1 (
echo Failed to activate conda environment: py311
pause
exit /b 1
)
python "%~dp0build_exe.py" %*
if errorlevel 1 (
echo.
echo Build failed.
pause
exit /b 1
)
echo.
echo Build finished.
pause
-19
View File
@@ -1,19 +0,0 @@
param(
[string]$CondaEnv = "py311"
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectRoot = Split-Path -Parent $ScriptDir
Set-Location $ProjectRoot
Write-Host "Project root: $ProjectRoot"
Write-Host "Conda env: $CondaEnv"
$ForwardArgs = $args -join " "
cmd /d /s /c "call conda activate $CondaEnv && python `"$ScriptDir\build_exe.py`" $ForwardArgs"
if ($LASTEXITCODE -ne 0) {
throw "build failed"
}
+111 -27
View File
@@ -1,5 +1,6 @@
import argparse
import importlib.util
import json
import shutil
import subprocess
import sys
@@ -13,6 +14,33 @@ LOGO_FILES = (
PROJECT_ROOT / "gripper" / "logo.png",
PROJECT_ROOT / "gripper" / "logo_topbar.png",
)
LITE_EXCLUDES = (
# RAFT and its CUDA runtime. The default application uses OpenCV DIS.
"torch",
"torchvision",
"torchaudio",
"torchgen",
# Optional Orisys GPU Poisson backends.
"cupy",
"cupyx",
"cupy_backends",
"pyopencl",
"pyvkfft",
# Optional Orisys mesh viewer and its notebook/web dependencies.
"open3d",
"dash",
"flask",
"plotly",
"IPython",
"jedi",
"parso",
"nbformat",
"notebook",
# Transitive data-science packages not used by this GUI.
"matplotlib",
"pandas",
"sklearn",
)
def module_exists(name):
@@ -25,10 +53,6 @@ def require_module(name, install_hint):
raise SystemExit(f"Missing module '{name}'. Install it first: {install_hint}")
def data_arg(source, target):
return f"{source};{target}"
def copy_runtime_dir(source, app_dir, name):
target = app_dir / name
if target.exists():
@@ -36,13 +60,69 @@ def copy_runtime_dir(source, app_dir, name):
shutil.copytree(
source,
target,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
ignore=shutil.ignore_patterns(
"__pycache__",
"*.pyc",
"gui_layout.json",
"gui_layout.json.tmp",
),
)
return target
def build(app_name, clean_dist):
def directory_size(path):
return sum(item.stat().st_size for item in path.rglob("*") if item.is_file())
def print_size_report(app_dir, budget_mb):
total_bytes = directory_size(app_dir)
total_mb = total_bytes / (1024 * 1024)
print(f"Package size: {total_mb:.1f} MiB ({total_bytes / 1_000_000:.1f} MB)")
if total_mb > budget_mb:
print(f"Warning: package exceeds the {budget_mb:.0f} MiB target.")
largest = sorted(
(item for item in app_dir.rglob("*") if item.is_file()),
key=lambda item: item.stat().st_size,
reverse=True,
)[:10]
print("Largest packaged files:")
for item in largest:
print(f" {item.stat().st_size / (1024 * 1024):8.1f} MiB {item.relative_to(app_dir)}")
def validate_lite_sensor_configs():
control_path = PROJECT_ROOT / "config" / "gripper.json"
try:
control_config = json.loads(control_path.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
raise SystemExit(f"Cannot validate lite build config '{control_path}': {exc}") from exc
config_names = {
control_config.get("sensor_config1", "./config/ddjx01.json"),
control_config.get("sensor_config2", "./config/ddjx01.json"),
}
for config_name in config_names:
config_path = Path(config_name)
if not config_path.is_absolute():
config_path = PROJECT_ROOT / config_path
try:
sensor_config = json.loads(config_path.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
raise SystemExit(f"Cannot validate lite sensor config '{config_path}': {exc}") from exc
algorithm = str(sensor_config.get("tracker", {}).get("algorithm", "dis")).lower()
if algorithm == "raft":
raise SystemExit(
f"Lite profile cannot package RAFT configured in '{config_path}'. "
"Set tracker.algorithm to 'dis' or build with --profile full."
)
def build(app_name, clean_dist, profile, size_budget_mb):
require_module("PyInstaller", "pip install pyinstaller")
require_module("numba", "pip install numba")
if profile == "lite":
validate_lite_sensor_configs()
dist_dir = PROJECT_ROOT / "dist"
work_dir = BUILD_DIR / "_pyinstaller"
@@ -73,10 +153,6 @@ def build(app_name, clean_dist):
str(PROJECT_ROOT),
"--paths",
str(PROJECT_ROOT / "sdk"),
"--add-data",
data_arg(PROJECT_ROOT / "config", "config"),
"--add-data",
data_arg(PROJECT_ROOT / "sdk", "sdk"),
"--hidden-import",
"changingtek_p_rtu_Servo",
"--hidden-import",
@@ -87,26 +163,19 @@ def build(app_name, clean_dist):
"cv2",
"--hidden-import",
"numpy",
"--collect-all",
"PyQt5",
"--collect-all",
"pyqtgraph",
"--hidden-import",
"pyqtgraph.opengl",
]
if VISUALIZATION_DIR.exists():
args.extend(
[
"--add-data",
data_arg(VISUALIZATION_DIR, "crt_ctag2f90d_gripper_visualization"),
]
)
else:
if profile == "lite":
for module_name in LITE_EXCLUDES:
args.extend(["--exclude-module", module_name])
if not VISUALIZATION_DIR.exists():
print(f"Warning: visualization assets were not found: {VISUALIZATION_DIR}")
for logo_file in LOGO_FILES:
if logo_file.exists():
args.extend(["--add-data", data_arg(logo_file, "gripper")])
else:
if not logo_file.exists():
print(f"Warning: logo asset was not found: {logo_file}")
if module_exists("OpenGL"):
@@ -125,7 +194,8 @@ def build(app_name, clean_dist):
subprocess.run(args, cwd=PROJECT_ROOT, check=True)
exe_path = dist_dir / app_name / f"{app_name}.exe"
executable_name = f"{app_name}.exe" if sys.platform == "win32" else app_name
exe_path = dist_dir / app_name / executable_name
if not exe_path.exists():
raise SystemExit(f"Build finished but exe was not found: {exe_path}")
@@ -154,14 +224,28 @@ def build(app_name, clean_dist):
print(f"Runtime 3D model copied to: {visualization_dir}")
for logo_path in copied_logos:
print(f"Runtime logo copied to: {logo_path}")
print(f"Build profile: {profile}")
print_size_report(app_dir, size_budget_mb)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--name", default="GripperDemo02", help="Output exe/app folder name.")
parser.add_argument("--clean-dist", action="store_true", help="Delete dist/<name> before building.")
parser.add_argument(
"--profile",
choices=("lite", "full"),
default="lite",
help="lite excludes RAFT/CUDA/CuPy/Open3D; full bundles installed optional backends.",
)
parser.add_argument(
"--size-budget-mb",
type=float,
default=500.0,
help="Print a warning when the onedir package exceeds this many MiB.",
)
args = parser.parse_args()
build(args.name, args.clean_dist)
build(args.name, args.clean_dist, args.profile, args.size_budget_mb)
if __name__ == "__main__":
+4
View File
@@ -7,6 +7,10 @@ from pathlib import Path
if getattr(sys, "frozen", False):
os.chdir(Path(sys.executable).resolve().parent)
from gripper.common.runtime_logging import configure_frozen_logging
configure_frozen_logging()
from gripper.visualizer import main