完善夹爪界面、跨平台打包与运行日志
This commit is contained in:
+56
-14
@@ -1,46 +1,88 @@
|
||||
# Python caches
|
||||
# Python bytecode and caches
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.pyre/
|
||||
.pytype/
|
||||
.hypothesis/
|
||||
.nox/
|
||||
.tox/
|
||||
|
||||
# Virtual environments
|
||||
# Virtual environments and local Python tooling
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.conda/
|
||||
.python-version
|
||||
pip-wheel-metadata/
|
||||
|
||||
|
||||
# Test and coverage output
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
debug/
|
||||
|
||||
# PyInstaller output
|
||||
# Python packaging and PyInstaller output
|
||||
dist/
|
||||
wheelhouse/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
build/_pyinstaller/
|
||||
build/*.spec
|
||||
*.manifest
|
||||
|
||||
# Runtime logs and local generated files
|
||||
# Test, coverage, and profiling output
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
prof/
|
||||
*.prof
|
||||
debug/
|
||||
|
||||
# Runtime logs and machine-local state
|
||||
*.log
|
||||
log/
|
||||
logs/
|
||||
debug.txt
|
||||
config/gui_layout.json
|
||||
config/gui_layout.json.tmp
|
||||
test.json
|
||||
|
||||
# IDE/editor files
|
||||
# Local launcher and shell wrappers
|
||||
*.bat
|
||||
*.cmd
|
||||
*.ps1
|
||||
*.sh
|
||||
|
||||
# Temporary and backup files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
||||
*.orig
|
||||
*.rej
|
||||
*~
|
||||
|
||||
# IDE and editor metadata
|
||||
.idea/
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
# Jupyter
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# Operating-system metadata
|
||||
.DS_Store
|
||||
._*
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Local agent/tool metadata
|
||||
.agents/
|
||||
.codex/
|
||||
|
||||
# Hardware vendor tool runtime logs
|
||||
# Hardware vendor runtime output
|
||||
知行夹爪/**/log/
|
||||
|
||||
@@ -68,9 +68,27 @@ pip install pyinstaller PyOpenGL PyOpenGL_accelerate
|
||||
然后从项目根目录运行:
|
||||
|
||||
```powershell
|
||||
build\build_exe.bat
|
||||
python build\build_exe.py
|
||||
```
|
||||
|
||||
默认使用轻量打包配置,使用 OpenCV DIS 光流和 CPU Poisson 后端,不打包
|
||||
Torch/CUDA、CuPy 和 Orisys 可选的 Open3D 查看器。项目自己的 PyOpenGL
|
||||
夹爪 3D 模型仍然可用。当前参考产物约为 340 MiB:
|
||||
|
||||
```powershell
|
||||
python build\build_exe.py --profile lite --clean-dist
|
||||
```
|
||||
|
||||
只有需要 RAFT/CUDA 或 CuPy 后端时才使用完整配置。完整配置会包含 CUDA
|
||||
运行库,产物通常为数 GB:
|
||||
|
||||
```powershell
|
||||
python build\build_exe.py --profile full --clean-dist
|
||||
```
|
||||
|
||||
轻量配置要求传感器配置中的 `tracker.algorithm` 为 `dis`;打包脚本会在
|
||||
构建前检查并阻止错误的 RAFT 轻量包。
|
||||
|
||||
输出文件在:
|
||||
|
||||
```text
|
||||
@@ -86,6 +104,35 @@ dist\GripperDemo02\sdk
|
||||
|
||||
现场修改参数时,直接改 exe 同级的 `config\gripper.json`。
|
||||
|
||||
窗口版 EXE 的运行日志保存在同级 `logs` 目录。`logs\latest.txt` 记录最近
|
||||
一次运行的会话目录;其中 `main.log` 是 GUI/控制日志,`worker_*.log` 是
|
||||
左右传感器进程日志,并包含 OpenCV 原生警告和异常 traceback。程序自动保留
|
||||
最近 10 次运行。
|
||||
|
||||
### Linux
|
||||
|
||||
Windows 生成的 `.exe` 仅用于 Windows。Linux 需要在 Linux 环境使用同一份
|
||||
源码重新安装依赖和打包,推荐 Python 3.11:
|
||||
|
||||
```bash
|
||||
python3.11 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements-dev.txt
|
||||
pip install -e ../orisys-sdk
|
||||
python build/build_exe.py --profile lite --clean-dist
|
||||
```
|
||||
|
||||
Linux 产物没有 `.exe` 后缀,位于 `dist/GripperDemo02/GripperDemo02`。直接
|
||||
运行源码可使用:
|
||||
|
||||
```bash
|
||||
python -m gripper.visualizer --windowed
|
||||
```
|
||||
|
||||
将 `config/gripper.json` 的串口从 `COM*` 改为实际设备,例如
|
||||
`/dev/ttyUSB0`;摄像头通常对应 `/dev/video0` 和 `/dev/video1`。运行用户需
|
||||
拥有 `video` 与 `dialout` 设备组权限。WSL 还需要先透传 USB 摄像头和串口。
|
||||
|
||||
## 关键配置
|
||||
|
||||
- `trigger_normal_force`: 单侧法向力触发阈值。
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import atexit
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_LOG_STREAM = None
|
||||
|
||||
|
||||
def _cleanup_old_sessions(log_root, keep_sessions):
|
||||
sessions = sorted(
|
||||
(path for path in log_root.iterdir() if path.is_dir()),
|
||||
key=lambda path: path.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
for old_session in sessions[max(1, int(keep_sessions)) :]:
|
||||
try:
|
||||
shutil.rmtree(old_session)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _redirect_standard_streams(stream):
|
||||
for descriptor in (1, 2):
|
||||
try:
|
||||
os.dup2(stream.fileno(), descriptor)
|
||||
except OSError:
|
||||
pass
|
||||
sys.stdout = stream
|
||||
sys.stderr = stream
|
||||
|
||||
|
||||
def _install_exception_hooks(stream):
|
||||
def handle_exception(exc_type, exc_value, exc_traceback):
|
||||
print("\n[unhandled exception]", file=stream)
|
||||
traceback.print_exception(exc_type, exc_value, exc_traceback, file=stream)
|
||||
stream.flush()
|
||||
|
||||
sys.excepthook = handle_exception
|
||||
|
||||
try:
|
||||
import threading
|
||||
|
||||
def handle_thread_exception(args):
|
||||
print(f"\n[thread exception] name={args.thread.name}", file=stream)
|
||||
traceback.print_exception(
|
||||
args.exc_type,
|
||||
args.exc_value,
|
||||
args.exc_traceback,
|
||||
file=stream,
|
||||
)
|
||||
stream.flush()
|
||||
|
||||
threading.excepthook = handle_thread_exception
|
||||
except (AttributeError, ImportError):
|
||||
pass
|
||||
|
||||
|
||||
def configure_frozen_logging(app_name="GripperDemo02", keep_sessions=10):
|
||||
global _LOG_STREAM
|
||||
if _LOG_STREAM is not None:
|
||||
return Path(_LOG_STREAM.name)
|
||||
if not getattr(sys, "frozen", False):
|
||||
return None
|
||||
|
||||
app_dir = Path(sys.executable).resolve().parent
|
||||
log_root = app_dir / "logs"
|
||||
log_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
session_env = f"{app_name.upper()}_LOG_SESSION"
|
||||
session_name = os.environ.get(session_env)
|
||||
is_main_process = not session_name
|
||||
if is_main_process:
|
||||
session_name = datetime.now().strftime("%Y%m%d_%H%M%S") + f"_{os.getpid()}"
|
||||
os.environ[session_env] = session_name
|
||||
_cleanup_old_sessions(log_root, keep_sessions)
|
||||
|
||||
session_dir = log_root / session_name
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_name = "main.log" if is_main_process else f"worker_{os.getpid()}.log"
|
||||
log_path = session_dir / log_name
|
||||
_LOG_STREAM = open(log_path, "a", encoding="utf-8", buffering=1)
|
||||
_redirect_standard_streams(_LOG_STREAM)
|
||||
_install_exception_hooks(_LOG_STREAM)
|
||||
|
||||
if is_main_process:
|
||||
try:
|
||||
(log_root / "latest.txt").write_text(session_name + "\n", encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
print("=" * 72)
|
||||
print(f"{app_name} runtime log")
|
||||
print(f"time={datetime.now().isoformat(timespec='seconds')}")
|
||||
print(f"pid={os.getpid()} role={'main' if is_main_process else 'worker'}")
|
||||
print(f"executable={sys.executable}")
|
||||
print(f"log={log_path}")
|
||||
print(f"argv={sys.argv!r}")
|
||||
print("=" * 72)
|
||||
|
||||
atexit.register(_LOG_STREAM.flush)
|
||||
return log_path
|
||||
@@ -1,5 +1,6 @@
|
||||
import time
|
||||
import threading
|
||||
from collections import deque
|
||||
|
||||
from ..common.filters import clamp
|
||||
|
||||
@@ -22,7 +23,7 @@ class SideTriggerGripController:
|
||||
self.status_callback = status_callback
|
||||
self._stop_event = threading.Event()
|
||||
self._manual_lock = threading.Lock()
|
||||
self._manual_command = None
|
||||
self._manual_commands = deque()
|
||||
self._mode_lock = threading.Lock()
|
||||
self._control_mode = "smart"
|
||||
|
||||
@@ -47,7 +48,13 @@ class SideTriggerGripController:
|
||||
"position": position,
|
||||
}
|
||||
with self._manual_lock:
|
||||
self._manual_command = payload
|
||||
if command == "move_position":
|
||||
self._manual_commands = deque(
|
||||
item
|
||||
for item in self._manual_commands
|
||||
if item.get("command") != "move_position"
|
||||
)
|
||||
self._manual_commands.append(payload)
|
||||
|
||||
def set_control_mode(self, mode):
|
||||
if mode not in {"smart", "manual"}:
|
||||
@@ -61,9 +68,9 @@ class SideTriggerGripController:
|
||||
|
||||
def _pop_manual_command(self):
|
||||
with self._manual_lock:
|
||||
command = self._manual_command
|
||||
self._manual_command = None
|
||||
return command
|
||||
if not self._manual_commands:
|
||||
return None
|
||||
return self._manual_commands.popleft()
|
||||
|
||||
def run(self):
|
||||
self._stop_event.clear()
|
||||
@@ -828,9 +835,20 @@ class SideTriggerGripController:
|
||||
action,
|
||||
trigger_armed,
|
||||
):
|
||||
left_fps = 0.0
|
||||
right_fps = 0.0
|
||||
try:
|
||||
if len(self.reader.latest) >= 2:
|
||||
left_sample = self.reader.latest[0] or {}
|
||||
right_sample = self.reader.latest[1] or {}
|
||||
left_fps = float(left_sample.get("fps", 0.0) or 0.0)
|
||||
right_fps = float(right_sample.get("fps", 0.0) or 0.0)
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
pass
|
||||
print(
|
||||
f"L={feedback.left_normal:7.3f}{self.normal_unit} "
|
||||
f"R={feedback.right_normal:7.3f}{self.normal_unit} "
|
||||
f"fps=({left_fps:5.1f},{right_fps:5.1f}) "
|
||||
f"maxN={feedback.max_normal:7.3f}{self.normal_unit} "
|
||||
f"shear={feedback.max_shear:7.3f}{self.shear_unit} "
|
||||
f"trigger={int(trigger)} armed={int(trigger_armed)} "
|
||||
|
||||
@@ -6,6 +6,7 @@ import inspect
|
||||
import multiprocessing as mp
|
||||
import queue
|
||||
import time
|
||||
import traceback
|
||||
from multiprocessing import Process, Queue
|
||||
|
||||
import cv2
|
||||
@@ -248,6 +249,8 @@ def _sensor_force_worker(
|
||||
time.sleep(sleep_time)
|
||||
|
||||
except Exception as exc:
|
||||
print(f"sensor {sensor_id} worker failed: {exc}")
|
||||
traceback.print_exc()
|
||||
_put_latest(
|
||||
result_queue,
|
||||
{
|
||||
|
||||
+618
-105
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
# 开发 / 打包依赖
|
||||
-r requirements.txt
|
||||
|
||||
# PyInstaller 打包 exe
|
||||
pyinstaller>=6.0
|
||||
@@ -0,0 +1,28 @@
|
||||
# Gripper Demo 02 运行依赖
|
||||
# Python: 3.11(推荐 conda 环境 py311)
|
||||
#
|
||||
# 安装:
|
||||
# conda activate py311
|
||||
# pip install -r requirements.txt
|
||||
#
|
||||
# 说明:
|
||||
# - orisys 是触觉传感器私有 SDK,不在本文件中;需自行安装到当前环境
|
||||
# - 3D 模型需要 PyOpenGL;不装也能跑界面,但夹爪实体模型不可用
|
||||
# - 打包 exe 额外看 requirements-dev.txt
|
||||
|
||||
# 数值 / 图像
|
||||
numpy>=1.24
|
||||
opencv-python>=4.8
|
||||
numba>=0.60
|
||||
|
||||
# GUI
|
||||
PyQt5>=5.15
|
||||
pyqtgraph>=0.13
|
||||
|
||||
# 3D 夹爪模型(OpenGL)
|
||||
PyOpenGL>=3.1
|
||||
PyOpenGL-accelerate>=3.1
|
||||
|
||||
# 夹爪串口控制(Modbus RTU)
|
||||
minimalmodbus>=2.1
|
||||
pyserial>=3.5
|
||||
@@ -1,11 +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 -m gripper.visualizer %*
|
||||
pause
|
||||
Reference in New Issue
Block a user