串口搜索和摄像头搜索
@@ -19,6 +19,11 @@
|
||||
|
||||
## 运行
|
||||
|
||||
GUI 的完整模块说明、操作流程、参数表和故障排查见
|
||||
[GUI 使用说明书](docs/GUI_USER_MANUAL.zh-CN.md)。
|
||||
正式产品使用说明书(HTML,含界面截图,可打印/导出 PDF)见
|
||||
[产品使用说明书](docs/PRODUCT_MANUAL.zh-CN.html)。
|
||||
|
||||
从项目根目录运行:
|
||||
|
||||
```powershell
|
||||
@@ -86,6 +91,17 @@ python build\build_exe.py --profile lite --clean-dist
|
||||
python build\build_exe.py --profile full --clean-dist
|
||||
```
|
||||
|
||||
需要包含源码和 Python 3.11 虚拟环境的 Windows x64 包时,在 `py311`
|
||||
环境运行:
|
||||
|
||||
```powershell
|
||||
conda activate py311
|
||||
python build\build_venv_bundle.py
|
||||
```
|
||||
|
||||
该包使用精简运行环境,不包含 Torch/CUDA、CuPy 和 Open3D。输出位于
|
||||
`release\GripperDemo02-windows-x64-py311-venv-YYYYMMDD.zip`。
|
||||
|
||||
轻量配置要求传感器配置中的 `tracker.algorithm` 为 `dis`;打包脚本会在
|
||||
构建前检查并阻止错误的 RAFT 轻量包。
|
||||
|
||||
|
||||
@@ -99,8 +99,8 @@ def validate_lite_sensor_configs():
|
||||
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"),
|
||||
control_config.get("sensor_config1", "./config/ddjx01_q6a.json"),
|
||||
control_config.get("sensor_config2", "./config/ddjx01_q6a.json"),
|
||||
}
|
||||
for config_name in config_names:
|
||||
config_path = Path(config_name)
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import venv
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
BUILD_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_ROOT = BUILD_DIR.parent
|
||||
DIST_DIR = PROJECT_ROOT / "dist"
|
||||
RELEASE_DIR = PROJECT_ROOT / "release"
|
||||
LOCK_FILE = BUILD_DIR / "requirements-venv-win-x64.txt"
|
||||
RUNTIME_DIRS = (
|
||||
"gripper",
|
||||
"config",
|
||||
"sdk",
|
||||
"crt_ctag2f90d_gripper_visualization",
|
||||
"docs",
|
||||
)
|
||||
RUNTIME_FILES = (
|
||||
"README.md",
|
||||
"requirements.txt",
|
||||
"requirements-dev.txt",
|
||||
)
|
||||
|
||||
|
||||
def run(command, *, cwd=None, env=None, capture_output=False):
|
||||
print("+", subprocess.list2cmdline([str(item) for item in command]))
|
||||
return subprocess.run(
|
||||
[str(item) for item in command],
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=capture_output,
|
||||
)
|
||||
|
||||
|
||||
def require_supported_python():
|
||||
if sys.version_info[:2] != (3, 11):
|
||||
raise SystemExit(
|
||||
f"This bundle requires CPython 3.11; current interpreter is {sys.version.split()[0]}."
|
||||
)
|
||||
if os.name != "nt" or platform.machine().upper() not in {"AMD64", "X86_64"}:
|
||||
raise SystemExit(
|
||||
f"This lock targets Windows x64; current platform is {platform.platform()}."
|
||||
)
|
||||
|
||||
|
||||
def ensure_generated_path(path, parent):
|
||||
resolved = path.resolve()
|
||||
resolved_parent = parent.resolve()
|
||||
if resolved.parent != resolved_parent:
|
||||
raise SystemExit(f"Refusing to replace unexpected path: {resolved}")
|
||||
|
||||
|
||||
def remove_generated_dir(path, parent):
|
||||
ensure_generated_path(path, parent)
|
||||
if path.exists():
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
def copy_runtime_tree(source, target):
|
||||
if not source.exists():
|
||||
return False
|
||||
shutil.copytree(
|
||||
source,
|
||||
target,
|
||||
ignore=shutil.ignore_patterns(
|
||||
"__pycache__",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.tmp",
|
||||
"*.log",
|
||||
".git",
|
||||
".agents",
|
||||
".codex",
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def create_venv(venv_dir):
|
||||
print(f"Creating venv: {venv_dir}")
|
||||
venv.EnvBuilder(
|
||||
system_site_packages=False,
|
||||
clear=True,
|
||||
symlinks=False,
|
||||
with_pip=True,
|
||||
).create(venv_dir)
|
||||
python_exe = venv_dir / "Scripts" / "python.exe"
|
||||
if not python_exe.exists():
|
||||
raise SystemExit(f"venv interpreter was not created: {python_exe}")
|
||||
return python_exe
|
||||
|
||||
|
||||
def install_dependencies(python_exe, orisys_source):
|
||||
run(
|
||||
[
|
||||
python_exe,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"pip==26.1.2",
|
||||
"setuptools==78.1.0",
|
||||
"wheel==0.47.0",
|
||||
]
|
||||
)
|
||||
run(
|
||||
[
|
||||
python_exe,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--requirement",
|
||||
LOCK_FILE,
|
||||
]
|
||||
)
|
||||
run(
|
||||
[
|
||||
python_exe,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--no-deps",
|
||||
orisys_source,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def copy_application(bundle_dir):
|
||||
for name in RUNTIME_DIRS:
|
||||
source = PROJECT_ROOT / name
|
||||
if not copy_runtime_tree(source, bundle_dir / name):
|
||||
print(f"Warning: runtime directory does not exist: {source}")
|
||||
|
||||
for name in RUNTIME_FILES:
|
||||
source = PROJECT_ROOT / name
|
||||
if source.exists():
|
||||
shutil.copy2(source, bundle_dir / name)
|
||||
|
||||
shutil.copy2(LOCK_FILE, bundle_dir / "requirements-venv-lock.txt")
|
||||
|
||||
|
||||
def write_launchers(bundle_dir):
|
||||
launcher = bundle_dir / "start_gripper.cmd"
|
||||
launcher.write_text(
|
||||
"@echo off\n"
|
||||
"setlocal\n"
|
||||
"cd /d \"%~dp0\"\n"
|
||||
"set PYTHONUTF8=1\n"
|
||||
"set PYTHONNOUSERSITE=1\n"
|
||||
"if not exist logs mkdir logs\n"
|
||||
"\".venv\\Scripts\\python.exe\" -m gripper.visualizer --windowed %* "
|
||||
">> \"logs\\venv.log\" 2>&1\n"
|
||||
"if errorlevel 1 (\n"
|
||||
" echo Start failed. See logs\\venv.log\n"
|
||||
" pause\n"
|
||||
")\n",
|
||||
encoding="ascii",
|
||||
newline="\r\n",
|
||||
)
|
||||
|
||||
readme = bundle_dir / "README-VENV.txt"
|
||||
readme.write_text(
|
||||
"Gripper Demo 02 - Windows x64 Python 3.11 venv bundle\n"
|
||||
"========================================================\n\n"
|
||||
"1. Extract the complete archive to a writable directory.\n"
|
||||
"2. Edit config\\gripper.json: set video1, video2 and port.\n"
|
||||
"3. Run start_gripper.cmd. Add --dry-run for a serial-free test.\n"
|
||||
"4. Runtime output is appended to logs\\venv.log.\n\n"
|
||||
"This package uses a standard Python venv created with --copies. "
|
||||
"It is intended for the current machine or another Windows x64 "
|
||||
"machine with CPython 3.11 installed. Standard venv directories "
|
||||
"are not a fully standalone Python distribution. If the base "
|
||||
"interpreter differs, recreate .venv with Python 3.11 and install "
|
||||
"requirements-venv-lock.txt, then install the Orisys SDK.\n\n"
|
||||
"CUDA, Torch/RAFT, CuPy and Open3D are not included. The default "
|
||||
"application uses OpenCV DIS optical flow and the CPU Poisson backend.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def verify_bundle(python_exe, bundle_dir):
|
||||
verify_code = """
|
||||
import json
|
||||
import platform
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
import llvmlite
|
||||
import minimalmodbus
|
||||
import numba
|
||||
import numpy
|
||||
import OpenGL
|
||||
import orisys
|
||||
import pyqtgraph
|
||||
import scipy
|
||||
import serial
|
||||
from PyQt5 import QtCore
|
||||
from gripper.config.settings import Demo02Config
|
||||
from gripper.ui.visualizer import GripperDemo02Viewer
|
||||
|
||||
config = Demo02Config.from_args([
|
||||
"--control-config", "config/gripper.json", "--dry-run"
|
||||
])
|
||||
print(json.dumps({
|
||||
"python": sys.version.split()[0],
|
||||
"architecture": platform.machine(),
|
||||
"numpy": numpy.__version__,
|
||||
"opencv": cv2.__version__,
|
||||
"numba": numba.__version__,
|
||||
"llvmlite": llvmlite.__version__,
|
||||
"scipy": scipy.__version__,
|
||||
"pyqt": QtCore.PYQT_VERSION_STR,
|
||||
"qt": QtCore.QT_VERSION_STR,
|
||||
"pyqtgraph": pyqtgraph.__version__,
|
||||
"pyopengl": OpenGL.__version__,
|
||||
"orisys": getattr(orisys, "__version__", "unknown"),
|
||||
"dry_run": config.dry_run,
|
||||
}, indent=2))
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env["PYTHONNOUSERSITE"] = "1"
|
||||
result = run(
|
||||
[python_exe, "-c", verify_code],
|
||||
cwd=bundle_dir,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
)
|
||||
print(result.stdout.rstrip())
|
||||
(bundle_dir / "environment.json").write_text(
|
||||
result.stdout.strip() + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
freeze = run(
|
||||
[python_exe, "-m", "pip", "freeze", "--all"],
|
||||
cwd=bundle_dir,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
)
|
||||
(bundle_dir / "pip-freeze.txt").write_text(
|
||||
freeze.stdout,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def remove_bytecode(root):
|
||||
for cache_dir in sorted(root.rglob("__pycache__"), reverse=True):
|
||||
shutil.rmtree(cache_dir, ignore_errors=True)
|
||||
for pattern in ("*.pyc", "*.pyo"):
|
||||
for path in root.rglob(pattern):
|
||||
try:
|
||||
path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def directory_size(path):
|
||||
return sum(item.stat().st_size for item in path.rglob("*") if item.is_file())
|
||||
|
||||
|
||||
def create_zip(bundle_dir, archive_path):
|
||||
if archive_path.exists():
|
||||
archive_path.unlink()
|
||||
print(f"Creating archive: {archive_path}")
|
||||
with zipfile.ZipFile(
|
||||
archive_path,
|
||||
"w",
|
||||
compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=6,
|
||||
allowZip64=True,
|
||||
) as archive:
|
||||
for path in sorted(bundle_dir.rglob("*")):
|
||||
if path.is_file():
|
||||
archive.write(path, Path(bundle_dir.name) / path.relative_to(bundle_dir))
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
default="GripperDemo02Venv",
|
||||
help="Staging directory name under dist.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--orisys-source",
|
||||
type=Path,
|
||||
default=PROJECT_ROOT.parent / "orisys-sdk",
|
||||
help="Path to the private Orisys SDK source tree.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-archive",
|
||||
action="store_true",
|
||||
help="Create and verify the directory without writing a release zip.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
require_supported_python()
|
||||
if not LOCK_FILE.exists():
|
||||
raise SystemExit(f"Dependency lock does not exist: {LOCK_FILE}")
|
||||
orisys_source = args.orisys_source.resolve()
|
||||
if not (orisys_source / "pyproject.toml").exists():
|
||||
raise SystemExit(f"Orisys SDK source was not found: {orisys_source}")
|
||||
|
||||
DIST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
RELEASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
bundle_dir = DIST_DIR / args.name
|
||||
remove_generated_dir(bundle_dir, DIST_DIR)
|
||||
bundle_dir.mkdir(parents=True)
|
||||
|
||||
venv_dir = bundle_dir / ".venv"
|
||||
python_exe = create_venv(venv_dir)
|
||||
install_dependencies(python_exe, orisys_source)
|
||||
copy_application(bundle_dir)
|
||||
write_launchers(bundle_dir)
|
||||
verify_bundle(python_exe, bundle_dir)
|
||||
remove_bytecode(bundle_dir)
|
||||
|
||||
unpacked_mb = directory_size(bundle_dir) / (1024 * 1024)
|
||||
print(f"Unpacked bundle size: {unpacked_mb:.1f} MiB")
|
||||
|
||||
if args.no_archive:
|
||||
print(f"Bundle ready: {bundle_dir}")
|
||||
return 0
|
||||
|
||||
stamp = datetime.now().strftime("%Y%m%d")
|
||||
archive_path = RELEASE_DIR / f"GripperDemo02-windows-x64-py311-venv-{stamp}.zip"
|
||||
create_zip(bundle_dir, archive_path)
|
||||
digest = sha256_file(archive_path)
|
||||
checksum_path = archive_path.with_suffix(archive_path.suffix + ".sha256")
|
||||
checksum_path.write_text(f"{digest} {archive_path.name}\n", encoding="ascii")
|
||||
archive_mb = archive_path.stat().st_size / (1024 * 1024)
|
||||
|
||||
report = {
|
||||
"bundle": str(bundle_dir),
|
||||
"archive": str(archive_path),
|
||||
"checksum": digest,
|
||||
"unpacked_mib": round(unpacked_mb, 1),
|
||||
"archive_mib": round(archive_mb, 1),
|
||||
}
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Capture product-manual screenshots using the real visualizer entry path.
|
||||
|
||||
Equivalent to: python -m gripper.visualizer
|
||||
(fullscreen by default, user config/gripper.json, start_control)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
OUT = REPO / "docs" / "images" / "manual"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
sys.path.insert(0, str(REPO))
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QApplication, QFrame
|
||||
|
||||
from gripper.ui.visualizer import GripperDemo02Viewer, _load_company_icon
|
||||
|
||||
|
||||
def process(app, ms=200):
|
||||
end = time.monotonic() + ms / 1000.0
|
||||
while time.monotonic() < end:
|
||||
app.processEvents()
|
||||
time.sleep(0.02)
|
||||
|
||||
|
||||
def save(widget, name: str):
|
||||
path = OUT / name
|
||||
pix = widget.grab()
|
||||
if pix.isNull() or not pix.save(str(path), "PNG"):
|
||||
raise RuntimeError(f"save failed: {name}")
|
||||
print(f"saved {path.relative_to(REPO)}")
|
||||
return path
|
||||
|
||||
|
||||
def wait_ready(app, viewer, timeout=60.0):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
process(app, 80)
|
||||
with viewer.status_lock:
|
||||
st = viewer.latest_status
|
||||
if not st:
|
||||
continue
|
||||
left = st.get("left_sample") or {}
|
||||
right = st.get("right_sample") or {}
|
||||
if left and right and st.get("state") != "error":
|
||||
print(
|
||||
f"ready state={st.get('state')} action={st.get('action')} "
|
||||
f"fps=({(left or {}).get('fps')}, {(right or {}).get('fps')})"
|
||||
)
|
||||
return st
|
||||
if st.get("state") == "error":
|
||||
raise RuntimeError(f"controller error: {st.get('action')}")
|
||||
raise TimeoutError("sensors not ready")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
app = QApplication(sys.argv[:1])
|
||||
app.setWindowIcon(_load_company_icon(64))
|
||||
|
||||
# Same path as gripper.visualizer.main (no --windowed → fullscreen)
|
||||
viewer = GripperDemo02Viewer([])
|
||||
restored = viewer.restore_saved_layout(force_windowed=False)
|
||||
if not restored:
|
||||
viewer.showFullScreen()
|
||||
viewer.start_control()
|
||||
|
||||
print("waiting for live sensors...")
|
||||
wait_ready(app, viewer, timeout=75.0)
|
||||
# let curves accumulate
|
||||
process(app, 4000)
|
||||
|
||||
save(viewer, "01-main-overview.png")
|
||||
|
||||
viewer.left_flow.raise_()
|
||||
viewer.right_flow.raise_()
|
||||
process(app, 200)
|
||||
save(viewer, "02-flow-windows.png")
|
||||
|
||||
viewer.plot_panel.raise_()
|
||||
process(app, 200)
|
||||
save(viewer, "03-force-plots.png")
|
||||
|
||||
viewer.gripper_control_panel.raise_()
|
||||
process(app, 200)
|
||||
save(viewer, "04-gripper-control-smart.png")
|
||||
|
||||
viewer.set_control_mode("manual")
|
||||
process(app, 300)
|
||||
save(viewer, "05-gripper-control-manual.png")
|
||||
viewer.set_control_mode("smart")
|
||||
process(app, 200)
|
||||
|
||||
viewer.sensor_control_panel.raise_()
|
||||
process(app, 200)
|
||||
save(viewer, "06-sensor-control.png")
|
||||
|
||||
top = viewer.findChild(QFrame, "TopBar")
|
||||
if top is not None:
|
||||
save(top, "07-top-bar.png")
|
||||
|
||||
viewer.open_visual_settings()
|
||||
process(app, 500)
|
||||
dialog = viewer.visual_settings_dialog
|
||||
save(dialog, "08-settings-visual.png")
|
||||
|
||||
dialog.nav.setCurrentRow(1)
|
||||
process(app, 200)
|
||||
save(dialog, "09-settings-layout.png")
|
||||
|
||||
for row in range(dialog.nav.count()):
|
||||
title = dialog.nav.item(row).text()
|
||||
if title == "串口 / 设备":
|
||||
dialog.nav.setCurrentRow(row)
|
||||
process(app, 200)
|
||||
save(dialog, "11-settings-serial.png")
|
||||
elif title == "触发 / 夹持":
|
||||
dialog.nav.setCurrentRow(row)
|
||||
process(app, 200)
|
||||
save(dialog, "10-settings-trigger.png")
|
||||
|
||||
dialog.close()
|
||||
process(app, 300)
|
||||
# live hold_check may not be active; still save current live frame
|
||||
save(viewer, "12-hold-check-state.png")
|
||||
|
||||
print("stopping...")
|
||||
viewer.stop_control()
|
||||
process(app, 2000)
|
||||
viewer.close()
|
||||
app.processEvents()
|
||||
print(f"done → {OUT}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Capture live Gripper Demo 02 window using screen mouse clicks + PrintWindow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
import time
|
||||
from ctypes import wintypes
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
OUT = REPO / "docs" / "images" / "manual"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
gdi32 = ctypes.windll.gdi32
|
||||
user32.SetProcessDPIAware()
|
||||
PW_RENDERFULLCONTENT = 0x00000002
|
||||
|
||||
|
||||
class RECT(ctypes.Structure):
|
||||
_fields_ = [("left", wintypes.LONG), ("top", wintypes.LONG),
|
||||
("right", wintypes.LONG), ("bottom", wintypes.LONG)]
|
||||
|
||||
|
||||
class POINT(ctypes.Structure):
|
||||
_fields_ = [("x", wintypes.LONG), ("y", wintypes.LONG)]
|
||||
|
||||
|
||||
def find_hwnd(title="Gripper Demo 02"):
|
||||
found = []
|
||||
|
||||
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||||
def cb(hwnd, _):
|
||||
if not user32.IsWindowVisible(hwnd):
|
||||
return True
|
||||
n = user32.GetWindowTextLengthW(hwnd)
|
||||
if n <= 0:
|
||||
return True
|
||||
buf = ctypes.create_unicode_buffer(n + 1)
|
||||
user32.GetWindowTextW(hwnd, buf, n + 1)
|
||||
if title in buf.value:
|
||||
found.append(hwnd)
|
||||
return True
|
||||
|
||||
user32.EnumWindows(cb, 0)
|
||||
return found[0] if found else None
|
||||
|
||||
|
||||
def capture(hwnd, path: Path):
|
||||
rect = RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rect))
|
||||
w, h = rect.right - rect.left, rect.bottom - rect.top
|
||||
hdc = user32.GetWindowDC(hwnd)
|
||||
mem = gdi32.CreateCompatibleDC(hdc)
|
||||
bmp = gdi32.CreateCompatibleBitmap(hdc, w, h)
|
||||
old = gdi32.SelectObject(mem, bmp)
|
||||
if not user32.PrintWindow(hwnd, mem, PW_RENDERFULLCONTENT):
|
||||
screen = user32.GetDC(0)
|
||||
gdi32.BitBlt(mem, 0, 0, w, h, screen, rect.left, rect.top, 0x00CC0020)
|
||||
user32.ReleaseDC(0, screen)
|
||||
|
||||
class BIH(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("biSize", wintypes.DWORD), ("biWidth", wintypes.LONG),
|
||||
("biHeight", wintypes.LONG), ("biPlanes", wintypes.WORD),
|
||||
("biBitCount", wintypes.WORD), ("biCompression", wintypes.DWORD),
|
||||
("biSizeImage", wintypes.DWORD), ("biXPelsPerMeter", wintypes.LONG),
|
||||
("biYPelsPerMeter", wintypes.LONG), ("biClrUsed", wintypes.DWORD),
|
||||
("biClrImportant", wintypes.DWORD),
|
||||
]
|
||||
|
||||
class BI(ctypes.Structure):
|
||||
_fields_ = [("bmiHeader", BIH), ("bmiColors", wintypes.DWORD * 3)]
|
||||
|
||||
bi = BI()
|
||||
bi.bmiHeader.biSize = ctypes.sizeof(BIH)
|
||||
bi.bmiHeader.biWidth = w
|
||||
bi.bmiHeader.biHeight = -h
|
||||
bi.bmiHeader.biPlanes = 1
|
||||
bi.bmiHeader.biBitCount = 32
|
||||
buf = (ctypes.c_ubyte * (w * h * 4))()
|
||||
gdi32.GetDIBits(mem, bmp, 0, h, buf, ctypes.byref(bi), 0)
|
||||
gdi32.SelectObject(mem, old)
|
||||
gdi32.DeleteObject(bmp)
|
||||
gdi32.DeleteDC(mem)
|
||||
user32.ReleaseDC(hwnd, hdc)
|
||||
Image.frombuffer("RGBA", (w, h), bytes(buf), "raw", "BGRA", 0, 1).convert("RGB").save(path)
|
||||
print(f"saved {path.name} ({w}x{h})")
|
||||
return w, h
|
||||
|
||||
|
||||
def click_screen(x, y):
|
||||
user32.SetCursorPos(int(x), int(y))
|
||||
time.sleep(0.05)
|
||||
user32.mouse_event(0x0002, 0, 0, 0, 0) # LEFTDOWN
|
||||
time.sleep(0.04)
|
||||
user32.mouse_event(0x0004, 0, 0, 0, 0) # LEFTUP
|
||||
|
||||
|
||||
def main():
|
||||
hwnd = find_hwnd()
|
||||
if not hwnd:
|
||||
print("window not found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
||||
user32.SetForegroundWindow(hwnd)
|
||||
time.sleep(0.5)
|
||||
|
||||
rect = RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rect))
|
||||
left, top = rect.left, rect.top
|
||||
w, h = rect.right - rect.left, rect.bottom - rect.top
|
||||
print(f"window rect=({left},{top}) {w}x{h}")
|
||||
|
||||
# 1) main overview
|
||||
capture(hwnd, OUT / "01-main-overview.png")
|
||||
full = Image.open(OUT / "01-main-overview.png")
|
||||
|
||||
# panel crops from live layout
|
||||
crops = {
|
||||
"02-flow-windows.png": (0, int(h * 0.06), w, int(h * 0.55)),
|
||||
"03-force-plots.png": (int(w * 0.55), int(h * 0.08), w - 8, int(h * 0.72)),
|
||||
"04-gripper-control-smart.png": (8, int(h * 0.62), int(w * 0.32), h - 8),
|
||||
"06-sensor-control.png": (int(w * 0.68), int(h * 0.68), w - 8, h - 8),
|
||||
"07-top-bar.png": (0, 0, w, max(70, int(h * 0.065))),
|
||||
"12-hold-check-state.png": (0, 0, w, h),
|
||||
}
|
||||
for name, box in crops.items():
|
||||
full.crop(box).save(OUT / name)
|
||||
print(f"saved {name}")
|
||||
|
||||
# 2) switch to manual mode — button near bottom-left of gripper panel
|
||||
# "手动模式" is left of "智能模式" in gripper control panel
|
||||
manual_x = left + int(w * 0.06)
|
||||
manual_y = top + int(h * 0.68)
|
||||
print(f"click manual mode @ ({manual_x},{manual_y})")
|
||||
click_screen(manual_x, manual_y)
|
||||
time.sleep(0.6)
|
||||
capture(hwnd, OUT / "05-gripper-control-manual.png")
|
||||
|
||||
# switch back to smart
|
||||
smart_x = left + int(w * 0.14)
|
||||
smart_y = top + int(h * 0.68)
|
||||
click_screen(smart_x, smart_y)
|
||||
time.sleep(0.4)
|
||||
|
||||
# 3) open settings — measured from top-bar: first icon ~x=1640
|
||||
settings_x = left + 1640
|
||||
settings_y = top + 32
|
||||
print(f"click settings @ ({settings_x},{settings_y})")
|
||||
click_screen(settings_x, settings_y)
|
||||
time.sleep(1.5)
|
||||
capture(hwnd, OUT / "08-settings-visual.png")
|
||||
|
||||
# embedded settings panel (~72% of window, centered)
|
||||
# nav list is left column inside the panel
|
||||
dlg_left = left + int(w * 0.14)
|
||||
dlg_top = top + int(h * 0.11)
|
||||
nav_x = dlg_left + 90
|
||||
# nav rows: 画面(0), 布局(1), 相机(2), 串口(3), ... 触发(7)
|
||||
row_y0 = dlg_top + 95
|
||||
row_h = 34
|
||||
|
||||
click_screen(nav_x, row_y0 + 1 * row_h) # 布局
|
||||
time.sleep(0.6)
|
||||
capture(hwnd, OUT / "09-settings-layout.png")
|
||||
|
||||
click_screen(nav_x, row_y0 + 3 * row_h) # 串口 / 设备
|
||||
time.sleep(0.6)
|
||||
capture(hwnd, OUT / "11-settings-serial.png")
|
||||
|
||||
click_screen(nav_x, row_y0 + 7 * row_h) # 触发 / 夹持
|
||||
time.sleep(0.6)
|
||||
capture(hwnd, OUT / "10-settings-trigger.png")
|
||||
|
||||
# close settings panel (X on panel header)
|
||||
click_screen(left + int(w * 0.14) + int(w * 0.72) - 30, dlg_top + 28)
|
||||
time.sleep(0.3)
|
||||
|
||||
print("done")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Generate manual diagrams (SVG). Fast path — no Docker / LaTeX."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
GENERATOR = REPO / "build" / "generate_manual_diagrams.py"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("Generating manual diagrams via generate_manual_diagrams.py …")
|
||||
subprocess.run([sys.executable, str(GENERATOR)], check=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,217 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Generate manual SVG diagrams with correct UTF-8 encoding."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[1] / "docs" / "assets" / "diagrams"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
SYSTEM_ARCH = r'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 520" role="img">
|
||||
<defs>
|
||||
<linearGradient id="sensorGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#dbeafe"/><stop offset="100%" stop-color="#bfdbfe"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="procGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e0e7ff"/><stop offset="100%" stop-color="#c7d2fe"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="ctrlGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fef3c7"/><stop offset="100%" stop-color="#fde68a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="guiGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#d1fae5"/><stop offset="100%" stop-color="#a7f3d0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hwGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f1f5f9"/><stop offset="100%" stop-color="#e2e8f0"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
|
||||
<path d="M0,0 L8,4 L0,8 Z" fill="#475569"/>
|
||||
</marker>
|
||||
<filter id="shadow" x="-4%" y="-4%" width="108%" height="112%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#0f172a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<style>
|
||||
.t1{font:700 15px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#0f172a}
|
||||
.t2{font:400 12px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#475569}
|
||||
.tl{font:700 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#64748b;letter-spacing:.08em}
|
||||
.te{font:500 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#334155}
|
||||
.tp{font:600 10px Consolas,monospace;fill:#1e40af}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect x="24" y="52" width="832" height="108" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
|
||||
<text x="40" y="72" class="tl">感知层</text>
|
||||
<rect x="24" y="176" width="832" height="148" rx="8" fill="#fafafa" stroke="#e2e8f0"/>
|
||||
<text x="40" y="196" class="tl">软件处理层</text>
|
||||
<rect x="24" y="340" width="832" height="108" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
|
||||
<text x="40" y="360" class="tl">执行与呈现层</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="56" y="88" width="168" height="56" rx="8" fill="url(#sensorGrad)" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<text x="140" y="112" text-anchor="middle" class="t1">左触觉传感器</text>
|
||||
<text x="140" y="130" text-anchor="middle" class="t2">USB 摄像头 + 敏感面</text>
|
||||
</g>
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="656" y="88" width="168" height="56" rx="8" fill="url(#sensorGrad)" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<text x="740" y="112" text-anchor="middle" class="t1">右触觉传感器</text>
|
||||
<text x="740" y="130" text-anchor="middle" class="t2">USB 摄像头 + 敏感面</text>
|
||||
</g>
|
||||
|
||||
<path d="M224 116 H340" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<path d="M656 116 H540" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="440" y="108" text-anchor="middle" class="te">视频帧采集</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="300" y="212" width="280" height="76" rx="10" fill="url(#procGrad)" stroke="#6366f1" stroke-width="1.5"/>
|
||||
<text x="440" y="240" text-anchor="middle" class="t1">触觉力计算 / 光流处理</text>
|
||||
<text x="440" y="260" text-anchor="middle" class="t2">法向力 · 切向力 · 光流/幅值图像</text>
|
||||
<text x="440" y="276" text-anchor="middle" class="t2">Orisys SDK · OpenCV DIS</text>
|
||||
</g>
|
||||
|
||||
<path d="M140 144 V188 H300" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<path d="M740 144 V188 H580" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="56" y="220" width="200" height="60" rx="8" fill="url(#ctrlGrad)" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="156" y="246" text-anchor="middle" class="t1">智能夹持状态机</text>
|
||||
<text x="156" y="264" text-anchor="middle" class="t2">触发 · 闭合 · 加力 · 释放</text>
|
||||
</g>
|
||||
|
||||
<path d="M300 250 H256" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="278" y="242" text-anchor="middle" class="te">力信号</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="624" y="220" width="200" height="60" rx="8" fill="url(#hwGrad)" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="724" y="246" text-anchor="middle" class="t1">夹爪控制器</text>
|
||||
<text x="724" y="264" text-anchor="middle" class="t2">Modbus RTU 从站</text>
|
||||
</g>
|
||||
|
||||
<path d="M256 250 H420" fill="none" stroke="#64748b" stroke-width="1.5"/>
|
||||
<path d="M420 250 H624" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="440" y="242" text-anchor="middle" class="tp">Modbus RTU / 串口</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="624" y="368" width="200" height="56" rx="8" fill="url(#hwGrad)" stroke="#475569" stroke-width="1.5"/>
|
||||
<text x="724" y="392" text-anchor="middle" class="t1">智能夹爪(执行机构)</text>
|
||||
<text x="724" y="410" text-anchor="middle" class="t2">位置 · 力度 · 开/合/保持</text>
|
||||
</g>
|
||||
<path d="M724 280 V368" fill="none" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="738" y="328" class="te">驱动</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="240" y="356" width="280" height="80" rx="10" fill="url(#guiGrad)" stroke="#059669" stroke-width="1.5"/>
|
||||
<text x="380" y="384" text-anchor="middle" class="t1">GUI 监控界面</text>
|
||||
<text x="380" y="404" text-anchor="middle" class="t2">3D 模型 · 光流画面 · 力曲线 · 状态</text>
|
||||
<text x="380" y="422" text-anchor="middle" class="t2">PyQt5 · PyOpenGL · pyqtgraph</text>
|
||||
</g>
|
||||
|
||||
<path d="M156 280 V320 H380 V356" fill="none" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="168" y="312" class="te">状态 / 动作</text>
|
||||
<path d="M440 288 V356" fill="none" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="452" y="328" class="te">图像 / 力值</text>
|
||||
|
||||
<path d="M624 396 H520 V250 H256" fill="none" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="5 4" marker-end="url(#arrow)"/>
|
||||
<text x="572" y="388" text-anchor="middle" class="te">位置反馈</text>
|
||||
|
||||
<g transform="translate(56,468)">
|
||||
<line x1="0" y1="8" x2="28" y2="8" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="36" y="12" class="t2">数据 / 控制流</text>
|
||||
<line x1="140" y1="8" x2="168" y2="8" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="5 4" marker-end="url(#arrow)"/>
|
||||
<text x="176" y="12" class="t2">反馈</text>
|
||||
</g>
|
||||
</svg>
|
||||
'''
|
||||
|
||||
GRIP_FLOW = r'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 920 324" role="img">
|
||||
<defs>
|
||||
<linearGradient id="gIdle" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#dbeafe"/><stop offset="100%" stop-color="#bfdbfe"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gAct" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fef3c7"/><stop offset="100%" stop-color="#fde68a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gHold" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fce7f3"/><stop offset="100%" stop-color="#fbcfe8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gRec" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#d1fae5"/><stop offset="100%" stop-color="#a7f3d0"/>
|
||||
</linearGradient>
|
||||
<marker id="arr" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
|
||||
<path d="M0,0 L8,4 L0,8 Z" fill="#475569"/>
|
||||
</marker>
|
||||
<filter id="sh" x="-6%" y="-8%" width="112%" height="120%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2.5" flood-color="#0f172a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<style>
|
||||
.cn{font:700 14px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#0f172a}
|
||||
.en{font:600 11px Consolas,monospace;fill:#475569}
|
||||
.sub{font:400 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#64748b}
|
||||
.ed{font:500 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#334155}
|
||||
.nt{font:400 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#64748b}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<g filter="url(#sh)"><rect x="24" y="40" width="148" height="72" rx="10" fill="url(#gIdle)" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="98" y="66" text-anchor="middle" class="cn">等待触发</text>
|
||||
<text x="98" y="84" text-anchor="middle" class="en">open_wait</text>
|
||||
<text x="98" y="100" text-anchor="middle" class="sub">已就绪 armed</text></g>
|
||||
|
||||
<path d="M172 76 H208" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="190" y="68" text-anchor="middle" class="ed">侧边触发</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="208" y="40" width="148" height="72" rx="10" fill="url(#gAct)" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="282" y="66" text-anchor="middle" class="cn">闭合中</text>
|
||||
<text x="282" y="84" text-anchor="middle" class="en">closing</text>
|
||||
<text x="282" y="100" text-anchor="middle" class="sub">寻找物体</text></g>
|
||||
|
||||
<path d="M356 76 H392" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="374" y="68" text-anchor="middle" class="ed">双侧接触</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="392" y="40" width="148" height="72" rx="10" fill="url(#gAct)" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="466" y="66" text-anchor="middle" class="cn">夹持加力</text>
|
||||
<text x="466" y="84" text-anchor="middle" class="en">gripping</text>
|
||||
<text x="466" y="100" text-anchor="middle" class="sub">低力 → 基础力</text></g>
|
||||
|
||||
<path d="M540 76 H576" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="558" y="68" text-anchor="middle" class="ed">到达保持力</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="576" y="40" width="148" height="72" rx="10" fill="url(#gHold)" stroke="#db2777" stroke-width="1.5"/>
|
||||
<text x="650" y="66" text-anchor="middle" class="cn">夹持检测</text>
|
||||
<text x="650" y="84" text-anchor="middle" class="en">hold_check</text>
|
||||
<text x="650" y="100" text-anchor="middle" class="sub">自适应补力 / 释放检测</text></g>
|
||||
|
||||
<path d="M724 76 H760" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="742" y="68" text-anchor="middle" class="ed">取物释放</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="760" y="40" width="136" height="72" rx="10" fill="url(#gRec)" stroke="#059669" stroke-width="1.5"/>
|
||||
<text x="828" y="66" text-anchor="middle" class="cn">张开恢复</text>
|
||||
<text x="828" y="84" text-anchor="middle" class="en">open_recover</text>
|
||||
<text x="828" y="100" text-anchor="middle" class="sub">位置 / 力恢复</text></g>
|
||||
|
||||
<path d="M828 112 V204 H98 V112" fill="none" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<rect x="340" y="188" width="240" height="24" rx="4" fill="#eff6ff" stroke="#bfdbfe"/>
|
||||
<text x="460" y="204" text-anchor="middle" class="ed" fill="#1e40af">恢复完成 + 休息时间(rearm)→ 再次就绪</text>
|
||||
|
||||
<path d="M282 112 V152 H828" fill="none" stroke="#dc2626" stroke-width="1.2" stroke-dasharray="5 4"/>
|
||||
<rect x="200" y="128" width="164" height="22" rx="4" fill="#fef2f2" stroke="#fecaca"/>
|
||||
<text x="282" y="143" text-anchor="middle" class="ed" fill="#b91c1c">闭合超时 close_timeout → 张开</text>
|
||||
|
||||
<rect x="24" y="232" width="872" height="76" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
|
||||
<text x="40" y="254" class="cn" font-size="12">阶段说明</text>
|
||||
<text x="40" y="274" class="nt">① 等待触发:夹爪张开,单侧力超阈且为新上升沿时进入闭合。</text>
|
||||
<text x="40" y="294" class="nt">② 闭合~夹持:物体接触后低力保持,分步加力至 hold_force,并按切向负载自适应补力。</text>
|
||||
<text x="40" y="314" class="nt">③ 释放~恢复:力变化超阈则张开;满足恢复条件并经过 rearm_seconds 后,方可再次触发。</text>
|
||||
</svg>
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
(OUT / "system-architecture.svg").write_text(SYSTEM_ARCH, encoding="utf-8")
|
||||
(OUT / "grip-state-flow.svg").write_text(GRIP_FLOW, encoding="utf-8")
|
||||
print("Wrote", OUT / "system-architecture.svg")
|
||||
print("Wrote", OUT / "grip-state-flow.svg")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Render manual diagrams as high-resolution PNG.
|
||||
|
||||
LaTeX/TikZ sources: docs/diagrams/tex/*.tex
|
||||
Compile with: python build/compile_manual_diagrams.py (requires Docker or local XeLaTeX)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
|
||||
from matplotlib import font_manager as fm
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
OUT = REPO / "docs" / "assets" / "diagrams"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
FONT = next(
|
||||
(f.name for f in fm.fontManager.ttflist if "YaHei" in f.name or "Noto Sans CJK" in f.name),
|
||||
"sans-serif",
|
||||
)
|
||||
plt.rcParams.update(
|
||||
{
|
||||
"font.family": FONT,
|
||||
"font.sans-serif": [FONT, "Microsoft YaHei", "SimHei", "DejaVu Sans"],
|
||||
"axes.unicode_minus": False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _box(ax, xy, w, h, title, fc, ec, subtitle=None, fs=9):
|
||||
x, y = xy
|
||||
ax.add_patch(
|
||||
FancyBboxPatch(
|
||||
(x, y),
|
||||
w,
|
||||
h,
|
||||
boxstyle="round,pad=0.02,rounding_size=0.07",
|
||||
linewidth=1.2,
|
||||
edgecolor=ec,
|
||||
facecolor=fc,
|
||||
)
|
||||
)
|
||||
if subtitle:
|
||||
ax.text(x + w / 2, y + h * 0.65, title, ha="center", va="center", fontsize=fs, fontweight="bold")
|
||||
ax.text(x + w / 2, y + h * 0.3, subtitle, ha="center", va="center", fontsize=fs - 1.5, color="#475569")
|
||||
else:
|
||||
ax.text(x + w / 2, y + h / 2, title, ha="center", va="center", fontsize=fs, fontweight="bold")
|
||||
|
||||
|
||||
def _arr(ax, p0, p1, text=None, color="#64748b", dashed=False):
|
||||
style = "Simple,tail_width=0.45,head_width=3.8,head_length=5.5"
|
||||
ax.add_patch(
|
||||
FancyArrowPatch(
|
||||
p0,
|
||||
p1,
|
||||
arrowstyle=style,
|
||||
linewidth=1.0,
|
||||
linestyle="-" if not dashed else (0, (4, 3)),
|
||||
color=color,
|
||||
shrinkA=1,
|
||||
shrinkB=1,
|
||||
connectionstyle="arc3,rad=0.0",
|
||||
)
|
||||
)
|
||||
if text:
|
||||
mx, my = (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2
|
||||
ax.text(mx, my + 0.08, text, ha="center", va="bottom", fontsize=7.5, color="#334155")
|
||||
|
||||
|
||||
def render_system_architecture(path: Path) -> None:
|
||||
fig, ax = plt.subplots(figsize=(9.2, 5.2), dpi=240)
|
||||
ax.set_xlim(0, 9.2)
|
||||
ax.set_ylim(0, 5.2)
|
||||
ax.axis("off")
|
||||
|
||||
layers = [(3.55, 1.05, "感知层"), (2.05, 1.25, "软件处理层"), (0.35, 1.05, "执行与呈现层")]
|
||||
for y, h, name in layers:
|
||||
ax.add_patch(FancyBboxPatch((0.2, y), 8.8, h, boxstyle="round,pad=0.01,rounding_size=0.05", fc="#f8fafc", ec="#d1d5db", lw=0.8))
|
||||
ax.text(0.35, y + h - 0.15, name, fontsize=8, fontweight="bold", color="#64748b")
|
||||
|
||||
_box(ax, (0.45, 3.75), 1.85, 0.62, "左触觉传感器", "#eff6ff", "#3b82f6", "USB 摄像头")
|
||||
_box(ax, (7.0, 3.75), 1.85, 0.62, "右触觉传感器", "#eff6ff", "#3b82f6", "USB 摄像头")
|
||||
_box(ax, (3.1, 2.35), 3.0, 0.72, "触觉力计算 / 光流", "#eef2ff", "#6366f1", "法向力 · 切向力 · 图像")
|
||||
_box(ax, (0.45, 2.35), 2.0, 0.62, "智能夹持状态机", "#fff7ed", "#d97706")
|
||||
_box(ax, (6.85, 2.35), 2.0, 0.62, "夹爪控制器", "#f1f5f9", "#64748b", "Modbus RTU")
|
||||
_box(ax, (3.1, 0.55), 3.0, 0.62, "GUI 监控界面", "#ecfdf5", "#059669", "3D · 光流 · 曲线 · 状态")
|
||||
_box(ax, (6.85, 0.55), 2.0, 0.62, "智能夹爪", "#f1f5f9", "#475569")
|
||||
|
||||
_arr(ax, (1.37, 3.75), (3.5, 3.07), "视频帧")
|
||||
_arr(ax, (7.0, 3.75), (6.7, 3.07), "视频帧")
|
||||
_arr(ax, (3.1, 2.72), (2.45, 2.68), "力信号")
|
||||
_arr(ax, (2.45, 2.5), (6.85, 2.66), "串口命令")
|
||||
_arr(ax, (7.85, 2.35), (7.85, 1.17), "驱动")
|
||||
_arr(ax, (4.6, 2.35), (4.6, 1.17), "图像/力值")
|
||||
_arr(ax, (1.45, 2.35), (3.4, 0.9), "状态")
|
||||
# feedback: gripper → state machine (dotted, via bottom)
|
||||
ax.plot([7.85, 7.85, 0.15, 0.15], [0.55, 0.22, 0.22, 2.66], color="#94a3b8", lw=0.9, linestyle=(0, (4, 3)))
|
||||
ax.annotate("", xy=(0.45, 2.66), xytext=(0.15, 2.66),
|
||||
arrowprops=dict(arrowstyle="-|>", color="#94a3b8", lw=0.9))
|
||||
ax.text(4.0, 0.1, "位置反馈(虚线)", fontsize=7.5, color="#94a3b8", ha="center")
|
||||
ax.add_patch(
|
||||
FancyArrowPatch(
|
||||
(6.85, 0.86),
|
||||
(1.45, 2.2),
|
||||
arrowstyle="Simple,tail_width=0.4,head_width=3.5,head_length=5",
|
||||
linewidth=0.9,
|
||||
linestyle=(0, (4, 3)),
|
||||
color="#94a3b8",
|
||||
connectionstyle="arc3,rad=-0.15",
|
||||
shrinkA=2,
|
||||
shrinkB=2,
|
||||
)
|
||||
)
|
||||
ax.text(4.2, 1.45, "位置反馈", fontsize=7.5, color="#64748b", ha="center")
|
||||
|
||||
fig.savefig(path, bbox_inches="tight", facecolor="white", pad_inches=0.08)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def render_grip_flow(path: Path) -> None:
|
||||
fig, ax = plt.subplots(figsize=(10.5, 2.5), dpi=240)
|
||||
ax.set_xlim(0, 10.5)
|
||||
ax.set_ylim(0, 2.5)
|
||||
ax.axis("off")
|
||||
|
||||
specs = [
|
||||
(0.2, "等待触发", "open_wait", "#eff6ff", "#2563eb"),
|
||||
(2.1, "闭合中", "closing", "#fff7ed", "#d97706"),
|
||||
(4.0, "夹持加力", "gripping", "#fff7ed", "#d97706"),
|
||||
(5.9, "夹持检测", "hold_check", "#fdf2f8", "#db2777"),
|
||||
(7.8, "张开恢复", "open_recover", "#ecfdf5", "#059669"),
|
||||
]
|
||||
w, h, y = 1.55, 0.88, 1.35
|
||||
xs = []
|
||||
for x, cn, en, fc, ec in specs:
|
||||
_box(ax, (x, y), w, h, cn, fc, ec, en)
|
||||
xs.append(x + w / 2)
|
||||
|
||||
for i, label in enumerate(["侧边触发", "接触", "到保持力", "取物"]):
|
||||
_arr(ax, (xs[i] + w / 2 - 0.02, y + h / 2), (xs[i + 1] - w / 2 + 0.02, y + h / 2), label)
|
||||
|
||||
ax.plot([xs[-1], xs[-1], xs[0], xs[0]], [y, 0.55, 0.55, y], color="#2563eb", lw=1.0)
|
||||
ax.add_patch(FancyArrowPatch((xs[0], y), (xs[0], y + 0.01), arrowstyle="Simple,head_width=3.5,head_length=5", color="#2563eb"))
|
||||
ax.annotate("", xy=(xs[0], y), xytext=(xs[0], 0.55), arrowprops=dict(arrowstyle="-|>", color="#2563eb", lw=1.0))
|
||||
ax.text(4.9, 0.38, "恢复 + rearm", ha="center", fontsize=7.5, color="#1e40af")
|
||||
|
||||
ax.plot([xs[1], xs[1], xs[-1]], [y, 0.85, 0.85], color="#dc2626", lw=1.0, linestyle=(0, (4, 3)))
|
||||
ax.annotate("", xy=(xs[-1], 0.85), xytext=(xs[1], 0.85), arrowprops=dict(arrowstyle="-|>", color="#dc2626", lw=1.0, linestyle=(0, (4, 3))))
|
||||
ax.text(6.2, 0.72, "闭合超时", ha="center", fontsize=7.5, color="#b91c1c")
|
||||
|
||||
fig.savefig(path, bbox_inches="tight", facecolor="white", pad_inches=0.08)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
render_system_architecture(OUT / "system-architecture.png")
|
||||
render_grip_flow(OUT / "grip-state-flow.png")
|
||||
print("Wrote", OUT / "system-architecture.png")
|
||||
print("Wrote", OUT / "grip-state-flow.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
# Gripper Demo 02 Windows x64 / CPython 3.11 venv lock.
|
||||
# Optional CUDA, RAFT, CuPy and Open3D backends are intentionally excluded.
|
||||
|
||||
numpy==2.4.4
|
||||
opencv-python==5.0.0.93
|
||||
llvmlite==0.48.0
|
||||
numba==0.66.0
|
||||
scipy==1.17.1
|
||||
|
||||
PyQt5==5.15.11
|
||||
PyQt5-Qt5==5.15.2
|
||||
PyQt5_sip==12.18.0
|
||||
pyqtgraph==0.14.0
|
||||
|
||||
PyOpenGL==3.1.10
|
||||
PyOpenGL-accelerate==3.1.10
|
||||
|
||||
minimalmodbus==2.1.1
|
||||
pyserial==3.5
|
||||
|
||||
pyinstaller==6.21.0
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.7 MiB |
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "prototype",
|
||||
"version": "1.0",
|
||||
"camera": {
|
||||
"resolution_raw": [1920, 1080],
|
||||
"fps": 30,
|
||||
"units": 15,
|
||||
"units_used": 9,
|
||||
"resolution_stitch": [365, 365],
|
||||
"stitch_res_list": [365,263,123,63]
|
||||
},
|
||||
"stitcher": {
|
||||
"min_area_frac": 0.01,
|
||||
"max_area_frac": 0.05,
|
||||
"max_aspect_ratio": 50,
|
||||
"index": [1,2,3,6,7,8,11,12,13],
|
||||
"margin": 10,
|
||||
"nominal_ov_pos": 100,
|
||||
"n": [3,3],
|
||||
"row_tol": 0.6,
|
||||
"box_size": [250, 250]
|
||||
},
|
||||
"tracker": {
|
||||
"adaptive": true,
|
||||
"lower_threshold": 12.0,
|
||||
"upper_threshold": 18.0,
|
||||
"loop_closure_interval": 1,
|
||||
"algorithm": "dis",
|
||||
"dis_preset": "medium",
|
||||
"dis_mode": 3,
|
||||
"algorithm_list": ["raft", "dis"],
|
||||
"hhd_downsample": 3
|
||||
},
|
||||
"contact": {
|
||||
"min_contour_area": 3000,
|
||||
"flow_threshold":5,
|
||||
"div_threshold":0.05,
|
||||
"expansion_threshold": 0.01,
|
||||
"kernel_size": [5,5],
|
||||
"denoise_sigma": 12
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
{
|
||||
"video1": "0",
|
||||
"video1": "2",
|
||||
"video2": "1",
|
||||
"sensor_config1": "./config/ddjx01.json",
|
||||
"sensor_config2": "./config/ddjx01.json",
|
||||
"sensor_config1": "./config/ddjx01_q6a.json",
|
||||
"sensor_config2": "./config/ddjx01_q6a.json",
|
||||
"sensor_fps": 30.0,
|
||||
"motion_threshold": 0.0,
|
||||
"sensor_cpu": false,
|
||||
"port": "COM5",
|
||||
"port": "COM9",
|
||||
"slave_id": 1,
|
||||
"baudrate": 115200,
|
||||
"serial_timeout": 0.2,
|
||||
"open_pos": 0,
|
||||
"close_pos": 9000,
|
||||
"speed": 50,
|
||||
"accel": 80,
|
||||
"decel": 80,
|
||||
"speed": 55,
|
||||
"accel": 70,
|
||||
"decel": 70,
|
||||
"release_open_speed": 50,
|
||||
"release_open_accel": 120,
|
||||
"release_open_decel": 120,
|
||||
"open_force": 20,
|
||||
"close_force": 20,
|
||||
"grip_start_force": 20,
|
||||
"hold_force": 20,
|
||||
"initial_force": 20,
|
||||
"release_open_accel": 70,
|
||||
"release_open_decel": 70,
|
||||
"open_force": 25,
|
||||
"close_force": 33,
|
||||
"grip_start_force": 25,
|
||||
"hold_force": 33,
|
||||
"initial_force": 25,
|
||||
"force_min": 20,
|
||||
"force_max": 80,
|
||||
"force_max": 99,
|
||||
"control_hz": 30.0,
|
||||
"baseline_seconds": 1.0,
|
||||
"filter_alpha": 0.55,
|
||||
"shear_filter_alpha": 0.65,
|
||||
"trigger_normal_force": 0.1,
|
||||
"trigger_shear_force": 0.1,
|
||||
"trigger_clear_stable_seconds": 0.2,
|
||||
"trigger_clear_stable_seconds": 0.15,
|
||||
"open_wait_stable_samples": 12,
|
||||
"open_wait_stable_range_n": 0.03,
|
||||
"open_wait_stable_range_n": 0.01,
|
||||
"open_wait_stable_trend_n": 0.015,
|
||||
"grip_normal_force": 0.2,
|
||||
"grip_requires_both": true,
|
||||
"close_timeout_seconds": 5.0,
|
||||
"close_timeout_seconds": 3.5,
|
||||
"closing_command_interval": 0.2,
|
||||
"force_ramp_step": 5,
|
||||
"force_ramp_interval": 0.02,
|
||||
"grip_settle_seconds": 0.05,
|
||||
"force_ramp_step": 1,
|
||||
"force_ramp_interval": 0.1,
|
||||
"grip_settle_seconds": 0.15,
|
||||
"adaptive_grip_enabled": true,
|
||||
"adaptive_shear_start_force": 0.1,
|
||||
"adaptive_shear_full_force": 0.6,
|
||||
"adaptive_force_min": 15,
|
||||
"adaptive_force_min": 33,
|
||||
"adaptive_force_max": 80,
|
||||
"adaptive_force_step": 1,
|
||||
"adaptive_force_interval": 0.05,
|
||||
"release_arm_delay_seconds": 0.8,
|
||||
"release_normal_change": 0.2,
|
||||
"release_shear_change": 0.2,
|
||||
"rearm_seconds": 0.5,
|
||||
"adaptive_force_step": 5,
|
||||
"adaptive_force_interval": 0.03,
|
||||
"release_arm_delay_seconds": 1.2,
|
||||
"release_normal_change": 0.35,
|
||||
"release_shear_change": 0.35,
|
||||
"rearm_seconds": 0.4,
|
||||
"recover_normal_force": 0.02,
|
||||
"recover_shear_force": 0.02,
|
||||
"recover_stable_seconds": 0.4,
|
||||
|
||||
@@ -0,0 +1,749 @@
|
||||
# Orisys 夹爪 GUI 使用说明书
|
||||
|
||||
> **正式产品说明书(HTML,含截图):** [产品使用说明书](PRODUCT_MANUAL.zh-CN.html)
|
||||
|
||||
> 文档版本:V0.1(快速初版)
|
||||
> 更新日期:2026-07-17
|
||||
> 适用对象:现场演示人员、设备调试人员、参数维护人员
|
||||
> 对应程序:Orisys 夹爪当前主分支 GUI
|
||||
|
||||
## 目录
|
||||
|
||||
1. [文档说明](#1-文档说明)
|
||||
2. [使用前检查](#2-使用前检查)
|
||||
3. [快速启动](#3-快速启动)
|
||||
4. [主界面总览](#4-主界面总览)
|
||||
5. [顶部状态与程序控制栏](#5-顶部状态与程序控制栏)
|
||||
6. [夹爪 3D 模型模块](#6-夹爪-3d-模型模块)
|
||||
7. [左右光流窗口](#7-左右光流窗口)
|
||||
8. [曲线图模块](#8-曲线图模块)
|
||||
9. [夹爪控制模块](#9-夹爪控制模块)
|
||||
10. [传感器控制模块](#10-传感器控制模块)
|
||||
11. [智能夹持标准流程](#11-智能夹持标准流程)
|
||||
12. [状态与动作说明](#12-状态与动作说明)
|
||||
13. [设置模块](#13-设置模块)
|
||||
14. [参数分组说明](#14-参数分组说明)
|
||||
15. [布局管理](#15-布局管理)
|
||||
16. [日志说明](#16-日志说明)
|
||||
17. [常见故障排查](#17-常见故障排查)
|
||||
18. [快捷键和退出](#18-快捷键和退出)
|
||||
19. [现场操作检查表](#19-现场操作检查表)
|
||||
|
||||
## 1. 文档说明
|
||||
|
||||
本说明书介绍 Orisys 夹爪图形界面的启动、监控、智能夹持、手动控制、参数设置、布局管理和故障排查。
|
||||
|
||||
系统由以下部分组成:
|
||||
|
||||
- 左、右两个视觉触觉传感器及摄像头。
|
||||
- 通过 Modbus RTU 串口控制的夹爪。
|
||||
- 触觉力计算和自动夹持控制程序。
|
||||
- PyQt5 GUI、夹爪 3D 模型、光流画面和六路力曲线。
|
||||
|
||||
数据和控制关系如下:
|
||||
|
||||
```text
|
||||
左/右摄像头 -> Orisys 触觉计算 -> 法向力/切向力/光流画面
|
||||
|
|
||||
v
|
||||
智能夹持状态机
|
||||
|
|
||||
v
|
||||
串口 <-------------------------- 夹爪控制器
|
||||
|
|
||||
v
|
||||
GUI 状态、3D 模型、曲线
|
||||
```
|
||||
|
||||
## 2. 使用前检查
|
||||
|
||||
### 2.1 硬件检查
|
||||
|
||||
1. 确认夹爪电源、控制器和机械连接正常。
|
||||
2. 确认左、右触觉传感器摄像头均已连接。
|
||||
3. 确认 USB 串口已经识别,Windows 下记下 `COM` 端口号。
|
||||
4. 清理夹爪开合范围内的物体,首次调试先使用较低速度和力度。
|
||||
5. 启动和重新标定基准时,让两侧触觉传感器保持空载、静止。
|
||||
|
||||
### 2.2 关键文件
|
||||
|
||||
打包版程序目录中的关键内容如下,运行时应保留完整目录结构:
|
||||
|
||||
```text
|
||||
GripperDemo02/
|
||||
├─ GripperDemo02.exe
|
||||
├─ _internal/
|
||||
├─ config/
|
||||
│ ├─ gripper.json
|
||||
│ ├─ ddjx01_q6a.json
|
||||
│ ├─ compound_eye.png
|
||||
│ └─ gui_layout.json # 保存布局后生成
|
||||
├─ sdk/
|
||||
├─ crt_ctag2f90d_gripper_visualization/
|
||||
└─ logs/ # EXE 首次运行后生成
|
||||
```
|
||||
|
||||
主要配置文件:
|
||||
|
||||
- `config/gripper.json`:摄像头、串口、位置、速度、力度和状态机参数。
|
||||
- `config/ddjx01_q6a.json`:触觉传感器和光流算法参数。
|
||||
- `config/gui_layout.json`:窗口、浮动面板、3D 视角和画面方向。
|
||||
|
||||
## 3. 快速启动
|
||||
|
||||
### 3.1 Windows 打包版
|
||||
|
||||
1. 解压整个发布包,不要只取出 EXE。
|
||||
2. 打开 `config/gripper.json`。
|
||||
3. 将 `video1`、`video2` 设置为左右摄像头编号,默认是 `"0"`、`"1"`。
|
||||
4. 将 `port` 设置为实际串口,例如 `"COM5"`。
|
||||
5. 双击 `GripperDemo02.exe`。
|
||||
6. GUI 显示后会自动启动控制程序,并默认进入智能模式。
|
||||
7. 启动阶段夹爪先张开,随后进行传感器基准标定。此时保持两侧传感器空载。
|
||||
8. 确认左右传感器状态点有数据、FPS 正常,并等待顶部显示 `open-wait-armed(已就绪)`。
|
||||
|
||||
程序窗口出现后通常不需要再次点击顶部“启动”图标。只有控制程序已经停止并回到空闲状态时,才使用该图标重新启动。
|
||||
|
||||
### 3.2 源码运行
|
||||
|
||||
Windows:
|
||||
|
||||
```powershell
|
||||
conda activate py311
|
||||
python -m gripper.visualizer
|
||||
```
|
||||
|
||||
窗口模式:
|
||||
|
||||
```powershell
|
||||
python -m gripper.visualizer --windowed
|
||||
```
|
||||
|
||||
只跳过真实夹爪串口动作、保留传感器和 GUI:
|
||||
|
||||
```powershell
|
||||
python -m gripper.visualizer --windowed --dry-run
|
||||
```
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
python -m gripper.visualizer --windowed
|
||||
```
|
||||
|
||||
Linux 下串口通常为 `/dev/ttyUSB0`,摄像头通常对应 `/dev/video0`、`/dev/video1`。Windows、Linux x86_64 和 Linux ARM64 使用各自平台构建的程序,Windows EXE 不跨平台运行。
|
||||
|
||||
## 4. 主界面总览
|
||||
|
||||
主界面分为以下模块:
|
||||
|
||||
| 模块 | 主要用途 |
|
||||
| --- | --- |
|
||||
| 顶部状态与程序控制栏 | 查看状态,打开设置,启动、停止或重启控制程序,管理窗口 |
|
||||
| 夹爪 3D 模型 | 查看夹爪实时开合位置、力比例和控制状态 |
|
||||
| 左右光流窗口 | 查看两个传感器的光流箭头图或幅值图 |
|
||||
| 曲线图区域 | 查看左右法向力、切向力 X/Y 的最近趋势 |
|
||||
| 夹爪控制区域 | 切换智能/手动模式,设置速度、最大力和位置,执行手动动作 |
|
||||
| 传感器控制区域 | 查看左右传感器状态、FPS、力值和夹爪运行参数,重启单侧传感器 |
|
||||
|
||||
光流窗口、曲线图、夹爪控制和传感器控制都是 3D 工作区上的浮动面板,可以移动和调整大小。
|
||||
|
||||
## 5. 顶部状态与程序控制栏
|
||||
|
||||
### 5.1 状态显示
|
||||
|
||||
顶部左侧显示两个字段:
|
||||
|
||||
- `state`:控制状态,例如等待触发、闭合中、夹持检测。
|
||||
- `action`:当前正在执行的细分动作,例如等待稳定、检测到物体、释放张开。
|
||||
|
||||
界面同时保留英文状态码和中文说明,便于和日志内容对应。
|
||||
|
||||
### 5.2 顶部图标
|
||||
|
||||
从设置图标开始,常用图标功能如下:
|
||||
|
||||
| 图标 | 功能 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 设置 | 打开设置 | 调整画面、布局和 `gripper.json` 参数 |
|
||||
| 启动 | 启动控制程序 | 空闲状态可用;重新创建传感器、串口和控制器 |
|
||||
| 停止 | 停止控制程序 | 运行状态可用;停止过程需要等待设备释放 |
|
||||
| 重启 | 重启控制程序 | 先停止并释放旧控制器,再重新连接和标定 |
|
||||
| 最小化 | 最小化主窗口 | 不停止控制程序 |
|
||||
| 全屏 | 全屏/窗口切换 | 只改变显示方式 |
|
||||
| 关闭 | 退出程序 | 停止控制线程并关闭主窗口 |
|
||||
|
||||
鼠标悬停在图标上可查看提示。启动、停止、重启过程中,相关按钮会自动锁定,避免重复操作。
|
||||
|
||||
注意:顶部“停止”是软件控制流程的停止按钮,不是独立的硬件急停装置。
|
||||
|
||||
## 6. 夹爪 3D 模型模块
|
||||
|
||||
### 6.1 显示内容
|
||||
|
||||
3D 模型根据实时夹爪位置切换开合姿态。区域内还显示:
|
||||
|
||||
- 当前控制状态。
|
||||
- 左、右法向力。
|
||||
- 当前夹爪位置。
|
||||
- 当前力比例 `Force`。
|
||||
- 自适应目标力比例 `Target`。
|
||||
|
||||
### 6.2 视角操作
|
||||
|
||||
- 鼠标左键拖动:旋转观察视角。
|
||||
- 鼠标中键拖动:平移观察中心。
|
||||
- 鼠标滚轮:缩放模型。
|
||||
|
||||
保存当前布局时,3D 相机中心、距离、俯仰角、方位角和视场角会一并保存。
|
||||
|
||||
### 6.3 3D 模型异常
|
||||
|
||||
若该区域显示依赖或模型错误提示,依次检查:
|
||||
|
||||
1. `PyOpenGL` 和 `pyqtgraph.opengl` 是否存在。
|
||||
2. 显卡驱动和 OpenGL 是否正常。
|
||||
3. `crt_ctag2f90d_gripper_visualization` 目录及 STL/URDF 文件是否完整。
|
||||
4. EXE 是否仍位于完整打包目录内。
|
||||
|
||||
3D 模型异常不会改变配置文件,但会影响夹爪姿态的可视化观察。
|
||||
|
||||
## 7. 左右光流窗口
|
||||
|
||||
### 7.1 光流与幅值
|
||||
|
||||
左右窗口分别对应两个触觉传感器。点击窗口右上角的模式按钮,可在以下画面间切换:
|
||||
|
||||
- `光流`:显示光流方向箭头,用于观察触觉表面的位移方向。
|
||||
- `幅值`:显示光流变化强度,用于观察形变集中区域。
|
||||
|
||||
按钮文字表示当前显示模式。幅值画面暂时无数据时,窗口会回退显示光流画面。
|
||||
|
||||
### 7.2 移动和缩放
|
||||
|
||||
- 拖动窗口标题栏或画面区域可移动窗口。
|
||||
- 将鼠标移到窗口右下角约 18 像素区域,拖动可改变大小。
|
||||
- 图像始终保持原始宽高比,不会为了填满窗口而拉伸变形。
|
||||
- 窗口会被限制在 3D 工作区内,防止完全移出可视区域。
|
||||
|
||||
### 7.3 旋转和镜像
|
||||
|
||||
进入“设置 -> 画面”,左右画面可分别执行:
|
||||
|
||||
- `旋转`:每次顺时针旋转 90 度。
|
||||
- `水平`:切换水平镜像。
|
||||
- `垂直`:切换垂直镜像。
|
||||
- `重置`:恢复 0 度并关闭水平、垂直镜像。
|
||||
|
||||
画面变化立即生效。需要下次启动继续保留时,再进入“设置 -> 布局”执行“保存当前布局”。
|
||||
|
||||
## 8. 曲线图模块
|
||||
|
||||
### 8.1 曲线布局
|
||||
|
||||
曲线采用 3 行 x 2 列排列:
|
||||
|
||||
| 行 | 左列 | 右列 |
|
||||
| --- | --- | --- |
|
||||
| 第 1 行 | 左法向力 | 右法向力 |
|
||||
| 第 2 行 | 左切向力 X | 右切向力 X |
|
||||
| 第 3 行 | 左切向力 Y | 右切向力 Y |
|
||||
|
||||
横轴为时间,单位秒;纵轴为力,单位 N。
|
||||
|
||||
### 8.2 曲线显示规则
|
||||
|
||||
- 显示最近约 6 秒数据,最多保留 180 个 GUI 刷新点。
|
||||
- 默认 Y 轴范围为 `0~1 N`。
|
||||
- 数据超过当前范围后,Y 轴自动向上扩展。
|
||||
- 启动或重新启动控制程序时,曲线历史和 Y 轴范围重置。
|
||||
- 曲线经过平滑处理,适合观察趋势。
|
||||
- GUI 将小于 `0.05 N` 的显示值归零,负值也按 0 绘制。
|
||||
- 曲线区禁用了单图鼠标缩放和右键菜单,避免六张图的比例被分别改乱。
|
||||
|
||||
移动曲线面板时拖动“曲线图区域”标题,调整面板大小时拖动面板右下角。
|
||||
|
||||
## 9. 夹爪控制模块
|
||||
|
||||
### 9.1 智能模式
|
||||
|
||||
程序启动后默认选择智能模式。智能模式由触觉信号自动完成触发闭合、接触检测、逐步加力、自适应夹持和取物释放。
|
||||
|
||||
智能模式下,速度、最大力、位置滑条和手动动作按钮被锁定,避免手动命令干扰自动状态机。
|
||||
|
||||
### 9.2 手动模式
|
||||
|
||||
控制程序处于运行状态时,点击“手动模式”后可操作:
|
||||
|
||||
- 速度。
|
||||
- 最大力。
|
||||
- 夹爪位置。
|
||||
- 保持/释放。
|
||||
- 夹爪张开。
|
||||
- 夹爪闭合。
|
||||
|
||||
控制程序停止时,手动控件保持禁用。
|
||||
|
||||
### 9.3 速度
|
||||
|
||||
- 范围:`0~100%`。
|
||||
- 左右箭头步进:`5%`。
|
||||
- 拖动或点击调整后,本次运行的控制器速度会更新。
|
||||
- 运行时调整不会自动写回 `gripper.json`。
|
||||
|
||||
### 9.4 最大力
|
||||
|
||||
- 范围:`0~100%`。
|
||||
- 左右箭头步进:`5%`。
|
||||
- 调整滑条本身不会立即驱动夹爪。
|
||||
- 下一次执行张开、闭合、保持或指定位置动作时,该值随命令发送。
|
||||
- 实际执行值仍受 `force_min` 和 `force_max` 限制。
|
||||
|
||||
### 9.5 夹爪位置
|
||||
|
||||
- 可调范围由 `open_pos` 与 `close_pos` 的较小值和较大值确定。
|
||||
- 左右箭头步进:`100`。
|
||||
- 拖动滑条时只更新目标显示,释放滑条后发送位置命令。
|
||||
- 点击左右箭头后立即发送新的位置命令。
|
||||
- GUI 收到位置到达反馈后恢复跟随实际位置;长时间无到达反馈时约 3 秒后恢复跟随。
|
||||
|
||||
### 9.6 手动动作按钮
|
||||
|
||||
| 按钮 | 行为 |
|
||||
| --- | --- |
|
||||
| 夹爪张开 | 以当前速度、最大力移动到 `open_pos` |
|
||||
| 夹爪闭合 | 以当前速度、最大力移动到 `close_pos` |
|
||||
| 保持 | 读取当前位置,以当前最大力保持该位置;按钮文字变为“释放” |
|
||||
| 释放 | 取消手动保持状态,回到手动待机 |
|
||||
|
||||
“释放”不等同于发送张开命令。需要物理张开夹爪时,点击“夹爪张开”。
|
||||
|
||||
## 10. 传感器控制模块
|
||||
|
||||
### 10.1 传感器状态
|
||||
|
||||
左右传感器分别显示状态点和 FPS:
|
||||
|
||||
- 绿色:当前状态包中存在该侧传感器样本。
|
||||
- 红色:当前没有收到该侧传感器样本。
|
||||
- `--`:尚未获得有效状态或 FPS。
|
||||
|
||||
FPS 文本约每秒更新一次。短时间红点可能来自相机启动、重启或临时取帧中断,应结合日志判断。
|
||||
|
||||
### 10.2 单侧重启
|
||||
|
||||
左右传感器各有一个重启图标,仅在控制程序运行时启用。点击后会停止该侧旧采集进程、清理队列并重新拉起。
|
||||
|
||||
单侧重启不会重新执行整个控制程序的力基准标定。相机恢复后若力值零点明显漂移,应使用顶部“重启控制程序”,并在标定阶段保持传感器空载。
|
||||
|
||||
### 10.3 数据卡
|
||||
|
||||
| 数据 | 含义 |
|
||||
| --- | --- |
|
||||
| 左/右法向力 | 垂直触觉表面的力,单位 N |
|
||||
| 左/右切向力 | 触觉表面平面内的合力,单位 N |
|
||||
| 夹爪当前力比例 | 当前控制状态使用的力百分比 |
|
||||
| 夹爪目标力比例 | 自适应夹持计算出的目标力百分比 |
|
||||
| 速度 | 当前控制器速度百分比 |
|
||||
|
||||
## 11. 智能夹持标准流程
|
||||
|
||||
1. 启动控制程序后,夹爪先移动到张开位置。
|
||||
2. 系统在 `baseline_seconds` 内标定传感器基准,此时保持两侧空载。
|
||||
3. 状态进入 `open_wait(等待触发)`,先采集稳定样本。
|
||||
4. 顶部动作显示 `open-wait-armed(已就绪)` 后,用物体从任意一侧轻扫触觉传感器。
|
||||
5. 法向力或切向力变化达到阈值后,动作显示 `side-trigger-close`,夹爪开始闭合。
|
||||
6. 将物体移动到夹爪中间。默认配置要求左右两侧都达到接触阈值。
|
||||
7. 检测到物体后,夹爪先用较低力度保持当前位置,再逐步加到基础保持力。
|
||||
8. 系统根据切向力估计负载,按小步进提高目标夹紧力。
|
||||
9. 自适应加力稳定后,系统重新记录释放参考值并进入释放检测。
|
||||
10. 取走或移动物体造成法向力、切向力变化达到释放阈值时,夹爪张开。
|
||||
11. 状态进入 `open_recover(张开恢复)`,等待位置到位或残余力清除。
|
||||
12. 满足恢复和休息时间后重新进入等待触发。
|
||||
|
||||
若闭合超过 `close_timeout_seconds` 仍未检测到物体,系统会执行超时张开。
|
||||
|
||||
## 12. 状态与动作说明
|
||||
|
||||
### 12.1 state 状态
|
||||
|
||||
| state | 中文显示 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `open_wait` | 等待触发 | 建立稳定基准或等待新的侧边触发 |
|
||||
| `closing` | 闭合中 | 已触发闭合,正在寻找物体 |
|
||||
| `gripping` | 夹持加力 | 已检测到物体,正在逐步增加夹持力 |
|
||||
| `hold_check` | 夹持检测 | 自适应加力、保持及释放检测 |
|
||||
| `open_recover` | 张开恢复 | 已张开,等待位置或力信号恢复 |
|
||||
| `manual_close` | 手动闭合 | 手动闭合状态 |
|
||||
| `manual_hold` | 手动保持 | 手动保持当前位置 |
|
||||
| `manual_position` | 手动到位 | 手动移动到指定位置 |
|
||||
| `error` | 错误 | 控制线程发生异常,结合日志排查 |
|
||||
|
||||
### 12.2 常见 action 动作
|
||||
|
||||
| action | 含义 |
|
||||
| --- | --- |
|
||||
| `waiting-for-sensor-samples` | 等待左右传感器数据 |
|
||||
| `open-wait-stabilizing` | 正在建立稳定触发基准 |
|
||||
| `open-wait-armed` | 触发检测已经就绪 |
|
||||
| `side-trigger-close` | 侧边触发后开始闭合 |
|
||||
| `closing-to-grip` | 闭合寻找物体 |
|
||||
| `closing-continue` | 按间隔继续发送闭合命令 |
|
||||
| `object-detected-low-force` | 已检测到物体,切换到低力保持 |
|
||||
| `close-timeout-open` | 闭合超时,执行张开 |
|
||||
| `grip-ramp-up` | 正在逐步增加基础夹持力 |
|
||||
| `grip-hold` | 基础夹持力保持 |
|
||||
| `hold-reference-armed` | 已记录夹持参考值 |
|
||||
| `adaptive-grip-ramp` | 正在自适应增加夹持力 |
|
||||
| `adaptive-grip-wait` | 等待下一次自适应加力 |
|
||||
| `release-arm-delay` | 等待释放检测稳定 |
|
||||
| `release-reference-armed` | 已重新记录释放参考值 |
|
||||
| `hold-check` | 正在监测保持状态 |
|
||||
| `force-change-release-open` | 检测到取物力变化并执行张开 |
|
||||
| `open-recover-position` | 正在等待张开位置恢复 |
|
||||
| `open-recover-position-stabilizing` | 张开位置已接近目标,等待稳定 |
|
||||
| `open-recover-wait-force-clear` | 等待残余触觉力清除 |
|
||||
| `open-recover-stabilizing` | 恢复条件已满足,等待稳定时间 |
|
||||
| `open-ready` / `open-ready-position` | 恢复完成,准备重新等待触发 |
|
||||
| `manual-*` | 当前执行的手动动作 |
|
||||
| `left-sensor-restart` / `right-sensor-restart` | 对应传感器正在重启 |
|
||||
| `*-sensor-restart-failed` | 对应传感器重启失败 |
|
||||
|
||||
## 13. 设置模块
|
||||
|
||||
点击顶部设置图标打开设置。窗口模式下,设置作为独立窗口显示;全屏模式下,设置面板嵌入主界面并可拖动。
|
||||
|
||||
### 13.1 画面页
|
||||
|
||||
分别调整左、右光流画面的旋转和镜像。修改立即生效,保存布局后跨次启动保留。
|
||||
|
||||
### 13.2 布局页
|
||||
|
||||
#### 保存当前布局
|
||||
|
||||
保存以下内容到 `config/gui_layout.json`:
|
||||
|
||||
- 主窗口普通状态的位置和大小。
|
||||
- 保存时的全屏或最大化状态。
|
||||
- 左右光流、曲线、夹爪控制、传感器控制面板的位置和大小。
|
||||
- 3D 相机中心、距离、俯仰角、方位角和视场角。
|
||||
- 左右画面的旋转和镜像。
|
||||
|
||||
#### 应用已保存布局
|
||||
|
||||
在当前会话中恢复:
|
||||
|
||||
- 浮动面板的位置和大小。
|
||||
- 3D 相机视角。
|
||||
- 左右画面的旋转和镜像。
|
||||
|
||||
该按钮不会改变当前主窗口大小、位置或全屏状态。完整窗口状态会在下次启动时恢复;使用 `--windowed` 启动时会保持窗口模式。
|
||||
|
||||
保存布局不包含曲线历史、当前光流/幅值选择、智能/手动模式、滑条临时值和控制运行状态。
|
||||
|
||||
### 13.3 参数页
|
||||
|
||||
设置界面按功能显示当前 `gripper.json` 中已有的参数:
|
||||
|
||||
- 相机 / 传感器。
|
||||
- 串口 / 设备。
|
||||
- 位置 / 运动。
|
||||
- 力度。
|
||||
- 控制 / 滤波。
|
||||
- 触发 / 夹持。
|
||||
- 自适应 / 释放。
|
||||
- 恢复。
|
||||
- 标定。
|
||||
- 其他未归类字段。
|
||||
|
||||
控件规则:
|
||||
|
||||
- 布尔值使用复选框。
|
||||
- 字典、数组使用 JSON 文本编辑框。
|
||||
- 数字和字符串使用单行输入框。
|
||||
|
||||
底部按钮:
|
||||
|
||||
- `重新载入`:重新读取磁盘配置,覆盖当前未保存编辑。
|
||||
- `保存到 gripper.json`:检查数据类型并写入配置文件。
|
||||
- `关闭`:关闭设置窗口。
|
||||
|
||||
保存配置后,点击顶部“重启控制程序”让全部参数重新加载。保存参数与保存布局是两个独立操作。
|
||||
|
||||
## 14. 参数分组说明
|
||||
|
||||
以下默认值来自当前 `config/gripper.json`。百分比参数通常使用 `0~100` 整数,力学阈值单位通常为 N,时间单位为秒。
|
||||
|
||||
### 14.1 相机 / 传感器
|
||||
|
||||
| 参数 | 默认值 | 作用 |
|
||||
| --- | ---: | --- |
|
||||
| `video1` | `"0"` | 左摄像头编号或设备路径 |
|
||||
| `video2` | `"1"` | 右摄像头编号或设备路径 |
|
||||
| `sensor_config1` | `./config/ddjx01_q6a.json` | 左触觉传感器配置 |
|
||||
| `sensor_config2` | `./config/ddjx01_q6a.json` | 右触觉传感器配置 |
|
||||
| `sensor_fps` | `30.0` | 传感器目标采集频率 |
|
||||
| `motion_threshold` | `0.0` | Orisys 形变/运动检测阈值 |
|
||||
| `sensor_cpu` | `false` | 是否要求传感器 SDK 使用 CPU 模式 |
|
||||
|
||||
### 14.2 串口 / 设备
|
||||
|
||||
| 参数 | 默认值 | 作用 |
|
||||
| --- | ---: | --- |
|
||||
| `port` | `COM5` | 夹爪串口号;Linux 示例 `/dev/ttyUSB0` |
|
||||
| `slave_id` | `1` | Modbus 从站 ID |
|
||||
| `baudrate` | `115200` | 串口波特率 |
|
||||
| `serial_timeout` | `0.2` | 单次串口操作超时时间 |
|
||||
|
||||
### 14.3 位置 / 运动
|
||||
|
||||
| 参数 | 默认值 | 作用 |
|
||||
| --- | ---: | --- |
|
||||
| `open_pos` | `0` | 完全张开目标位置 |
|
||||
| `close_pos` | `9000` | 完全闭合目标位置 |
|
||||
| `speed` | `50` | 默认运动速度百分比 |
|
||||
| `accel` | `80` | 默认加速度 |
|
||||
| `decel` | `80` | 默认减速度 |
|
||||
| `release_open_speed` | `50` | 检测到释放时的张开速度 |
|
||||
| `release_open_accel` | `120` | 释放张开的加速度 |
|
||||
| `release_open_decel` | `120` | 释放张开的减速度 |
|
||||
|
||||
### 14.4 力度
|
||||
|
||||
| 参数 | 默认值 | 作用 |
|
||||
| --- | ---: | --- |
|
||||
| `open_force` | `20` | 张开动作使用的力比例 |
|
||||
| `close_force` | `20` | 普通闭合动作使用的力比例 |
|
||||
| `grip_start_force` | `20` | 检测到物体后先使用的低力 |
|
||||
| `hold_force` | `20` | 基础保持目标力 |
|
||||
| `initial_force` | `20` | 配置展示字段;当前运行值由 `open_force` 生成 |
|
||||
| `force_min` | `20` | 所有夹爪力命令的下限 |
|
||||
| `force_max` | `80` | 所有夹爪力命令的上限 |
|
||||
|
||||
### 14.5 控制 / 滤波
|
||||
|
||||
| 参数 | 默认值 | 作用 |
|
||||
| --- | ---: | --- |
|
||||
| `control_hz` | `30.0` | 状态机目标控制频率 |
|
||||
| `baseline_seconds` | `1.0` | 启动时空载基准标定时间 |
|
||||
| `filter_alpha` | `0.55` | 法向力指数滤波权重;越大响应越快、平滑越弱 |
|
||||
| `shear_filter_alpha` | `0.65` | 切向力指数滤波权重 |
|
||||
| `dry_run` | `false` | 跳过真实夹爪串口动作,仅记录动作日志 |
|
||||
| `open_at_end` | `true` | 控制循环结束时尝试发送张开命令 |
|
||||
| `trigger_force_update` | `false` | 更新目标力后是否额外触发一次运动命令 |
|
||||
|
||||
### 14.6 触发 / 夹持
|
||||
|
||||
| 参数 | 默认值 | 作用 |
|
||||
| --- | ---: | --- |
|
||||
| `trigger_normal_force` | `0.1` | 相对稳定基准的法向触发阈值 |
|
||||
| `trigger_shear_force` | `0.1` | 相对稳定基准的切向触发阈值 |
|
||||
| `trigger_clear_stable_seconds` | `0.2` | 兼容配置字段;当前重新就绪主要由稳定样本和恢复参数控制 |
|
||||
| `open_wait_stable_samples` | `12` | 等待触发前所需稳定样本数 |
|
||||
| `open_wait_stable_range_n` | `0.03` | 稳定窗口允许的力值范围 |
|
||||
| `open_wait_stable_trend_n` | `0.015` | 稳定窗口允许的首尾趋势变化 |
|
||||
| `grip_normal_force` | `0.2` | 判断触碰到物体的法向力阈值 |
|
||||
| `grip_requires_both` | `true` | 是否要求左右两侧同时接触 |
|
||||
| `close_timeout_seconds` | `5.0` | 闭合寻找物体的最长时间 |
|
||||
| `closing_command_interval` | `0.2` | 闭合过程中重发命令的间隔 |
|
||||
| `force_ramp_step` | `5` | 基础夹持每次增加的力比例 |
|
||||
| `force_ramp_interval` | `0.02` | 基础夹持两次加力间隔 |
|
||||
| `grip_settle_seconds` | `0.05` | 到达基础保持力后的稳定时间 |
|
||||
|
||||
### 14.7 自适应 / 释放
|
||||
|
||||
| 参数 | 默认值 | 作用 |
|
||||
| --- | ---: | --- |
|
||||
| `adaptive_grip_enabled` | `true` | 启用按切向负载自适应加力 |
|
||||
| `adaptive_shear_start_force` | `0.1` | 开始提高目标力的切向力 |
|
||||
| `adaptive_shear_full_force` | `0.6` | 映射到最大目标力的切向力 |
|
||||
| `adaptive_force_min` | `15` | 自适应目标力下限 |
|
||||
| `adaptive_force_max` | `80` | 自适应目标力上限 |
|
||||
| `adaptive_force_step` | `1` | 自适应每次增加的力比例 |
|
||||
| `adaptive_force_interval` | `0.05` | 自适应两次加力间隔 |
|
||||
| `release_arm_delay_seconds` | `0.8` | 停止加力后启用释放检测前的等待时间 |
|
||||
| `release_normal_change` | `0.2` | 相对释放参考值的法向变化阈值 |
|
||||
| `release_shear_change` | `0.2` | 相对释放参考值的切向变化阈值 |
|
||||
|
||||
### 14.8 恢复
|
||||
|
||||
| 参数 | 默认值 | 作用 |
|
||||
| --- | ---: | --- |
|
||||
| `rearm_seconds` | `0.5` | 张开后重新允许触发前的最短休息时间 |
|
||||
| `recover_normal_force` | `0.02` | 力恢复路径允许的最大法向残余力 |
|
||||
| `recover_shear_force` | `0.02` | 力恢复路径允许的最大切向残余力 |
|
||||
| `recover_stable_seconds` | `0.4` | 残余力满足阈值后的稳定时间 |
|
||||
| `open_position_recover_enabled` | `true` | 启用按张开位置判断恢复 |
|
||||
| `open_position_tolerance` | `100` | 实际位置接近 `open_pos` 的允许误差 |
|
||||
| `open_position_stable_seconds` | `0.1` | 张开位置到位后的稳定时间 |
|
||||
| `open_position_check_interval` | `0.1` | 读取张开位置的时间间隔 |
|
||||
| `hold_fallback_pos` | `null` | 读取不到当前位置时使用的保持位置;`null` 表示不指定 |
|
||||
|
||||
### 14.9 标定
|
||||
|
||||
`normal_force_calibration` 和 `shear_force_calibration` 是 JSON 对象,用于把传感器原始量映射到 N。常用字段:
|
||||
|
||||
- `enabled`:是否启用该标定。
|
||||
- `method`:当前使用分段线性映射 `piecewise_linear`。
|
||||
- `extrapolate`:是否允许超出标定点后继续外推。
|
||||
- `clamp_output_min`:输出下限。
|
||||
- `points`:原始值与真实力值的标定点列表。
|
||||
- `signed`:切向分量是否保留方向符号。
|
||||
- `input`:切向标定输入形式,当前为 `components`。
|
||||
|
||||
标定点会直接影响界面力值、触发阈值、接触判断和释放检测。修改前应备份 `gripper.json`,并使用测量数据核对单位和方向。
|
||||
|
||||
## 15. 布局管理
|
||||
|
||||
### 15.1 推荐保存步骤
|
||||
|
||||
1. 调整主窗口到需要的窗口或全屏状态。
|
||||
2. 移动并缩放左右光流窗口。
|
||||
3. 移动并缩放曲线、夹爪控制、传感器控制面板。
|
||||
4. 调整 3D 模型视角。
|
||||
5. 在“设置 -> 画面”调整左右画面方向。
|
||||
6. 在“设置 -> 布局”点击“保存当前布局”。
|
||||
|
||||
浮动面板使用相对于 3D 工作区的比例保存。窗口分辨率变化后,程序按比例恢复并把面板限制在可视区域内。
|
||||
|
||||
### 15.2 恢复默认布局
|
||||
|
||||
关闭程序后,将 `config/gui_layout.json` 备份并删除,再启动 GUI,即可使用程序默认布局。布局文件损坏、版本不匹配或不存在时,程序也会使用默认布局。
|
||||
|
||||
## 16. 日志说明
|
||||
|
||||
### 16.1 打包版日志
|
||||
|
||||
EXE 日志位于程序同级 `logs` 目录:
|
||||
|
||||
```text
|
||||
logs/
|
||||
├─ latest.txt
|
||||
└─ YYYYMMDD_HHMMSS_PID/
|
||||
├─ main.log
|
||||
├─ worker_PID.log
|
||||
└─ worker_PID.log
|
||||
```
|
||||
|
||||
- `latest.txt`:最近一次运行的会话目录名。
|
||||
- `main.log`:GUI、控制器、串口、性能信息和异常堆栈。
|
||||
- `worker_*.log`:左右传感器进程日志,也包含 OpenCV 原生警告。
|
||||
- 程序自动保留最近 10 次运行会话。
|
||||
|
||||
### 16.2 源码运行日志
|
||||
|
||||
源码方式运行时,日志主要输出在启动程序的终端窗口,不自动使用 EXE 的会话日志重定向。
|
||||
|
||||
### 16.3 推荐查看顺序
|
||||
|
||||
1. 打开 `logs/latest.txt`。
|
||||
2. 进入对应会话目录。
|
||||
3. 先看 `main.log` 最后的异常和 `state=error` 原因。
|
||||
4. 相机或光流问题再看两个 `worker_*.log`。
|
||||
5. 将 `main.log` 和 worker 日志一起保留,便于复现启动顺序和设备状态。
|
||||
|
||||
## 17. 常见故障排查
|
||||
|
||||
### 17.1 左右传感器没有画面或 FPS 为 `--`
|
||||
|
||||
检查顺序:
|
||||
|
||||
1. 检查摄像头 USB 连接和系统相机权限。
|
||||
2. 检查 `video1`、`video2` 是否对应实际编号,并确认没有填成同一个相机。
|
||||
3. 关闭其他正在占用摄像头的软件。
|
||||
4. 点击对应传感器的单侧重启图标。
|
||||
5. 仍未恢复时点击顶部“重启控制程序”。
|
||||
6. 查看对应 `worker_*.log`。
|
||||
|
||||
Windows 日志连续出现 `CvCapture_MSMF::grabFrame ... can't grab frame`,通常表示相机已经被打开,但系统未能继续取到视频帧。重点检查相机占用、USB 连接、带宽和设备编号。
|
||||
|
||||
### 17.2 串口连接失败
|
||||
|
||||
检查:
|
||||
|
||||
- `port` 是否为当前实际端口。
|
||||
- 夹爪电源是否开启。
|
||||
- `slave_id`、`baudrate` 是否与设备一致。
|
||||
- CH341 等串口驱动是否安装。
|
||||
- 串口是否被其他软件占用。
|
||||
- Linux 用户是否有 `dialout` 权限。
|
||||
|
||||
在 `main.log` 搜索 `serial`、`Modbus`、`motor is not connected` 和异常堆栈。
|
||||
|
||||
### 17.3 GUI 打开后立即进入 error
|
||||
|
||||
1. 查看顶部 `action` 中的异常摘要。
|
||||
2. 打开最近会话的 `main.log`。
|
||||
3. 检查两个传感器进程是否都成功输出样本。
|
||||
4. 检查串口配置和 Orisys SDK。
|
||||
5. 使用 `--dry-run` 区分夹爪串口问题和传感器问题。
|
||||
|
||||
### 17.4 力值漂移或空载不归零
|
||||
|
||||
1. 移开两侧传感器上的全部负载。
|
||||
2. 点击顶部“重启控制程序”。
|
||||
3. 在 `baseline_seconds` 标定阶段保持设备静止。
|
||||
4. 检查光照、摄像头固定和触觉表面是否发生变化。
|
||||
5. 核对标定 JSON 和传感器配置。
|
||||
|
||||
### 17.5 FPS 较低或 GUI 卡顿
|
||||
|
||||
1. 检查 `config/ddjx01_q6a.json` 中光流算法是否为 `dis`。
|
||||
2. 检查是否错误使用了完整 CUDA/RAFT 打包环境。
|
||||
3. 比较 `sensor_cpu=true` 与 `false` 的实际性能。
|
||||
4. 适当降低 `sensor_fps` 或传感器分辨率。
|
||||
5. 更新显卡驱动并检查 3D OpenGL 是否正常。
|
||||
6. 查看 `main.log` 中 `[perf]` 性能记录和 worker 日志。
|
||||
|
||||
### 17.6 保存参数后没有生效
|
||||
|
||||
1. 确认点击的是“保存到 gripper.json”。
|
||||
2. 确认保存提示显示成功。
|
||||
3. 关闭设置窗口。
|
||||
4. 点击顶部“重启控制程序”。
|
||||
5. 检查设置标题显示的配置文件路径是否是当前程序正在使用的文件。
|
||||
|
||||
### 17.7 布局异常或面板位置不合适
|
||||
|
||||
- 当前窗口内恢复:设置 -> 布局 -> 应用已保存布局。
|
||||
- 恢复默认布局:关闭程序,备份并删除 `config/gui_layout.json`,重新启动。
|
||||
- 更换显示器后,程序会尽量将主窗口和面板限制在可见区域,但不同 DPI 下不保证像素级一致。
|
||||
|
||||
## 18. 快捷键和退出
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
| --- | --- |
|
||||
| `Esc` | 在全屏与普通窗口之间切换 |
|
||||
| `Q` | 退出 GUI |
|
||||
|
||||
正常退出会请求停止控制器,并等待控制线程结束。`open_at_end=true` 时,控制器结束流程会尝试让夹爪回到张开位置;设备断电、串口异常或控制器尚未成功连接时,以现场设备实际状态为准。
|
||||
|
||||
## 19. 现场操作检查表
|
||||
|
||||
### 启动前
|
||||
|
||||
- [ ] 左右摄像头已经识别。
|
||||
- [ ] 夹爪电源和串口已经识别。
|
||||
- [ ] `video1`、`video2`、`port` 配置正确。
|
||||
- [ ] 夹爪开合区域已经清理。
|
||||
- [ ] 两侧传感器表面无预加载。
|
||||
|
||||
### 启动后
|
||||
|
||||
- [ ] 左右传感器有 FPS,状态点正常。
|
||||
- [ ] 3D 模型和夹爪实际位置基本一致。
|
||||
- [ ] 顶部没有 `state=error`。
|
||||
- [ ] 智能模式等待到 `open-wait-armed` 后再触发。
|
||||
- [ ] 首次手动测试使用较低速度和最大力。
|
||||
|
||||
### 参数或布局修改后
|
||||
|
||||
- [ ] 参数已保存到 `gripper.json`。
|
||||
- [ ] 已重启控制程序加载新参数。
|
||||
- [ ] 布局已单独执行“保存当前布局”。
|
||||
- [ ] 最近一次运行日志可以正常生成和定位。
|
||||
|
||||
---
|
||||
|
||||
本版本为快速初版,后续版本可补充带编号的主界面截图、设备接线图、参数调试示例和现场验收记录表。
|
||||
@@ -0,0 +1,920 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Orisys 夹爪产品使用说明书">
|
||||
<title>Orisys 夹爪产品使用说明书</title>
|
||||
<link rel="stylesheet" href="assets/manual.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="manual-shell">
|
||||
|
||||
<nav class="manual-nav" aria-label="目录导航">
|
||||
<div class="manual-nav__brand">
|
||||
<h2>Orisys 夹爪</h2>
|
||||
<p>产品使用说明书<br>文档编号 GD02-UM-001</p>
|
||||
</div>
|
||||
<p class="nav-group">概述</p>
|
||||
<ul>
|
||||
<li><a href="#cover">封面</a></li>
|
||||
<li><a href="#revision">版本履历</a></li>
|
||||
<li><a href="#toc">目录</a></li>
|
||||
<li><a href="#sec-1">1. 引言</a></li>
|
||||
<li><a href="#sec-2">2. 安全须知</a></li>
|
||||
<li><a href="#sec-3">3. 产品说明</a></li>
|
||||
</ul>
|
||||
<p class="nav-group">安装与界面</p>
|
||||
<ul>
|
||||
<li><a href="#sec-4">4. 安装与配置</a></li>
|
||||
<li><a href="#sec-5">5. 启动与停止</a></li>
|
||||
<li><a href="#sec-6">6. 主界面说明</a></li>
|
||||
<li><a href="#sec-7">7. 功能模块</a></li>
|
||||
</ul>
|
||||
<p class="nav-group">操作</p>
|
||||
<ul>
|
||||
<li><a href="#sec-8">8. 智能夹持流程</a></li>
|
||||
<li><a href="#sec-9">9. 手动控制</a></li>
|
||||
<li><a href="#sec-10">10. 参数与设置</a></li>
|
||||
</ul>
|
||||
<p class="nav-group">维护</p>
|
||||
<ul>
|
||||
<li><a href="#sec-11">11. 状态与日志</a></li>
|
||||
<li><a href="#sec-12">12. 故障排除</a></li>
|
||||
<li><a href="#sec-13">13. 维护与检查</a></li>
|
||||
<li><a href="#sec-a">附录</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main class="manual-main">
|
||||
<article class="manual-content">
|
||||
|
||||
<!-- 封面 -->
|
||||
<header class="cover" id="cover">
|
||||
<p class="cover__doc-type">用户手册 · User Manual</p>
|
||||
<h1 class="cover__title">Orisys 夹爪</h1>
|
||||
<p class="cover__subtitle">图形化监控与智能夹持控制软件</p>
|
||||
<table class="cover__meta">
|
||||
<tr><th>文档编号</th><td>GD02-UM-001</td></tr>
|
||||
<tr><th>文档版本</th><td>V1.0</td></tr>
|
||||
<tr><th>发布日期</th><td>2026-07-21</td></tr>
|
||||
<tr><th>适用软件</th><td>Orisys 夹爪 GUI(Release 20260717 及同系列版本)</td></tr>
|
||||
<tr><th>适用平台</th><td>Windows x64 / Linux x86_64</td></tr>
|
||||
<tr><th>文档密级</th><td>随产品交付资料</td></tr>
|
||||
</table>
|
||||
<p class="cover__footer">
|
||||
本文档为 Orisys 夹爪软件之正式使用说明。未经书面许可,不得擅自复制、修改或向第三方传播。<br>
|
||||
软件界面、参数默认值及功能以实际交付版本为准。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- 版本履历 -->
|
||||
<section class="doc-section" id="revision">
|
||||
<div class="section-head">
|
||||
<span class="section-num">DOCUMENT CONTROL</span>
|
||||
<h2>版本履历</h2>
|
||||
</div>
|
||||
<table class="data-table revision-table">
|
||||
<caption>表 0-1 文档修订记录</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>版本</th>
|
||||
<th>日期</th>
|
||||
<th>修订人</th>
|
||||
<th>修订说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>V1.0</td>
|
||||
<td>2026-07-21</td>
|
||||
<td>—</td>
|
||||
<td>首版发布。含完整界面说明、操作流程、参数设置、故障排除及现场检查表。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="data-table">
|
||||
<caption>表 0-2 适用读者</caption>
|
||||
<thead>
|
||||
<tr><th>读者角色</th><th>建议阅读章节</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>现场演示人员</td><td>第 2、5、6、8 章及附录 A</td></tr>
|
||||
<tr><td>设备调试人员</td><td>第 4、7、9、10、11、12 章</td></tr>
|
||||
<tr><td>参数维护人员</td><td>第 10、11、12 章及附录 B</td></tr>
|
||||
<tr><td>技术支持人员</td><td>全文及《GUI 详细技术说明》</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="callout callout--note">
|
||||
<span class="callout__label">说明</span>
|
||||
<p>本手册描述软件操作与配置方法,不替代夹爪、传感器硬件厂商提供的机械安装与安全规范。硬件安装、电气接线须按对应硬件说明书执行。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 目录 -->
|
||||
<nav class="toc" id="toc" aria-label="章节目录">
|
||||
<h2>目 录</h2>
|
||||
<ol>
|
||||
<li><a href="#sec-1">引言</a></li>
|
||||
<li><a href="#sec-2">安全须知</a></li>
|
||||
<li><a href="#sec-3">产品说明</a></li>
|
||||
<li><a href="#sec-4">安装与配置</a></li>
|
||||
<li><a href="#sec-5">启动与停止</a></li>
|
||||
<li><a href="#sec-6">主界面说明</a></li>
|
||||
<li><a href="#sec-7">功能模块操作</a></li>
|
||||
<li><a href="#sec-8">智能夹持标准流程</a></li>
|
||||
<li><a href="#sec-9">手动控制操作</a></li>
|
||||
<li><a href="#sec-10">参数配置与设置</a></li>
|
||||
<li><a href="#sec-11">运行状态与日志</a></li>
|
||||
<li><a href="#sec-12">故障排除</a></li>
|
||||
<li><a href="#sec-13">维护与定期检查</a></li>
|
||||
<li><a href="#sec-a">附录</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- 第1章 -->
|
||||
<section class="doc-section" id="sec-1">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 1</span>
|
||||
<h2>引言</h2>
|
||||
</div>
|
||||
|
||||
<h3>1.1 编写目的</h3>
|
||||
<p>本手册用于指导用户正确安装、配置、操作和维护 Orisys 夹爪软件(以下简称「本软件」)。用户应在使用本软件前通读第 2 章安全须知,并按本手册规定步骤进行操作。</p>
|
||||
|
||||
<h3>1.2 产品简介</h3>
|
||||
<p>Orisys 夹爪是一套集成视觉触觉感知、智能夹持控制与图形化监控的演示系统。系统通过左右两个视觉触觉传感器采集法向力与切向力信息,经 Modbus RTU 串口驱动夹爪执行动作,并在 PyQt5 图形界面中同步显示 3D 模型、光流画面、力曲线及运行状态。</p>
|
||||
<p>本软件主要实现以下功能:</p>
|
||||
<ul>
|
||||
<li>单侧触觉触发后自动闭合、接触检测、分步加力、自适应夹持及取物释放;</li>
|
||||
<li>实时显示夹爪三维姿态、左右传感器光流/幅值图像及六路力值曲线;</li>
|
||||
<li>提供智能模式与手动模式,支持速度、力度、位置等参数调节;</li>
|
||||
<li>支持运行参数在线编辑、工作区布局保存及运行日志记录。</li>
|
||||
</ul>
|
||||
|
||||
<h3>1.3 术语与缩略语</h3>
|
||||
<table class="data-table data-table--compact">
|
||||
<caption>表 1-1 术语定义</caption>
|
||||
<thead>
|
||||
<tr><th>术语</th><th>含义</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>法向力</td><td>垂直于触觉敏感表面的力分量,单位 N</td></tr>
|
||||
<tr><td>切向力</td><td>沿触觉敏感表面平面内的合力,单位 N</td></tr>
|
||||
<tr><td>光流</td><td>基于图像序列估计的表面位移矢量场,用于可视化触觉形变</td></tr>
|
||||
<tr><td>智能模式</td><td>由状态机根据触觉信号自动控制夹爪,用户不可干预运动参数</td></tr>
|
||||
<tr><td>手动模式</td><td>用户通过界面直接下发张开、闭合、保持及位置命令</td></tr>
|
||||
<tr><td>state</td><td>控制状态机当前所处状态(如等待触发、闭合中、夹持检测等)</td></tr>
|
||||
<tr><td>action</td><td>当前控制周期内正在执行的具体动作描述</td></tr>
|
||||
<tr><td>dry-run</td><td>跳过真实串口动作、仅记录控制逻辑的调试模式</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>1.4 引用文件</h3>
|
||||
<table class="data-table data-table--compact">
|
||||
<caption>表 1-2 相关文档</caption>
|
||||
<thead>
|
||||
<tr><th>文档名称</th><th>说明</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>config/gripper.json</code></td><td>主运行配置文件</td></tr>
|
||||
<tr><td><code>config/ddjx01_q6a.json</code></td><td>触觉传感器及光流算法配置</td></tr>
|
||||
<tr><td><code>docs/GUI_USER_MANUAL.zh-CN.md</code></td><td>GUI 详细技术说明(完整参数表及深度故障排查)</td></tr>
|
||||
<tr><td><code>README.md</code></td><td>开发与打包说明</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 第2章 -->
|
||||
<section class="doc-section" id="sec-2">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 2</span>
|
||||
<h2>安全须知</h2>
|
||||
</div>
|
||||
|
||||
<div class="callout callout--danger">
|
||||
<span class="callout__label">警告</span>
|
||||
<p>夹爪为运动部件,调试及演示过程中可能造成挤压伤害。操作前必须确认开合范围内无人员肢体及无关物体。首次运行须使用较低速度与力度。</p>
|
||||
</div>
|
||||
|
||||
<div class="callout callout--caution">
|
||||
<span class="callout__label">注意</span>
|
||||
<p>本软件顶部「停止」按钮仅停止软件控制流程,<strong>不是</strong>独立的硬件急停装置。发生异常时应立即切断夹爪电源或执行硬件急停(如设备具备)。</p>
|
||||
</div>
|
||||
|
||||
<h3>2.1 一般安全要求</h3>
|
||||
<ol>
|
||||
<li>仅经培训人员方可操作本系统;</li>
|
||||
<li>运行前检查夹爪安装牢固、电源及通信线缆连接可靠;</li>
|
||||
<li>传感器标定及系统重启标定阶段,须保持两侧触觉传感器空载且静止;</li>
|
||||
<li>修改力度、速度及触发阈值前,应备份 <code>config/gripper.json</code>;</li>
|
||||
<li>程序异常退出后,以夹爪实际物理状态为准,必要时手动断电复位。</li>
|
||||
</ol>
|
||||
|
||||
<h3>2.2 电气与设备安全</h3>
|
||||
<ul>
|
||||
<li>串口通信期间不得热插拔通信线缆(除非硬件支持);</li>
|
||||
<li>USB 摄像头及串口设备应连接至稳定供电的 USB 端口,避免共用易过载的 USB Hub;</li>
|
||||
<li>Linux 环境下运行用户须具备 <code>video</code>、<code>dialout</code> 设备组权限。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 第3章 -->
|
||||
<section class="doc-section" id="sec-3">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 3</span>
|
||||
<h2>产品说明</h2>
|
||||
</div>
|
||||
|
||||
<h3>3.1 系统组成</h3>
|
||||
<p>本系统由感知层、软件处理层及执行与呈现层构成。左、右触觉传感器采集视频帧并计算触觉力;智能夹持状态机根据力信号经 Modbus RTU 串口驱动夹爪;GUI 同步显示 3D 姿态、光流图像、力曲线及运行状态。</p>
|
||||
<figure class="figure figure--diagram">
|
||||
<img src="assets/diagrams/system-architecture.svg" alt="Orisys 夹爪 系统组成与数据流示意图" width="1104" style="padding:1.25rem 1.5rem 0.75rem;background:#fff;">
|
||||
<figcaption class="figure__caption"><strong>图 3-1</strong> 系统组成与数据流示意图。实线为主要控制/数据流;虚线为位置反馈。</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>3.2 配置清单</h3>
|
||||
<table class="data-table">
|
||||
<caption>表 3-1 硬件配置参考清单</caption>
|
||||
<thead>
|
||||
<tr><th>序号</th><th>名称</th><th>数量</th><th>备注</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>1</td><td>智能夹爪及驱动控制器</td><td>1 套</td><td>Modbus RTU,USB 转串口连接</td></tr>
|
||||
<tr><td>2</td><td>视觉触觉传感器组件</td><td>2 套</td><td>左、右各一,含独立 USB 摄像头</td></tr>
|
||||
<tr><td>3</td><td>通信及电源线缆</td><td>若干</td><td>按现场安装要求配置</td></tr>
|
||||
<tr><td>4</td><td>运行计算机</td><td>1 台</td><td>算力版</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>3.3 软件目录结构</h3>
|
||||
<p>发布包解压后须保持完整目录结构。禁止仅复制可执行文件至其他路径单独运行。</p>
|
||||
<pre class="code-block">GripperDemo02/
|
||||
├── GripperDemo02.exe # 主程序(Linux 无 .exe 后缀)
|
||||
├── _internal/ # 运行时依赖库(禁止删除)
|
||||
├── config/
|
||||
│ ├── gripper.json # 主配置文件
|
||||
│ ├── ddjx01_q6a.json # 传感器配置文件
|
||||
│ ├── compound_eye.png # 传感器资源
|
||||
│ └── gui_layout.json # 界面布局(保存后生成)
|
||||
├── sdk/ # 夹爪通信 SDK
|
||||
├── crt_ctag2f90d_gripper_visualization/ # 3D 模型资源
|
||||
└── logs/ # 运行日志(首次运行后自动生成)</pre>
|
||||
</section>
|
||||
|
||||
<!-- 第4章 -->
|
||||
<section class="doc-section" id="sec-4">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 4</span>
|
||||
<h2>安装与配置</h2>
|
||||
</div>
|
||||
|
||||
<h3>4.1 安装前检查</h3>
|
||||
<table class="data-table">
|
||||
<caption>表 4-1 安装前检查项目</caption>
|
||||
<thead>
|
||||
<tr><th>检查项</th><th>检查方法</th><th>合格标准</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>夹爪机械安装</td><td>目视及手动检查</td><td>安装牢固,开合路径无干涉</td></tr>
|
||||
<tr><td>夹爪电源</td><td>上电指示</td><td>控制器正常上电</td></tr>
|
||||
<tr><td>串口识别</td><td>设备管理器 / <code>dmesg</code></td><td>端口已识别(如 COM5、/dev/ttyUSB0)</td></tr>
|
||||
<tr><td>摄像头识别</td><td>系统相机应用或设备列表</td><td>两路摄像头均可独立打开</td></tr>
|
||||
<tr><td>工作区域</td><td>目视</td><td>夹爪开合范围内无杂物</td></tr>
|
||||
<tr><td>传感器状态</td><td>目视</td><td>触觉敏感面无预加载、无遮挡</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>4.2 基本配置</h3>
|
||||
<p>使用文本编辑器打开 <code>config/gripper.json</code>,至少须正确配置以下参数后方可运行。</p>
|
||||
<table class="data-table">
|
||||
<caption>表 4-2 必配参数</caption>
|
||||
<thead>
|
||||
<tr><th>参数名</th><th>说明</th><th>Windows 示例</th><th>Linux 示例</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>video1</code></td><td>左摄像头设备编号或路径</td><td><code>"0"</code></td><td><code>"0"</code> 或 <code>"/dev/video0"</code></td></tr>
|
||||
<tr><td><code>video2</code></td><td>右摄像头设备编号或路径</td><td><code>"1"</code></td><td><code>"1"</code> 或 <code>"/dev/video1"</code></td></tr>
|
||||
<tr><td><code>port</code></td><td>夹爪串口</td><td><code>"COM5"</code></td><td><code>"/dev/ttyUSB0"</code></td></tr>
|
||||
<tr><td><code>baudrate</code></td><td>串口波特率</td><td colspan="2"><code>115200</code>(须与设备一致)</td></tr>
|
||||
<tr><td><code>slave_id</code></td><td>Modbus 从站地址</td><td colspan="2"><code>1</code>(须与设备一致)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="callout callout--note">
|
||||
<span class="callout__label">说明</span>
|
||||
<p><code>video1</code> 与 <code>video2</code> 必须对应不同的物理摄像头。配置错误将导致单侧无数据或数据重复。</p>
|
||||
</div>
|
||||
|
||||
<h3>4.3 系统要求</h3>
|
||||
<p>本系统默认随<strong>算力版</strong>主机一并交付,出厂已完成操作系统、驱动及软件环境配置,用户开箱连接夹爪与传感器即可使用。若用户自备运行计算机,须满足表 4-3 所列环境要求,并自行完成摄像头、串口及依赖库配置。</p>
|
||||
<table class="data-table data-table--compact">
|
||||
<caption>表 4-3 自备计算机运行环境(可选)</caption>
|
||||
<thead>
|
||||
<tr><th>项目</th><th>要求</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>部署方式</td><td>算力版(默认交付)或用户自备计算机</td></tr>
|
||||
<tr><td>操作系统</td><td>Windows 10/11 x64,或 Ubuntu 20.04+ x86_64</td></tr>
|
||||
<tr><td>显示</td><td>分辨率 ≥ 1920×1080,支持 OpenGL 3.0+</td></tr>
|
||||
<tr><td>内存</td><td>≥ 8 GB</td></tr>
|
||||
<tr><td>USB</td><td>独立端口连接两路摄像头及串口设备</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 第5章 -->
|
||||
<section class="doc-section" id="sec-5">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 5</span>
|
||||
<h2>启动与停止</h2>
|
||||
</div>
|
||||
|
||||
<h3>5.1 正常启动程序</h3>
|
||||
<ol>
|
||||
<li>确认第 4 章检查项目均合格;</li>
|
||||
<li>双击运行 <code>GripperDemo02.exe</code>(Linux:<code>./GripperDemo02</code>);</li>
|
||||
<li>程序启动后自动创建控制线程,夹爪执行张开动作;</li>
|
||||
<li>系统在 <code>baseline_seconds</code>(默认 1.0 s)内完成传感器空载基准标定,此期间保持两侧传感器无接触;</li>
|
||||
<li>待顶部状态栏显示 <code>state=open_wait(等待触发)</code>,且 <code>action=open-wait-armed(已就绪)</code>;</li>
|
||||
<li>确认左右传感器状态指示为绿色,FPS 显示正常数值(非 <code>--</code>)。</li>
|
||||
</ol>
|
||||
|
||||
<div class="callout callout--note">
|
||||
<span class="callout__label">说明</span>
|
||||
<p>正常启动后无需再次点击顶部「启动」图标。仅当控制程序处于空闲(已停止)状态时,方使用该按钮重新建立设备连接。</p>
|
||||
</div>
|
||||
|
||||
<h3>5.2 正常停止与退出</h3>
|
||||
<table class="data-table">
|
||||
<caption>表 5-1 停止方式说明</caption>
|
||||
<thead>
|
||||
<tr><th>操作</th><th>作用范围</th><th>说明</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>顶部「停止」</td><td>控制线程</td><td>停止状态机及传感器采集,释放串口与摄像头资源</td></tr>
|
||||
<tr><td>顶部「关闭」或快捷键 Q</td><td>整个程序</td><td>先请求停止控制线程,等待结束后关闭窗口</td></tr>
|
||||
<tr><td>顶部「重启」</td><td>控制线程</td><td>等效于先停止再启动,重新标定传感器基准</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>当配置项 <code>open_at_end=true</code>(默认)时,控制流程结束时会尝试发送夹爪张开命令。若设备已断电或通信异常,以夹爪实际状态为准。</p>
|
||||
|
||||
<h3>5.3 源码方式运行(调试人员)</h3>
|
||||
<pre class="code-block"># Windows
|
||||
conda activate py311
|
||||
python -m gripper.visualizer --windowed
|
||||
|
||||
# 跳过真实串口(传感器与 GUI 仍运行)
|
||||
python -m gripper.visualizer --windowed --dry-run</pre>
|
||||
</section>
|
||||
|
||||
<!-- 第6章 -->
|
||||
<section class="doc-section" id="sec-6">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 6</span>
|
||||
<h2>主界面说明</h2>
|
||||
</div>
|
||||
|
||||
<p>本软件主界面由顶部状态控制栏、中央 3D 工作区及若干浮动功能面板组成。浮动面板可拖动、调整大小;布局可通过设置模块保存并在下次启动时恢复。</p>
|
||||
|
||||
<figure class="figure">
|
||||
<div class="figure-annotate figure--overview">
|
||||
<img src="images/manual/01-main-overview.png" alt="Orisys 夹爪 主界面" width="1600" height="960">
|
||||
<div class="anno-box anno-box--a"></div><span class="anno-tag anno-tag--a">A</span>
|
||||
<div class="anno-box anno-box--b"></div><span class="anno-tag anno-tag--b">B</span>
|
||||
<div class="anno-box anno-box--c"></div><span class="anno-tag anno-tag--c">C</span><span class="anno-label anno-label--c">左光流</span>
|
||||
<div class="anno-box anno-box--c-right"></div><span class="anno-tag anno-tag--c-right">C</span><span class="anno-label anno-label--c-right">右光流</span>
|
||||
<div class="anno-box anno-box--d"></div><span class="anno-tag anno-tag--d">D</span>
|
||||
<div class="anno-box anno-box--e"></div><span class="anno-tag anno-tag--e">E</span>
|
||||
<div class="anno-box anno-box--f"></div><span class="anno-tag anno-tag--f">F</span>
|
||||
</div>
|
||||
<figcaption class="figure__caption"><strong>图 6-1</strong> 主界面总览。彩色框标注各功能区域,对应表 6-1 编号。</figcaption>
|
||||
<ul class="figure__legend">
|
||||
<li><span class="legend-tag legend-tag--a">A</span> 顶部状态与控制栏</li>
|
||||
<li><span class="legend-tag legend-tag--b">B</span> 夹爪 3D 模型工作区</li>
|
||||
<li><span class="legend-tag legend-tag--c">C</span> 左/右光流窗口</li>
|
||||
<li><span class="legend-tag legend-tag--d">D</span> 六路力曲线图</li>
|
||||
<li><span class="legend-tag legend-tag--e">E</span> 夹爪控制区域</li>
|
||||
<li><span class="legend-tag legend-tag--f">F</span> 传感器控制区域</li>
|
||||
</ul>
|
||||
</figure>
|
||||
|
||||
<table class="data-table">
|
||||
<caption>表 6-1 主界面区域功能说明</caption>
|
||||
<thead>
|
||||
<tr><th>区域编号</th><th>名称</th><th>功能说明</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>A</td><td>顶部状态与控制栏</td><td>显示 <code>state</code>、<code>action</code>;提供设置、启停、重启及窗口管理</td></tr>
|
||||
<tr><td>B</td><td>夹爪 3D 模型工作区</td><td>显示夹爪实时开合姿态、当前状态及力/位置摘要</td></tr>
|
||||
<tr><td>C</td><td>左/右光流窗口</td><td>分别显示左、右传感器光流或幅值图像</td></tr>
|
||||
<tr><td>D</td><td>曲线图区域</td><td>六路力值(法向力、切向力 X/Y)实时趋势</td></tr>
|
||||
<tr><td>E</td><td>夹爪控制区域</td><td>智能/手动模式切换;速度、力度、位置及动作命令</td></tr>
|
||||
<tr><td>F</td><td>传感器控制区域</td><td>传感器在线状态、FPS、力值读数及单侧重启</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<figure class="figure figure--borderless">
|
||||
<div class="figure-annotate figure--topbar">
|
||||
<img src="images/manual/07-top-bar.png" alt="顶部状态与控制栏" width="1200" height="80">
|
||||
<div class="anno-box anno-box--a"></div><span class="anno-tag anno-tag--a">1</span><span class="anno-label anno-label--a">Logo</span>
|
||||
<div class="anno-box anno-box--b"></div><span class="anno-tag anno-tag--b">2</span><span class="anno-label anno-label--b">state / action</span>
|
||||
<div class="anno-box anno-box--c"></div><span class="anno-tag anno-tag--c">3</span><span class="anno-label anno-label--c">设置</span>
|
||||
<div class="anno-box anno-box--d"></div><span class="anno-tag anno-tag--d">4</span>
|
||||
<div class="anno-box anno-box--e"></div><span class="anno-tag anno-tag--e">5</span>
|
||||
<div class="anno-box anno-box--f"></div><span class="anno-tag anno-tag--f">6</span>
|
||||
<div class="anno-box anno-box--g"></div><span class="anno-tag anno-tag--g">7</span>
|
||||
</div>
|
||||
<figcaption class="figure__caption"><strong>图 6-2</strong> 顶部状态与控制栏局部放大标注。</figcaption>
|
||||
<ul class="figure__legend">
|
||||
<li><span class="legend-tag legend-tag--a">1</span> 品牌 Logo</li>
|
||||
<li><span class="legend-tag legend-tag--b">2</span> 运行状态(<code>state</code> / <code>action</code>)</li>
|
||||
<li><span class="legend-tag legend-tag--c">3</span> 设置</li>
|
||||
<li><span class="legend-tag legend-tag--d">4</span> 启动控制</li>
|
||||
<li><span class="legend-tag legend-tag--e">5</span> 停止控制</li>
|
||||
<li><span class="legend-tag legend-tag--f">6</span> 重启控制程序</li>
|
||||
<li><span class="legend-tag legend-tag--g">7</span> 最小化 / 全屏 / 关闭</li>
|
||||
</ul>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<!-- 第7章 -->
|
||||
<section class="doc-section" id="sec-7">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 7</span>
|
||||
<h2>功能模块操作</h2>
|
||||
</div>
|
||||
|
||||
<h3>7.1 夹爪 3D 模型</h3>
|
||||
<p>3D 模型区域根据实时夹爪位置在预缓存的开合档位间切换,显示当前机械姿态。区域下方叠加显示左/右法向力、夹爪位置、当前力百分比及自适应目标力百分比。</p>
|
||||
<table class="data-table data-table--compact">
|
||||
<caption>表 7-1 3D 视图操作</caption>
|
||||
<thead>
|
||||
<tr><th>输入</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>鼠标左键拖动</td><td>旋转观察视角</td></tr>
|
||||
<tr><td>鼠标中键拖动</td><td>平移观察中心</td></tr>
|
||||
<tr><td>鼠标滚轮</td><td>缩放</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<figure class="figure">
|
||||
<div class="figure-annotate figure--hold">
|
||||
<img src="images/manual/12-hold-check-state.png" alt="夹持检测状态界面" width="1600" height="960">
|
||||
<div class="anno-box anno-box--a"></div><span class="anno-tag anno-tag--a">1</span><span class="anno-label anno-label--a">state=hold_check</span>
|
||||
<div class="anno-box anno-box--b"></div><span class="anno-tag anno-tag--b">2</span><span class="anno-label anno-label--b">3D 开合姿态</span>
|
||||
<div class="anno-box anno-box--c"></div><span class="anno-tag anno-tag--c">3</span><span class="anno-label anno-label--c">力/位置摘要</span>
|
||||
</div>
|
||||
<figcaption class="figure__caption"><strong>图 7-1</strong> 夹持检测(<code>hold_check</code>)状态示例:标注顶部状态行、3D 模型区及底部实时摘要。</figcaption>
|
||||
<ul class="figure__legend">
|
||||
<li><span class="legend-tag legend-tag--a">1</span> 状态栏显示 <code>hold_check</code> / 当前动作</li>
|
||||
<li><span class="legend-tag legend-tag--b">2</span> 3D 模型反映当前开合档位</li>
|
||||
<li><span class="legend-tag legend-tag--c">3</span> 左/右法向力、位置、力比例摘要</li>
|
||||
</ul>
|
||||
</figure>
|
||||
|
||||
<h3>7.2 光流窗口</h3>
|
||||
<p>左、右光流窗口为独立浮动面板,分别对应两路触觉传感器。用户可通过右上角模式按钮在「光流」与「幅值」显示模式间切换。</p>
|
||||
<figure class="figure">
|
||||
<div class="figure-annotate figure--flow">
|
||||
<img src="images/manual/02-flow-windows.png" alt="左右光流窗口" width="1600" height="960">
|
||||
<div class="anno-box anno-box--a"></div><span class="anno-tag anno-tag--a">1</span><span class="anno-label anno-label--a">左侧传感器</span>
|
||||
<div class="anno-box anno-box--b"></div><span class="anno-tag anno-tag--b">2</span><span class="anno-label anno-label--b">右侧传感器</span>
|
||||
<div class="anno-box anno-box--c"></div><span class="anno-tag anno-tag--c">3</span><span class="anno-label anno-label--c">光流/幅值切换</span>
|
||||
</div>
|
||||
<figcaption class="figure__caption"><strong>图 7-2</strong> 光流窗口特写。标注左右面板及模式切换按钮位置。</figcaption>
|
||||
<ul class="figure__legend">
|
||||
<li><span class="legend-tag legend-tag--a">1</span> 左传感器光流/幅值画面</li>
|
||||
<li><span class="legend-tag legend-tag--b">2</span> 右传感器光流/幅值画面</li>
|
||||
<li><span class="legend-tag legend-tag--c">3</span> 「光流 / 幅值」模式切换按钮</li>
|
||||
</ul>
|
||||
</figure>
|
||||
<table class="data-table data-table--compact">
|
||||
<caption>表 7-2 光流窗口操作</caption>
|
||||
<thead>
|
||||
<tr><th>操作</th><th>方法</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>切换显示模式</td><td>点击窗口右上角「光流 / 幅值」按钮</td></tr>
|
||||
<tr><td>移动窗口</td><td>拖动标题栏或图像区域</td></tr>
|
||||
<tr><td>调整大小</td><td>拖动窗口右下角约 18 px 区域</td></tr>
|
||||
<tr><td>旋转 / 镜像</td><td>设置 → 画面 → 对应侧按钮(见 10.1 节)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>7.3 力曲线图</h3>
|
||||
<p>曲线区域以 3 行 × 2 列布局显示六路力值,横轴为时间(秒),纵轴为力(牛顿)。默认显示最近约 6 s 数据;显示值经平滑处理,绝对值低于 0.05 N 时归零显示。</p>
|
||||
<figure class="figure">
|
||||
<div class="figure-annotate figure--plots">
|
||||
<img src="images/manual/03-force-plots.png" alt="六路力曲线图" width="1600" height="960">
|
||||
<div class="anno-box anno-box--a"></div><span class="anno-tag anno-tag--a">D</span><span class="anno-label anno-label--a">曲线图区域</span>
|
||||
<div class="anno-box anno-box--b"></div><span class="anno-tag anno-tag--b">1</span><span class="anno-label anno-label--b">法向力</span>
|
||||
<div class="anno-box anno-box--c"></div><span class="anno-tag anno-tag--c">2</span>
|
||||
<div class="anno-box anno-box--d"></div><span class="anno-tag anno-tag--d">3</span>
|
||||
</div>
|
||||
<figcaption class="figure__caption"><strong>图 7-3</strong> 力曲线区域特写。三行分别对应法向力、切向力 X、切向力 Y。</figcaption>
|
||||
<ul class="figure__legend">
|
||||
<li><span class="legend-tag legend-tag--a">D</span> 曲线图浮动面板整体</li>
|
||||
<li><span class="legend-tag legend-tag--b">1</span> 第 1 行:左/右法向力</li>
|
||||
<li><span class="legend-tag legend-tag--c">2</span> 第 2 行:左/右切向力 X</li>
|
||||
<li><span class="legend-tag legend-tag--d">3</span> 第 3 行:左/右切向力 Y</li>
|
||||
</ul>
|
||||
</figure>
|
||||
|
||||
<h3>7.4 传感器控制区域</h3>
|
||||
<figure class="figure">
|
||||
<div class="figure-annotate figure--sensor">
|
||||
<img src="images/manual/06-sensor-control.png" alt="传感器控制区域" width="1600" height="960">
|
||||
<div class="anno-box anno-box--a"></div><span class="anno-tag anno-tag--a">F</span>
|
||||
<div class="anno-box anno-box--b"></div><span class="anno-tag anno-tag--b">1</span><span class="anno-label anno-label--b">在线状态</span>
|
||||
<div class="anno-box anno-box--c"></div><span class="anno-tag anno-tag--c">2</span>
|
||||
<div class="anno-box anno-box--d"></div><span class="anno-tag anno-tag--d">3</span><span class="anno-label anno-label--d">力值 / FPS</span>
|
||||
</div>
|
||||
<figcaption class="figure__caption"><strong>图 7-4</strong> 传感器控制区域特写。标注在线指示、FPS 及力值读数位置。</figcaption>
|
||||
<ul class="figure__legend">
|
||||
<li><span class="legend-tag legend-tag--a">F</span> 传感器控制浮动面板</li>
|
||||
<li><span class="legend-tag legend-tag--b">1</span> 绿/红状态点(是否收到样本)</li>
|
||||
<li><span class="legend-tag legend-tag--c">2</span> 左/右 FPS 显示</li>
|
||||
<li><span class="legend-tag legend-tag--d">3</span> 法向/切向力读数及重启按钮</li>
|
||||
</ul>
|
||||
</figure>
|
||||
<table class="data-table data-table--compact">
|
||||
<caption>表 7-3 传感器区域显示项</caption>
|
||||
<thead>
|
||||
<tr><th>显示项</th><th>说明</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>左/右法向力、切向力</td><td>经标定后的实时力值,单位 N</td></tr>
|
||||
<tr><td>夹爪当前力比例 / 目标力比例</td><td>控制器输出力百分比及自适应目标力百分比</td></tr>
|
||||
<tr><td>速度</td><td>当前控制器速度百分比</td></tr>
|
||||
<tr><td>重启图标</td><td>重启单侧传感器采集进程(不触发全程序基准重标定)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="callout callout--caution">
|
||||
<span class="callout__label">注意</span>
|
||||
<p>单侧重启仅恢复该侧相机采集。若力值零点出现明显漂移,应执行顶部「重启控制程序」,并在标定阶段保持传感器空载。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 第8章 -->
|
||||
<section class="doc-section" id="sec-8">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 8</span>
|
||||
<h2>智能夹持标准流程</h2>
|
||||
</div>
|
||||
|
||||
<p>智能模式为本软件默认工作模式。该模式下,夹爪动作由触觉信号驱动的状态机自动控制,用户不应在演示过程中切换至手动模式或干预运动参数。</p>
|
||||
|
||||
<figure class="figure figure--diagram">
|
||||
<img src="assets/diagrams/grip-state-flow.svg" alt="智能夹持状态流转图" width="1260" style="padding:1.25rem 1.5rem 0.75rem;background:#fff;">
|
||||
<figcaption class="figure__caption"><strong>图 8-1</strong> 智能夹持状态流转图。蓝色回环:恢复后重新就绪;红色虚线:闭合超时路径。</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>8.1 标准演示步骤</h3>
|
||||
<table class="data-table">
|
||||
<caption>表 8-1 现场演示操作步骤</caption>
|
||||
<thead>
|
||||
<tr><th>步骤</th><th>操作</th><th>预期现象</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>1</td><td>按 5.1 节完成启动,等待就绪</td><td><code>action=open-wait-armed(已就绪)</code></td></tr>
|
||||
<tr><td>2</td><td>用物体从任意一侧轻扫触觉传感器</td><td>力值上升;<code>action=side-trigger-close</code></td></tr>
|
||||
<tr><td>3</td><td>夹爪开始闭合,将物体移至钳口中央</td><td><code>state=closing</code> → <code>gripping</code></td></tr>
|
||||
<tr><td>4</td><td>系统检测接触后低力保持,再分步加力</td><td>3D 模型闭合;力比例逐步上升</td></tr>
|
||||
<tr><td>5</td><td>自适应补力完成后进入释放检测</td><td><code>state=hold_check</code></td></tr>
|
||||
<tr><td>6</td><td>取走或移动物体</td><td>法向/切向力变化超阈;夹爪张开</td></tr>
|
||||
<tr><td>7</td><td>等待恢复完成</td><td>返回 <code>open_wait</code>,可进行下一次演示</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>8.2 运行约束</h3>
|
||||
<ul>
|
||||
<li>默认配置要求左右两侧均达到接触阈值(<code>grip_requires_both=true</code>)方判定夹持成功;</li>
|
||||
<li>闭合超时(默认 5 s,参数 <code>close_timeout_seconds</code>)未检测到物体时,系统自动张开;</li>
|
||||
<li>释放后须满足位置/力恢复条件并经过最短休息时间(<code>rearm_seconds</code>,默认 0.5 s),方可再次触发;</li>
|
||||
<li>休息期间即便传感器检测到扫过,也不会触发闭合。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 第9章 -->
|
||||
<section class="doc-section" id="sec-9">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 9</span>
|
||||
<h2>手动控制操作</h2>
|
||||
</div>
|
||||
|
||||
<p>控制程序处于运行状态时,用户可点击「手动模式」切换至手动控制。手动模式下,用户可直接设定速度、最大力及目标位置,并执行张开、闭合、保持等命令。</p>
|
||||
|
||||
<figure class="figure">
|
||||
<div class="figure-annotate figure--grip-smart">
|
||||
<img src="images/manual/04-gripper-control-smart.png" alt="智能模式夹爪控制面板" width="1600" height="960">
|
||||
<div class="anno-box anno-box--a"></div><span class="anno-tag anno-tag--a">E</span>
|
||||
<div class="anno-box anno-box--b"></div><span class="anno-tag anno-tag--b">1</span><span class="anno-label anno-label--b">智能模式(锁定)</span>
|
||||
<div class="anno-box anno-box--c"></div><span class="anno-tag anno-tag--c">2</span><span class="anno-label anno-label--c">滑条禁用</span>
|
||||
<div class="anno-box anno-box--d"></div><span class="anno-tag anno-tag--d">3</span>
|
||||
</div>
|
||||
<figcaption class="figure__caption"><strong>图 9-1</strong> 智能模式:模式切换、滑条与动作按钮均为灰色锁定状态。</figcaption>
|
||||
<ul class="figure__legend">
|
||||
<li><span class="legend-tag legend-tag--a">E</span> 夹爪控制面板</li>
|
||||
<li><span class="legend-tag legend-tag--b">1</span> 「智能模式」高亮,「手动模式」不可切换干预</li>
|
||||
<li><span class="legend-tag legend-tag--c">2</span> 速度 / 最大力 / 位置滑条禁用</li>
|
||||
<li><span class="legend-tag legend-tag--d">3</span> 张开 / 闭合 / 保持按钮禁用</li>
|
||||
</ul>
|
||||
</figure>
|
||||
|
||||
<figure class="figure">
|
||||
<div class="figure-annotate figure--grip-manual">
|
||||
<img src="images/manual/05-gripper-control-manual.png" alt="手动模式夹爪控制面板" width="1600" height="960">
|
||||
<div class="anno-box anno-box--a"></div><span class="anno-tag anno-tag--a">E</span>
|
||||
<div class="anno-box anno-box--b"></div><span class="anno-tag anno-tag--b">1</span><span class="anno-label anno-label--b">手动模式</span>
|
||||
<div class="anno-box anno-box--c"></div><span class="anno-tag anno-tag--c">2</span><span class="anno-label anno-label--c">动作按钮可用</span>
|
||||
</div>
|
||||
<figcaption class="figure__caption"><strong>图 9-2</strong> 手动模式:滑条与动作按钮均已启用,可直接下发命令。</figcaption>
|
||||
<ul class="figure__legend">
|
||||
<li><span class="legend-tag legend-tag--a">E</span> 夹爪控制面板</li>
|
||||
<li><span class="legend-tag legend-tag--b">1</span> 「手动模式」激活,可调整速度/力度/位置</li>
|
||||
<li><span class="legend-tag legend-tag--c">2</span> 保持、张开、闭合按钮可点击</li>
|
||||
</ul>
|
||||
</figure>
|
||||
|
||||
<table class="data-table">
|
||||
<caption>表 9-1 手动控制参数</caption>
|
||||
<thead>
|
||||
<tr><th>参数</th><th>范围</th><th>步进</th><th>生效方式</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>速度</td><td>0~100%</td><td>5%</td><td>调整后立即更新控制器速度</td></tr>
|
||||
<tr><td>最大力</td><td>0~100%</td><td>5%</td><td>随下一次动作命令发送;受 <code>force_min</code>、<code>force_max</code> 约束</td></tr>
|
||||
<tr><td>夹爪位置</td><td><code>open_pos</code>~<code>close_pos</code></td><td>100</td><td>释放滑条或点击步进按钮后发送位置命令</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="data-table">
|
||||
<caption>表 9-2 手动动作按钮</caption>
|
||||
<thead>
|
||||
<tr><th>按钮</th><th>功能</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>夹爪张开</td><td>以当前速度、最大力运动至 <code>open_pos</code></td></tr>
|
||||
<tr><td>夹爪闭合</td><td>以当前速度、最大力运动至 <code>close_pos</code></td></tr>
|
||||
<tr><td>保持</td><td>读取当前位置并以当前最大力保持;按钮文字变为「释放」</td></tr>
|
||||
<tr><td>释放</td><td>取消手动保持状态,返回手动待机;<strong>不</strong>自动执行张开动作</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 第10章 -->
|
||||
<section class="doc-section" id="sec-10">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 10</span>
|
||||
<h2>参数配置与设置</h2>
|
||||
</div>
|
||||
|
||||
<p>点击顶部设置图标打开设置窗口。设置窗口分为「画面」「布局」及若干参数分组页,参数分组内容直接映射 <code>config/gripper.json</code> 文件字段。</p>
|
||||
|
||||
<figure class="figure">
|
||||
<img src="images/manual/08-settings-visual.png" alt="设置窗口-画面页" width="980" height="680">
|
||||
<figcaption class="figure__caption"><strong>图 10-1</strong> 设置 — 画面页。可分别对左、右光流画面执行旋转(90° 步进)、水平镜像、垂直镜像及重置。</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="figure">
|
||||
<img src="images/manual/09-settings-layout.png" alt="设置窗口-布局页" width="980" height="680">
|
||||
<figcaption class="figure__caption"><strong>图 10-2</strong> 设置 — 布局页。可将当前窗口尺寸、浮动面板位置、3D 视角及画面方向保存至 <code>config/gui_layout.json</code>。</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="figure">
|
||||
<img src="images/manual/11-settings-serial.png" alt="设置窗口-串口参数" width="980" height="680">
|
||||
<figcaption class="figure__caption"><strong>图 10-3</strong> 设置 — 串口 / 设备页:配置 <code>port</code>、<code>baudrate</code>、<code>slave_id</code> 等通信参数。</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="figure">
|
||||
<img src="images/manual/10-settings-trigger.png" alt="设置窗口-触发夹持参数" width="980" height="680">
|
||||
<figcaption class="figure__caption"><strong>图 10-4</strong> 设置 — 触发 / 夹持页:触发阈值、接触判定、闭合超时及分步加力参数。</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>10.1 参数保存程序</h3>
|
||||
<p>在设置窗口中修改参数后,须按以下顺序操作,否则运行中的控制程序不会加载新值:</p>
|
||||
<ol>
|
||||
<li>点击「保存到 gripper.json」,确认保存成功提示;</li>
|
||||
<li>关闭设置窗口;</li>
|
||||
<li>点击顶部「重启控制程序」。</li>
|
||||
</ol>
|
||||
|
||||
<div class="callout callout--caution">
|
||||
<span class="callout__label">注意</span>
|
||||
<p>「保存当前布局」与「保存到 gripper.json」为相互独立操作。布局文件 <code>gui_layout.json</code> 不包含运行参数;参数文件不包含曲线历史及当前运行状态。</p>
|
||||
</div>
|
||||
|
||||
<h3>10.2 常用参数速查</h3>
|
||||
<table class="data-table">
|
||||
<caption>表 10-1 常用运行参数</caption>
|
||||
<thead>
|
||||
<tr><th>参数</th><th>默认值</th><th>说明</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>trigger_normal_force</code></td><td>0.1 N</td><td>单侧法向力触发阈值</td></tr>
|
||||
<tr><td><code>trigger_shear_force</code></td><td>0.1 N</td><td>单侧切向力触发阈值</td></tr>
|
||||
<tr><td><code>grip_normal_force</code></td><td>0.2 N</td><td>判定接触的法向力阈值</td></tr>
|
||||
<tr><td><code>grip_requires_both</code></td><td>true</td><td>是否要求双侧同时接触</td></tr>
|
||||
<tr><td><code>hold_force</code></td><td>20%</td><td>基础保持目标力百分比</td></tr>
|
||||
<tr><td><code>adaptive_force_max</code></td><td>80%</td><td>自适应夹持力上限</td></tr>
|
||||
<tr><td><code>release_normal_change</code></td><td>0.2 N</td><td>释放检测法向力变化阈值</td></tr>
|
||||
<tr><td><code>rearm_seconds</code></td><td>0.5 s</td><td>张开后再次允许触发的最短休息时间</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>完整参数分组及默认值见《GUI 详细技术说明》(<code>docs/GUI_USER_MANUAL.zh-CN.md</code>)第 14 章。</p>
|
||||
</section>
|
||||
|
||||
<!-- 第11章 -->
|
||||
<section class="doc-section" id="sec-11">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 11</span>
|
||||
<h2>运行状态与日志</h2>
|
||||
</div>
|
||||
|
||||
<h3>11.1 状态码说明</h3>
|
||||
<table class="data-table">
|
||||
<caption>表 11-1 主要 state 状态</caption>
|
||||
<thead>
|
||||
<tr><th>state</th><th>界面显示</th><th>含义</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>open_wait</code></td><td>等待触发</td><td>夹爪张开,等待触觉触发</td></tr>
|
||||
<tr><td><code>closing</code></td><td>闭合中</td><td>已触发,正在闭合并寻找物体</td></tr>
|
||||
<tr><td><code>gripping</code></td><td>夹持加力</td><td>已检测到物体,正在增加夹持力</td></tr>
|
||||
<tr><td><code>hold_check</code></td><td>夹持检测</td><td>自适应补力、保持及释放检测</td></tr>
|
||||
<tr><td><code>open_recover</code></td><td>张开恢复</td><td>已张开,等待位置或力信号恢复</td></tr>
|
||||
<tr><td><code>error</code></td><td>错误</td><td>控制线程异常,须查看日志</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>11.2 日志文件</h3>
|
||||
<p>打包版程序运行日志保存在程序目录下 <code>logs/</code> 文件夹。程序自动保留最近 10 次运行会话。</p>
|
||||
<pre class="code-block">logs/
|
||||
├── latest.txt # 最近一次会话目录名
|
||||
└── YYYYMMDD_HHMMSS_PID/
|
||||
├── main.log # GUI、控制器、串口及异常信息
|
||||
└── worker_*.log # 左、右传感器进程日志</pre>
|
||||
<p>建议故障排查顺序:读取 <code>latest.txt</code> → 打开对应会话目录 → 检查 <code>main.log</code> 末尾异常 → 必要时对照 <code>worker_*.log</code>。</p>
|
||||
|
||||
<h3>11.3 快捷键</h3>
|
||||
<table class="data-table data-table--compact">
|
||||
<caption>表 11-2 键盘快捷键</caption>
|
||||
<thead>
|
||||
<tr><th>快捷键</th><th>功能</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Esc</td><td>全屏与窗口模式切换</td></tr>
|
||||
<tr><td>Q</td><td>退出程序</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 第12章 -->
|
||||
<section class="doc-section" id="sec-12">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 12</span>
|
||||
<h2>故障排除</h2>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
<caption>表 12-1 常见故障及处理</caption>
|
||||
<thead>
|
||||
<tr><th>序号</th><th>故障现象</th><th>可能原因</th><th>处理措施</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>传感器无画面,FPS 显示 <code>--</code></td>
|
||||
<td>摄像头未连接;设备编号错误;被其他程序占用</td>
|
||||
<td>检查 USB 连接及 <code>video1</code>/<code>video2</code>;关闭占用程序;执行单侧或全部重启</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>串口连接失败</td>
|
||||
<td>端口号错误;驱动未安装;端口被占用;从站地址/波特率不匹配</td>
|
||||
<td>核对 <code>port</code>、<code>baudrate</code>、<code>slave_id</code>;检查 <code>main.log</code> 中 Modbus 相关报错</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>启动后 <code>state=error</code></td>
|
||||
<td>传感器或串口初始化失败;配置文件格式错误</td>
|
||||
<td>查看 <code>main.log</code> 异常栈;可用 <code>--dry-run</code> 区分串口与传感器故障</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>空载力值不归零 / 漂移</td>
|
||||
<td>标定阶段有负载;光照或摄像头松动;标定参数不当</td>
|
||||
<td>移开负载后重启控制程序;标定阶段保持静止;检查标定 JSON</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>修改参数未生效</td>
|
||||
<td>未保存文件或未重启控制程序</td>
|
||||
<td>确认保存成功并执行「重启控制程序」</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>3D 模型无法显示</td>
|
||||
<td>OpenGL 不可用;模型资源缺失</td>
|
||||
<td>更新显卡驱动;确认 <code>crt_ctag2f90d_gripper_visualization</code> 目录完整</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>界面卡顿、FPS 低</td>
|
||||
<td>光流算法配置不当;分辨率或帧率过高</td>
|
||||
<td>确认 <code>ddjx01_q6a.json</code> 中算法为 <code>dis</code>;适当降低 <code>sensor_fps</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 第13章 -->
|
||||
<section class="doc-section" id="sec-13">
|
||||
<div class="section-head">
|
||||
<span class="section-num">CHAPTER 13</span>
|
||||
<h2>维护与定期检查</h2>
|
||||
</div>
|
||||
|
||||
<h3>13.1 日常维护</h3>
|
||||
<ul>
|
||||
<li>保持触觉敏感面清洁,避免油污及硬质颗粒;</li>
|
||||
<li>定期检查 USB 及串口线缆有无松动或外皮破损;</li>
|
||||
<li>长期停用后重新启用前,执行一次完整重启并完成空载标定;</li>
|
||||
<li>重要参数变更前备份 <code>config/gripper.json</code> 及 <code>config/gui_layout.json</code>。</li>
|
||||
</ul>
|
||||
|
||||
<h3>13.2 标定参数维护</h3>
|
||||
<p><code>normal_force_calibration</code> 与 <code>shear_force_calibration</code> 用于将传感器原始量映射至牛顿单位,直接影响触发、接触及释放判断。修改标定点须有实测数据支持,修改后须重新验证全流程。</p>
|
||||
</section>
|
||||
|
||||
<!-- 附录 -->
|
||||
<section class="doc-section" id="sec-a">
|
||||
<div class="section-head">
|
||||
<span class="section-num">APPENDIX</span>
|
||||
<h2>附录</h2>
|
||||
</div>
|
||||
|
||||
<h3 id="appendix-a">附录 A 现场操作检查表</h3>
|
||||
<h4>启动前检查</h4>
|
||||
<ul class="checklist">
|
||||
<li>左右摄像头已被操作系统识别</li>
|
||||
<li>夹爪电源及串口通信正常</li>
|
||||
<li><code>video1</code>、<code>video2</code>、<code>port</code> 配置正确</li>
|
||||
<li>夹爪开合范围内无人员及杂物</li>
|
||||
<li>两侧触觉传感器无预加载</li>
|
||||
</ul>
|
||||
<h4>启动后确认</h4>
|
||||
<ul class="checklist">
|
||||
<li>左右传感器 FPS 正常,状态点为绿色</li>
|
||||
<li>3D 模型姿态与夹爪实际位置一致</li>
|
||||
<li>顶部状态无 <code>error</code></li>
|
||||
<li>已等待 <code>open-wait-armed(已就绪)</code> 后再进行演示</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="appendix-b">附录 B 插图维护说明</h3>
|
||||
<p>本手册插图由软件界面自动截取生成。软件界面变更后,可在项目根目录执行以下命令重新生成插图:</p>
|
||||
<pre class="code-block">conda activate py311
|
||||
python build/capture_gui_screenshots.py
|
||||
python build/generate_manual_diagrams.py</pre>
|
||||
<p>截图输出路径:<code>docs/images/manual/</code>;矢量示意图输出路径:<code>docs/assets/diagrams/</code>。</p>
|
||||
|
||||
<hr style="margin: 2.5rem 0; border: none; border-top: 1px solid var(--border);">
|
||||
<p style="font-size: 0.8125rem; color: var(--ink-light); text-align: center;">
|
||||
— 文档结束 · GD02-UM-001 V1.0 —<br>
|
||||
Orisys 夹爪产品使用说明书
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<button type="button" class="toolbar-hint" onclick="window.print()" title="打印或导出 PDF">打印 / 导出 PDF</button>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var links = document.querySelectorAll('.manual-nav a[href^="#"]');
|
||||
var sections = [];
|
||||
links.forEach(function (a) {
|
||||
var id = a.getAttribute('href').slice(1);
|
||||
var el = document.getElementById(id);
|
||||
if (el) sections.push({ link: a, el: el });
|
||||
});
|
||||
function onScroll() {
|
||||
var scroll = window.scrollY + 80;
|
||||
var current = sections[0];
|
||||
sections.forEach(function (s) {
|
||||
if (s.el.offsetTop <= scroll) current = s;
|
||||
});
|
||||
links.forEach(function (a) { a.classList.remove('active'); });
|
||||
if (current) current.link.classList.add('active');
|
||||
}
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
onScroll();
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
# Orisys 夹爪产品使用说明书
|
||||
|
||||
> **正式版文档已迁移为 HTML 格式。**
|
||||
|
||||
请打开以下文件查阅完整产品使用说明书(含封面、版本履历、规范图号表号及全部界面截图):
|
||||
|
||||
**[`PRODUCT_MANUAL.zh-CN.html`](PRODUCT_MANUAL.zh-CN.html)**
|
||||
|
||||
## 使用方式
|
||||
|
||||
1. 在文件管理器中双击 `docs/PRODUCT_MANUAL.zh-CN.html`,使用浏览器打开;
|
||||
2. 点击右下角「打印 / 导出 PDF」或使用浏览器打印功能(Ctrl+P)导出 PDF;
|
||||
3. 左侧导航栏可快速跳转章节。
|
||||
|
||||
## 相关文档
|
||||
|
||||
| 文档 | 说明 |
|
||||
| --- | --- |
|
||||
| [PRODUCT_MANUAL.zh-CN.html](PRODUCT_MANUAL.zh-CN.html) | 正式产品使用说明书(推荐) |
|
||||
| [GUI_USER_MANUAL.zh-CN.md](GUI_USER_MANUAL.zh-CN.md) | 详细技术说明与完整参数表 |
|
||||
| [assets/manual.css](assets/manual.css) | 说明书样式表 |
|
||||
| [images/manual/](images/manual/) | 界面截图资源 |
|
||||
|
||||
## 更新截图
|
||||
|
||||
```powershell
|
||||
conda activate py311
|
||||
python build/capture_gui_screenshots.py
|
||||
```
|
||||
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 920 324" role="img">
|
||||
<defs>
|
||||
<linearGradient id="gIdle" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#dbeafe"/><stop offset="100%" stop-color="#bfdbfe"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gAct" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fef3c7"/><stop offset="100%" stop-color="#fde68a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gHold" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fce7f3"/><stop offset="100%" stop-color="#fbcfe8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gRec" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#d1fae5"/><stop offset="100%" stop-color="#a7f3d0"/>
|
||||
</linearGradient>
|
||||
<marker id="arr" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
|
||||
<path d="M0,0 L8,4 L0,8 Z" fill="#475569"/>
|
||||
</marker>
|
||||
<filter id="sh" x="-6%" y="-8%" width="112%" height="120%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2.5" flood-color="#0f172a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<style>
|
||||
.cn{font:700 14px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#0f172a}
|
||||
.en{font:600 11px Consolas,monospace;fill:#475569}
|
||||
.sub{font:400 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#64748b}
|
||||
.ed{font:500 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#334155}
|
||||
.nt{font:400 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#64748b}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<g filter="url(#sh)"><rect x="24" y="40" width="148" height="72" rx="10" fill="url(#gIdle)" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="98" y="66" text-anchor="middle" class="cn">等待触发</text>
|
||||
<text x="98" y="84" text-anchor="middle" class="en">open_wait</text>
|
||||
<text x="98" y="100" text-anchor="middle" class="sub">已就绪 armed</text></g>
|
||||
|
||||
<path d="M172 76 H208" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="190" y="68" text-anchor="middle" class="ed">侧边触发</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="208" y="40" width="148" height="72" rx="10" fill="url(#gAct)" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="282" y="66" text-anchor="middle" class="cn">闭合中</text>
|
||||
<text x="282" y="84" text-anchor="middle" class="en">closing</text>
|
||||
<text x="282" y="100" text-anchor="middle" class="sub">寻找物体</text></g>
|
||||
|
||||
<path d="M356 76 H392" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="374" y="68" text-anchor="middle" class="ed">双侧接触</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="392" y="40" width="148" height="72" rx="10" fill="url(#gAct)" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="466" y="66" text-anchor="middle" class="cn">夹持加力</text>
|
||||
<text x="466" y="84" text-anchor="middle" class="en">gripping</text>
|
||||
<text x="466" y="100" text-anchor="middle" class="sub">低力 → 基础力</text></g>
|
||||
|
||||
<path d="M540 76 H576" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="558" y="68" text-anchor="middle" class="ed">到达保持力</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="576" y="40" width="148" height="72" rx="10" fill="url(#gHold)" stroke="#db2777" stroke-width="1.5"/>
|
||||
<text x="650" y="66" text-anchor="middle" class="cn">夹持检测</text>
|
||||
<text x="650" y="84" text-anchor="middle" class="en">hold_check</text>
|
||||
<text x="650" y="100" text-anchor="middle" class="sub">自适应补力 / 释放检测</text></g>
|
||||
|
||||
<path d="M724 76 H760" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="742" y="68" text-anchor="middle" class="ed">取物释放</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="760" y="40" width="136" height="72" rx="10" fill="url(#gRec)" stroke="#059669" stroke-width="1.5"/>
|
||||
<text x="828" y="66" text-anchor="middle" class="cn">张开恢复</text>
|
||||
<text x="828" y="84" text-anchor="middle" class="en">open_recover</text>
|
||||
<text x="828" y="100" text-anchor="middle" class="sub">位置 / 力恢复</text></g>
|
||||
|
||||
<path d="M828 112 V204 H98 V112" fill="none" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<rect x="340" y="188" width="240" height="24" rx="4" fill="#eff6ff" stroke="#bfdbfe"/>
|
||||
<text x="460" y="204" text-anchor="middle" class="ed" fill="#1e40af">恢复完成 + 休息时间(rearm)→ 再次就绪</text>
|
||||
|
||||
<path d="M282 112 V152 H828" fill="none" stroke="#dc2626" stroke-width="1.2" stroke-dasharray="5 4"/>
|
||||
<rect x="200" y="128" width="164" height="22" rx="4" fill="#fef2f2" stroke="#fecaca"/>
|
||||
<text x="282" y="143" text-anchor="middle" class="ed" fill="#b91c1c">闭合超时 close_timeout → 张开</text>
|
||||
|
||||
<rect x="24" y="232" width="872" height="76" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
|
||||
<text x="40" y="254" class="cn" font-size="12">阶段说明</text>
|
||||
<text x="40" y="274" class="nt">① 等待触发:夹爪张开,单侧力超阈且为新上升沿时进入闭合。</text>
|
||||
<text x="40" y="294" class="nt">② 闭合~夹持:物体接触后低力保持,分步加力至 hold_force,并按切向负载自适应补力。</text>
|
||||
<text x="40" y="314" class="nt">③ 释放~恢复:力变化超阈则张开;满足恢复条件并经过 rearm_seconds 后,方可再次触发。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 152 KiB |
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 520" role="img">
|
||||
<defs>
|
||||
<linearGradient id="sensorGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#dbeafe"/><stop offset="100%" stop-color="#bfdbfe"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="procGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e0e7ff"/><stop offset="100%" stop-color="#c7d2fe"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="ctrlGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fef3c7"/><stop offset="100%" stop-color="#fde68a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="guiGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#d1fae5"/><stop offset="100%" stop-color="#a7f3d0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hwGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f1f5f9"/><stop offset="100%" stop-color="#e2e8f0"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
|
||||
<path d="M0,0 L8,4 L0,8 Z" fill="#475569"/>
|
||||
</marker>
|
||||
<filter id="shadow" x="-4%" y="-4%" width="108%" height="112%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#0f172a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<style>
|
||||
.t1{font:700 15px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#0f172a}
|
||||
.t2{font:400 12px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#475569}
|
||||
.tl{font:700 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#64748b;letter-spacing:.08em}
|
||||
.te{font:500 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#334155}
|
||||
.tp{font:600 10px Consolas,monospace;fill:#1e40af}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect x="24" y="52" width="832" height="108" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
|
||||
<text x="40" y="72" class="tl">感知层</text>
|
||||
<rect x="24" y="176" width="832" height="148" rx="8" fill="#fafafa" stroke="#e2e8f0"/>
|
||||
<text x="40" y="196" class="tl">软件处理层</text>
|
||||
<rect x="24" y="340" width="832" height="108" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
|
||||
<text x="40" y="360" class="tl">执行与呈现层</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="56" y="88" width="168" height="56" rx="8" fill="url(#sensorGrad)" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<text x="140" y="112" text-anchor="middle" class="t1">左触觉传感器</text>
|
||||
<text x="140" y="130" text-anchor="middle" class="t2">USB 摄像头 + 敏感面</text>
|
||||
</g>
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="656" y="88" width="168" height="56" rx="8" fill="url(#sensorGrad)" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<text x="740" y="112" text-anchor="middle" class="t1">右触觉传感器</text>
|
||||
<text x="740" y="130" text-anchor="middle" class="t2">USB 摄像头 + 敏感面</text>
|
||||
</g>
|
||||
|
||||
<path d="M224 116 H340" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<path d="M656 116 H540" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="440" y="108" text-anchor="middle" class="te">视频帧采集</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="300" y="212" width="280" height="76" rx="10" fill="url(#procGrad)" stroke="#6366f1" stroke-width="1.5"/>
|
||||
<text x="440" y="240" text-anchor="middle" class="t1">触觉力计算 / 光流处理</text>
|
||||
<text x="440" y="260" text-anchor="middle" class="t2">法向力 · 切向力 · 光流/幅值图像</text>
|
||||
<text x="440" y="276" text-anchor="middle" class="t2">Orisys SDK · OpenCV DIS</text>
|
||||
</g>
|
||||
|
||||
<path d="M140 144 V188 H300" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<path d="M740 144 V188 H580" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="56" y="220" width="200" height="60" rx="8" fill="url(#ctrlGrad)" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="156" y="246" text-anchor="middle" class="t1">智能夹持状态机</text>
|
||||
<text x="156" y="264" text-anchor="middle" class="t2">触发 · 闭合 · 加力 · 释放</text>
|
||||
</g>
|
||||
|
||||
<path d="M300 250 H256" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="278" y="242" text-anchor="middle" class="te">力信号</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="624" y="220" width="200" height="60" rx="8" fill="url(#hwGrad)" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="724" y="246" text-anchor="middle" class="t1">夹爪控制器</text>
|
||||
<text x="724" y="264" text-anchor="middle" class="t2">Modbus RTU 从站</text>
|
||||
</g>
|
||||
|
||||
<path d="M256 250 H420" fill="none" stroke="#64748b" stroke-width="1.5"/>
|
||||
<path d="M420 250 H624" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="440" y="242" text-anchor="middle" class="tp">Modbus RTU / 串口</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="624" y="368" width="200" height="56" rx="8" fill="url(#hwGrad)" stroke="#475569" stroke-width="1.5"/>
|
||||
<text x="724" y="392" text-anchor="middle" class="t1">智能夹爪(执行机构)</text>
|
||||
<text x="724" y="410" text-anchor="middle" class="t2">位置 · 力度 · 开/合/保持</text>
|
||||
</g>
|
||||
<path d="M724 280 V368" fill="none" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="738" y="328" class="te">驱动</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="240" y="356" width="280" height="80" rx="10" fill="url(#guiGrad)" stroke="#059669" stroke-width="1.5"/>
|
||||
<text x="380" y="384" text-anchor="middle" class="t1">GUI 监控界面</text>
|
||||
<text x="380" y="404" text-anchor="middle" class="t2">3D 模型 · 光流画面 · 力曲线 · 状态</text>
|
||||
<text x="380" y="422" text-anchor="middle" class="t2">PyQt5 · PyOpenGL · pyqtgraph</text>
|
||||
</g>
|
||||
|
||||
<path d="M156 280 V320 H380 V356" fill="none" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="168" y="312" class="te">状态 / 动作</text>
|
||||
<path d="M440 288 V356" fill="none" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="452" y="328" class="te">图像 / 力值</text>
|
||||
|
||||
<path d="M624 396 H520 V250 H256" fill="none" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="5 4" marker-end="url(#arrow)"/>
|
||||
<text x="572" y="388" text-anchor="middle" class="te">位置反馈</text>
|
||||
|
||||
<g transform="translate(56,468)">
|
||||
<line x1="0" y1="8" x2="28" y2="8" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="36" y="12" class="t2">数据 / 控制流</text>
|
||||
<line x1="140" y1="8" x2="168" y2="8" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="5 4" marker-end="url(#arrow)"/>
|
||||
<text x="176" y="12" class="t2">反馈</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,38 @@
|
||||
\documentclass[tikz,border=8pt]{standalone}
|
||||
\usepackage{ctex}
|
||||
\usepackage{tikz}
|
||||
\usetikzlibrary{positioning,arrows.meta,fit,backgrounds}
|
||||
|
||||
\tikzset{
|
||||
st/.style={draw, rounded corners=2pt, align=center, font=\small,
|
||||
minimum width=2.55cm, minimum height=1.15cm, inner sep=2pt},
|
||||
idle/.style={st, fill=blue!8, draw=blue!55},
|
||||
act/.style={st, fill=orange!10, draw=orange!60!black},
|
||||
hold/.style={st, fill=magenta!6, draw=magenta!55},
|
||||
rec/.style={st, fill=teal!8, draw=teal!60!black},
|
||||
arr/.style={-{Stealth[length=2.2mm]}, semithick, draw=gray!70},
|
||||
darr/.style={-{Stealth[length=2.2mm]}, semithick, draw=red!65, dashed},
|
||||
loop/.style={-{Stealth[length=2.2mm]}, semithick, draw=blue!60},
|
||||
ed/.style={font=\scriptsize, fill=white, inner sep=1pt, text=gray!25!black},
|
||||
}
|
||||
|
||||
\begin{document}
|
||||
\begin{tikzpicture}[node distance=7mm]
|
||||
|
||||
\node[idle] (w) {等待触发\\\scriptsize open\_wait};
|
||||
\node[act, right=of w] (c) {闭合中\\\scriptsize closing};
|
||||
\node[act, right=of c] (g) {夹持加力\\\scriptsize gripping};
|
||||
\node[hold, right=of g] (h) {夹持检测\\\scriptsize hold\_check};
|
||||
\node[rec, right=of h] (o) {张开恢复\\\scriptsize open\_recover};
|
||||
|
||||
\draw[arr] (w) -- node[ed, above] {侧边触发} (c);
|
||||
\draw[arr] (c) -- node[ed, above] {接触} (g);
|
||||
\draw[arr] (g) -- node[ed, above] {到保持力} (h);
|
||||
\draw[arr] (h) -- node[ed, above] {取物} (o);
|
||||
|
||||
\draw[loop] (o.south) -- ++(0,-0.75) -| node[ed, pos=0.25, below] {恢复 + rearm} (w.south);
|
||||
|
||||
\draw[darr] (c.south) -- ++(0,-0.55) -| node[ed, pos=0.12, below] {超时} (o.south);
|
||||
|
||||
\end{tikzpicture}
|
||||
\end{document}
|
||||
@@ -0,0 +1,49 @@
|
||||
\documentclass[tikz,border=8pt]{standalone}
|
||||
\usepackage{ctex}
|
||||
\usepackage{tikz}
|
||||
\usetikzlibrary{positioning,fit,arrows.meta,calc,backgrounds}
|
||||
|
||||
\tikzset{
|
||||
base/.style={draw, rounded corners=2pt, align=center, font=\small},
|
||||
sensor/.style={base, fill=blue!8, draw=blue!55, minimum width=3.0cm, minimum height=1.05cm},
|
||||
process/.style={base, fill=violet!8, draw=violet!55, minimum width=4.2cm, minimum height=1.15cm},
|
||||
control/.style={base, fill=orange!10, draw=orange!60!black, minimum width=3.0cm, minimum height=1.0cm},
|
||||
device/.style={base, fill=gray!10, draw=gray!60, minimum width=3.0cm, minimum height=1.0cm},
|
||||
gui/.style={base, fill=teal!8, draw=teal!60!black, minimum width=4.2cm, minimum height=1.05cm},
|
||||
layer/.style={draw=gray!35, rounded corners=3pt, inner sep=10pt},
|
||||
arr/.style={-{Stealth[length=2.2mm]}, semithick, draw=gray!70},
|
||||
darr/.style={-{Stealth[length=2.2mm]}, semithick, draw=gray!55, dashed},
|
||||
lbl/.style={font=\scriptsize, fill=white, inner sep=1pt, text=gray!30!black},
|
||||
}
|
||||
|
||||
\begin{document}
|
||||
\begin{tikzpicture}[node distance=8mm and 10mm]
|
||||
|
||||
\node[sensor] (L) {左触觉传感器\\{\scriptsize USB 摄像头}};
|
||||
\node[sensor, right=5.8cm of L] (R) {右触觉传感器\\{\scriptsize USB 摄像头}};
|
||||
|
||||
\node[process, below=1.35cm of $(L)!0.5!(R)$] (P) {触觉力计算 / 光流\\{\scriptsize 法向力 · 切向力 · 图像}};
|
||||
|
||||
\node[control, below left=1.1cm and 0.2cm of P] (SM) {智能夹持\\状态机};
|
||||
\node[device, below right=1.1cm and 0.2cm of P] (GC) {夹爪控制器\\{\scriptsize Modbus RTU}};
|
||||
|
||||
\node[device, below=1.0cm of GC] (G) {智能夹爪};
|
||||
\node[gui, below=1.0cm of P] (UI) {GUI 监控界面\\{\scriptsize 3D · 光流 · 曲线 · 状态}};
|
||||
|
||||
\begin{scope}[on background layer]
|
||||
\node[layer, fit=(L)(R), label={[font=\scriptsize\bfseries]above left:感知层}] {};
|
||||
\node[layer, fit=(P)(SM)(GC), label={[font=\scriptsize\bfseries]above left:软件处理层}] {};
|
||||
\node[layer, fit=(G)(UI), label={[font=\scriptsize\bfseries]above left:执行与呈现层}] {};
|
||||
\end{scope}
|
||||
|
||||
\draw[arr] (L) -- node[lbl, above] {视频帧} (P);
|
||||
\draw[arr] (R) -- node[lbl, above] {视频帧} (P);
|
||||
\draw[arr] (P) -- node[lbl, left] {力信号} (SM);
|
||||
\draw[arr] (SM) -- node[lbl, above] {串口命令} (GC);
|
||||
\draw[arr] (GC) -- node[lbl, right] {驱动} (G);
|
||||
\draw[arr] (P) -- node[lbl, right] {图像/力值} (UI);
|
||||
\draw[arr] (SM) -- node[lbl, left] {状态} (UI);
|
||||
\draw[darr] (G.west) -| node[lbl, near start, below] {位置反馈} (SM.south);
|
||||
|
||||
\end{tikzpicture}
|
||||
\end{document}
|
||||
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 218 KiB |
@@ -32,8 +32,8 @@ class Demo02Config:
|
||||
video2: str = "1"
|
||||
rotation1: dict | str = field(default_factory=lambda: copy.deepcopy(DEFAULT_ROTATION1))
|
||||
rotation2: dict | str = field(default_factory=lambda: copy.deepcopy(DEFAULT_ROTATION2))
|
||||
sensor_config1: str = "./config/ddjx01.json"
|
||||
sensor_config2: str = "./config/ddjx01.json"
|
||||
sensor_config1: str = "./config/ddjx01_q6a.json"
|
||||
sensor_config2: str = "./config/ddjx01_q6a.json"
|
||||
sensor_fps: float = 30.0
|
||||
motion_threshold: float = 0.0
|
||||
sensor_cpu: bool = False
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Discover USB cameras without coupling discovery to the sensor runtime."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
import cv2
|
||||
|
||||
|
||||
# Most USB camera stacks expose devices from 0 upward. Keeping this bounded
|
||||
# prevents a search from spending seconds probing empty DirectShow devices.
|
||||
DEFAULT_MAX_CAMERA_INDEX = 3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CameraInfo:
|
||||
index: int
|
||||
backend_name: str
|
||||
backend_api: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
if sys.platform.startswith("linux"):
|
||||
return f"/dev/video{self.index}"
|
||||
return str(self.index)
|
||||
|
||||
def label(self):
|
||||
device = self.source if sys.platform.startswith("linux") else f"相机 {self.index}"
|
||||
return f"{device} {self.width}x{self.height} [{self.backend_name}]"
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _suppress_opencv_noise():
|
||||
previous_level = None
|
||||
set_level = None
|
||||
try:
|
||||
logging = getattr(getattr(cv2, "utils", None), "logging", None)
|
||||
if logging is not None:
|
||||
set_level = getattr(logging, "setLogLevel", None)
|
||||
get_level = getattr(logging, "getLogLevel", None)
|
||||
silent = getattr(logging, "LOG_LEVEL_SILENT", 0)
|
||||
if set_level is not None:
|
||||
if get_level is not None:
|
||||
previous_level = get_level()
|
||||
set_level(silent)
|
||||
except Exception:
|
||||
previous_level = None
|
||||
|
||||
devnull = open(os.devnull, "w")
|
||||
try:
|
||||
with contextlib.redirect_stderr(devnull):
|
||||
try:
|
||||
stderr_fd = sys.stderr.fileno()
|
||||
saved_fd = os.dup(stderr_fd)
|
||||
except (AttributeError, OSError):
|
||||
yield
|
||||
else:
|
||||
try:
|
||||
os.dup2(devnull.fileno(), stderr_fd)
|
||||
yield
|
||||
finally:
|
||||
os.dup2(saved_fd, stderr_fd)
|
||||
os.close(saved_fd)
|
||||
finally:
|
||||
devnull.close()
|
||||
if set_level is not None and previous_level is not None:
|
||||
try:
|
||||
set_level(previous_level)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _backend_candidates():
|
||||
candidates = []
|
||||
if sys.platform == "win32":
|
||||
if hasattr(cv2, "CAP_DSHOW"):
|
||||
candidates.append(("DSHOW", cv2.CAP_DSHOW))
|
||||
if hasattr(cv2, "CAP_MSMF"):
|
||||
candidates.append(("MSMF", cv2.CAP_MSMF))
|
||||
elif sys.platform.startswith("linux") and hasattr(cv2, "CAP_V4L2"):
|
||||
candidates.append(("V4L2", cv2.CAP_V4L2))
|
||||
return candidates or [("ANY", cv2.CAP_ANY)]
|
||||
|
||||
|
||||
def camera_source_index(source):
|
||||
"""Return the numeric index for ``0`` or ``/dev/video0`` sources."""
|
||||
text = str(source or "").strip()
|
||||
if text.isdigit():
|
||||
return int(text)
|
||||
if text.startswith("/dev/video"):
|
||||
suffix = text[len("/dev/video"):]
|
||||
if suffix.isdigit():
|
||||
return int(suffix)
|
||||
return None
|
||||
|
||||
|
||||
def _candidate_indices(max_index):
|
||||
if sys.platform.startswith("linux"):
|
||||
indices = []
|
||||
for path in Path("/dev").glob("video*"):
|
||||
index = camera_source_index(str(path))
|
||||
if index is not None:
|
||||
indices.append(index)
|
||||
if indices:
|
||||
return sorted(set(indices))
|
||||
return list(range(max_index + 1))
|
||||
|
||||
|
||||
def _try_open(index, backend_name, backend_api):
|
||||
capture = cv2.VideoCapture(index, backend_api)
|
||||
try:
|
||||
if not capture.isOpened():
|
||||
return None
|
||||
ok, frame = capture.read()
|
||||
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH) or 0)
|
||||
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0)
|
||||
if ok and frame is not None:
|
||||
frame_height, frame_width = frame.shape[:2]
|
||||
width = width or int(frame_width)
|
||||
height = height or int(frame_height)
|
||||
elif width <= 0 or height <= 0:
|
||||
return None
|
||||
return CameraInfo(index, backend_name, backend_api, width, height)
|
||||
finally:
|
||||
capture.release()
|
||||
|
||||
|
||||
def list_cameras(
|
||||
max_index=DEFAULT_MAX_CAMERA_INDEX,
|
||||
skip_indices=None,
|
||||
fallback_backends=False,
|
||||
):
|
||||
"""Probe camera indices and return one working backend for each index."""
|
||||
found = []
|
||||
seen_indices = set()
|
||||
for source in skip_indices or ():
|
||||
index = camera_source_index(source)
|
||||
if index is not None:
|
||||
seen_indices.add(index)
|
||||
backends = _backend_candidates()
|
||||
if not fallback_backends:
|
||||
backends = backends[:1]
|
||||
with _suppress_opencv_noise():
|
||||
# Probe one backend across all indices before falling back to another
|
||||
# backend. On Windows this avoids opening every empty index twice when
|
||||
# DirectShow is available.
|
||||
for backend_name, backend_api in backends:
|
||||
for index in _candidate_indices(max_index):
|
||||
if index in seen_indices:
|
||||
continue
|
||||
info = _try_open(index, backend_name, backend_api)
|
||||
if info is not None:
|
||||
found.append(info)
|
||||
seen_indices.add(index)
|
||||
found.sort(key=lambda camera: camera.index)
|
||||
return found
|
||||
@@ -272,8 +272,8 @@ class TwoSensorForceReader:
|
||||
self,
|
||||
video1=0,
|
||||
video2=1,
|
||||
config1="./config/ddjx01.json",
|
||||
config2="./config/ddjx01.json",
|
||||
config1="./config/ddjx01_q6a.json",
|
||||
config2="./config/ddjx01_q6a.json",
|
||||
rotation1=None,
|
||||
rotation2=None,
|
||||
target_fps=30,
|
||||
@@ -433,8 +433,8 @@ def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video1", "-v1", type=str, default="0")
|
||||
parser.add_argument("--video2", "-v2", type=str, default="1")
|
||||
parser.add_argument("--config1", type=str, default="./config/ddjx01.json")
|
||||
parser.add_argument("--config2", type=str, default="./config/ddjx01.json")
|
||||
parser.add_argument("--config1", type=str, default="./config/ddjx01_q6a.json")
|
||||
parser.add_argument("--config2", type=str, default="./config/ddjx01_q6a.json")
|
||||
parser.add_argument("--rotation1", "-r1", type=str, default=None)
|
||||
parser.add_argument("--rotation2", "-r2", type=str, default=None)
|
||||
parser.add_argument("--target-fps", type=float, default=30)
|
||||
|
||||
@@ -18,6 +18,11 @@ if sys.platform.startswith("linux") and os.environ.get("WSL_DISTRO_NAME"):
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from serial.tools import list_ports as serial_list_ports
|
||||
except Exception:
|
||||
serial_list_ports = None
|
||||
|
||||
|
||||
def _fix_qt_plugin_path():
|
||||
try:
|
||||
@@ -42,6 +47,7 @@ try:
|
||||
QApplication,
|
||||
QButtonGroup,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QFrame,
|
||||
QGraphicsOpacityEffect,
|
||||
@@ -71,7 +77,7 @@ except Exception as exc:
|
||||
QColor = QLinearGradient = QPainter = QPen = QPolygonF = QRadialGradient = QVector3D = None
|
||||
QGridLayout = QHBoxLayout = QLabel = QPushButton = QSizeGrip = QSizePolicy = QSplitter = None
|
||||
QVBoxLayout = QWidget = None
|
||||
QButtonGroup = QCheckBox = QDialog = QFrame = QGraphicsOpacityEffect = QLineEdit = QListWidget = QMessageBox = QMainWindow = object
|
||||
QButtonGroup = QCheckBox = QComboBox = QDialog = QFrame = QGraphicsOpacityEffect = QLineEdit = QListWidget = QMessageBox = QMainWindow = object
|
||||
QProgressBar = QScrollArea = QSlider = QSpinBox = QStackedWidget = QTextEdit = None
|
||||
|
||||
try:
|
||||
@@ -92,6 +98,7 @@ else:
|
||||
|
||||
from ..app import build_controller
|
||||
from ..common.paths import PACKAGE_DIR, REPO_ROOT
|
||||
from ..sensors.camera_discovery import camera_source_index, list_cameras
|
||||
|
||||
|
||||
PLOT_POINTS = 180
|
||||
@@ -599,7 +606,7 @@ QSpinBox {
|
||||
border-radius: 4px;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
QLineEdit, QTextEdit {
|
||||
QLineEdit, QTextEdit, QComboBox {
|
||||
background-color: #ffffff;
|
||||
color: #1f2933;
|
||||
border: 1px solid #d7dee8;
|
||||
@@ -608,7 +615,7 @@ QLineEdit, QTextEdit {
|
||||
font-family: "Cascadia Code", "Consolas";
|
||||
font-size: 12px;
|
||||
}
|
||||
QLineEdit:focus, QTextEdit:focus {
|
||||
QLineEdit:focus, QTextEdit:focus, QComboBox:focus {
|
||||
border: 1px solid #7aa7e8;
|
||||
}
|
||||
QListWidget {
|
||||
@@ -733,6 +740,12 @@ def _draw_top_icon(kind, color="#344054", size=22):
|
||||
painter.setBrush(QColor("#ffffff"))
|
||||
painter.drawEllipse(QPointF(knob_x, y), size * 0.075, size * 0.075)
|
||||
painter.setBrush(Qt.NoBrush)
|
||||
elif kind == "search":
|
||||
painter.drawEllipse(QRectF(size * 0.25, size * 0.23, size * 0.40, size * 0.40))
|
||||
painter.drawLine(
|
||||
QPointF(size * 0.60, size * 0.60),
|
||||
QPointF(size * 0.76, size * 0.76),
|
||||
)
|
||||
elif kind == "minimize":
|
||||
painter.drawLine(
|
||||
QPointF(size * 0.32, size * 0.62),
|
||||
@@ -932,6 +945,7 @@ class ImageBox(QFrame):
|
||||
layout.setContentsMargins(6, 6, 6, 6)
|
||||
layout.setSpacing(5)
|
||||
header = QHBoxLayout()
|
||||
self.header_layout = header
|
||||
header.setSpacing(6)
|
||||
self.title_label = QLabel(title)
|
||||
self.title_label.setObjectName("SectionTitle")
|
||||
@@ -994,6 +1008,10 @@ class FloatingImageBox(ImageBox):
|
||||
self.image_label.setScaledContents(False)
|
||||
self.mode_toggle_button.setObjectName("FlowModeButton")
|
||||
self.mode_toggle_button.setText("光流")
|
||||
self.camera_search_button = TopIconButton("search", "搜索并设置左右摄像头")
|
||||
self.camera_search_button.setFixedSize(24, 24)
|
||||
self.camera_search_button.setIconSize(QSize(17, 17))
|
||||
self.header_layout.insertWidget(self.header_layout.count() - 1, self.camera_search_button)
|
||||
self._drag_mode = None
|
||||
self._drag_start_global = None
|
||||
self._start_geometry = None
|
||||
@@ -2203,6 +2221,141 @@ class SliderControl(QFrame):
|
||||
return int(self.slider.value())
|
||||
|
||||
|
||||
class CameraDiscoveryDialog(QDialog):
|
||||
def __init__(self, viewer):
|
||||
super().__init__(viewer)
|
||||
self.viewer = viewer
|
||||
self._scan_id = 0
|
||||
self._scan_result = None
|
||||
self.setWindowTitle("双摄像头设置")
|
||||
self.setWindowIcon(_load_company_icon(64))
|
||||
# Keep the main window responsive while OpenCV probes devices in the
|
||||
# worker thread. A modeless child also avoids a full-screen compositor
|
||||
# flash caused by switching into a nested modal event loop.
|
||||
self.setModal(False)
|
||||
self.setMinimumWidth(520)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(14, 14, 14, 14)
|
||||
layout.setSpacing(10)
|
||||
self.status_label = QLabel("正在搜索摄像头...")
|
||||
self.status_label.setObjectName("MetricLabel")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
camera_grid = QGridLayout()
|
||||
camera_grid.setHorizontalSpacing(10)
|
||||
camera_grid.setVerticalSpacing(8)
|
||||
self.camera_combos = {}
|
||||
for row, (side, title) in enumerate((("left", "左侧摄像头"), ("right", "右侧摄像头"))):
|
||||
label = QLabel(title)
|
||||
label.setObjectName("MetricLabel")
|
||||
combo = QComboBox()
|
||||
combo.setMinimumHeight(32)
|
||||
self.camera_combos[side] = combo
|
||||
camera_grid.addWidget(label, row, 0)
|
||||
camera_grid.addWidget(combo, row, 1)
|
||||
camera_grid.setColumnStretch(1, 1)
|
||||
layout.addLayout(camera_grid)
|
||||
|
||||
actions = QHBoxLayout()
|
||||
self.refresh_button = AnimatedButton("重新搜索")
|
||||
self.refresh_button.setObjectName("ManualButton")
|
||||
self.refresh_button.clicked.connect(self._start_scan)
|
||||
self.apply_button = AnimatedButton("应用左右摄像头")
|
||||
self.apply_button.setObjectName("OpenActionButton")
|
||||
self.apply_button.clicked.connect(self._apply)
|
||||
cancel_button = AnimatedButton("取消")
|
||||
cancel_button.setObjectName("ManualButton")
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
actions.addWidget(self.refresh_button)
|
||||
actions.addStretch(1)
|
||||
actions.addWidget(self.apply_button)
|
||||
actions.addWidget(cancel_button)
|
||||
layout.addLayout(actions)
|
||||
|
||||
self.poll_timer = QTimer(self)
|
||||
self.poll_timer.setInterval(100)
|
||||
self.poll_timer.timeout.connect(self._poll_scan)
|
||||
self._start_scan()
|
||||
|
||||
def _start_scan(self):
|
||||
self._scan_id += 1
|
||||
scan_id = self._scan_id
|
||||
self._scan_result = None
|
||||
self.status_label.setText("正在快速搜索摄像头(编号 0-3)...")
|
||||
current_sources = self.viewer.camera_sources()
|
||||
skip_indices = set()
|
||||
for side, combo in self.camera_combos.items():
|
||||
combo.clear()
|
||||
current = current_sources.get(side, "")
|
||||
if current:
|
||||
combo.addItem(f"当前配置 {current}(可能正在使用)", current)
|
||||
combo.setEnabled(True)
|
||||
current_index = camera_source_index(current)
|
||||
if current_index is not None:
|
||||
skip_indices.add(current_index)
|
||||
else:
|
||||
combo.setEnabled(False)
|
||||
self.refresh_button.setEnabled(False)
|
||||
self.apply_button.setEnabled(
|
||||
all(combo.count() > 0 for combo in self.camera_combos.values())
|
||||
)
|
||||
|
||||
def scan():
|
||||
try:
|
||||
self._scan_result = (
|
||||
scan_id,
|
||||
list_cameras(skip_indices=skip_indices),
|
||||
None,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._scan_result = (scan_id, [], str(exc))
|
||||
|
||||
threading.Thread(target=scan, daemon=True).start()
|
||||
self.poll_timer.start()
|
||||
|
||||
def _poll_scan(self):
|
||||
result = self._scan_result
|
||||
if result is None:
|
||||
return
|
||||
scan_id, cameras, error = result
|
||||
if scan_id != self._scan_id:
|
||||
return
|
||||
self.poll_timer.stop()
|
||||
self.refresh_button.setEnabled(True)
|
||||
current_sources = self.viewer.camera_sources()
|
||||
found_sources = set()
|
||||
for side, combo in self.camera_combos.items():
|
||||
combo.clear()
|
||||
for camera in cameras:
|
||||
combo.addItem(camera.label(), camera.source)
|
||||
found_sources.add(camera.source)
|
||||
current = current_sources.get(side, "")
|
||||
if current and current not in found_sources:
|
||||
combo.addItem(f"当前配置 {current}(可能正在使用)", current)
|
||||
current_index = combo.findData(current)
|
||||
if current_index >= 0:
|
||||
combo.setCurrentIndex(current_index)
|
||||
if error:
|
||||
self.status_label.setText(f"搜索失败:{error}")
|
||||
elif cameras:
|
||||
self.status_label.setText(f"找到 {len(cameras)} 个可用摄像头")
|
||||
else:
|
||||
self.status_label.setText("未发现空闲摄像头;正在使用的当前配置仍可保留")
|
||||
has_both = all(combo.count() > 0 for combo in self.camera_combos.values())
|
||||
for combo in self.camera_combos.values():
|
||||
combo.setEnabled(combo.count() > 0)
|
||||
self.apply_button.setEnabled(has_both)
|
||||
|
||||
def _apply(self):
|
||||
left = self.camera_combos["left"].currentData()
|
||||
right = self.camera_combos["right"].currentData()
|
||||
if left is None or right is None:
|
||||
return
|
||||
if self.viewer.apply_camera_sources(str(left), str(right)):
|
||||
self.accept()
|
||||
|
||||
|
||||
class VisualSettingsDialog(QDialog):
|
||||
CONFIG_GROUPS = [
|
||||
(
|
||||
@@ -2595,6 +2748,21 @@ class VisualSettingsDialog(QDialog):
|
||||
return scroll
|
||||
|
||||
def _make_control(self, key, value):
|
||||
if key == "port":
|
||||
container = QWidget()
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(6)
|
||||
control = QComboBox()
|
||||
control.setEditable(True)
|
||||
control.addItem(str(value), str(value))
|
||||
control.setToolTip("port")
|
||||
search_button = TopIconButton("search", "搜索可用串口")
|
||||
search_button.clicked.connect(self._search_serial_ports)
|
||||
layout.addWidget(control, 1)
|
||||
layout.addWidget(search_button)
|
||||
self.controls[key] = control
|
||||
return container
|
||||
if isinstance(value, bool):
|
||||
control = QCheckBox()
|
||||
control.setChecked(value)
|
||||
@@ -2612,6 +2780,11 @@ class VisualSettingsDialog(QDialog):
|
||||
|
||||
def _control_value(self, key, original):
|
||||
control = self.controls[key]
|
||||
if isinstance(control, QComboBox):
|
||||
selected = control.currentData()
|
||||
if control.currentIndex() >= 0 and selected is not None:
|
||||
return str(selected)
|
||||
return control.currentText().strip()
|
||||
if isinstance(original, bool):
|
||||
return bool(control.isChecked())
|
||||
if isinstance(original, (dict, list)):
|
||||
@@ -2636,11 +2809,50 @@ class VisualSettingsDialog(QDialog):
|
||||
return
|
||||
if isinstance(control, QCheckBox):
|
||||
control.setChecked(bool(value))
|
||||
elif isinstance(control, QComboBox):
|
||||
text = "" if value is None else str(value)
|
||||
index = control.findData(text)
|
||||
if index < 0:
|
||||
control.addItem(text, text)
|
||||
index = control.count() - 1
|
||||
control.setCurrentIndex(index)
|
||||
elif isinstance(control, QTextEdit):
|
||||
control.setPlainText(json.dumps(value, ensure_ascii=False, indent=2))
|
||||
elif isinstance(control, QLineEdit):
|
||||
control.setText("null" if value is None else str(value))
|
||||
|
||||
def _search_serial_ports(self):
|
||||
control = self.controls.get("port")
|
||||
if not isinstance(control, QComboBox):
|
||||
return
|
||||
current = self._control_value("port", self.config_data.get("port", ""))
|
||||
if serial_list_ports is None:
|
||||
QMessageBox.warning(self, "搜索失败", "PySerial 串口枚举模块未安装。")
|
||||
return
|
||||
try:
|
||||
ports = list(serial_list_ports.comports())
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "搜索失败", f"无法枚举串口:\n{exc}")
|
||||
return
|
||||
|
||||
control.clear()
|
||||
selected_index = -1
|
||||
for port in ports:
|
||||
device = str(port.device)
|
||||
description = str(port.description or "未知设备")
|
||||
control.addItem(f"{device} [{description}]", device)
|
||||
if device == current:
|
||||
selected_index = control.count() - 1
|
||||
if current and selected_index < 0:
|
||||
control.addItem(f"{current} [当前配置]", current)
|
||||
selected_index = control.count() - 1
|
||||
if selected_index >= 0:
|
||||
control.setCurrentIndex(selected_index)
|
||||
if not ports:
|
||||
QMessageBox.information(self, "搜索串口", "未发现可用串口,已保留当前配置。")
|
||||
return
|
||||
control.showPopup()
|
||||
|
||||
def _reload(self):
|
||||
self._load_config()
|
||||
for key, value in self.config_data.items():
|
||||
@@ -2665,12 +2877,26 @@ class VisualSettingsDialog(QDialog):
|
||||
QMessageBox.warning(self, "保存失败", f"无法写入配置文件:\n{exc}")
|
||||
return
|
||||
|
||||
try:
|
||||
layout_path = self.viewer.save_current_layout()
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "画面保存失败", f"参数已写入,但画面旋转无法保存:\n{exc}")
|
||||
return
|
||||
|
||||
self.config_data = updated
|
||||
QMessageBox.information(self, "已保存", "配置已写入 gripper.json。重启控制程序后完整生效。")
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"已保存",
|
||||
f"配置已写入 gripper.json,画面旋转已写入:\n{layout_path}\n重启控制程序后完整生效。",
|
||||
)
|
||||
|
||||
def _adjust(self, side, action):
|
||||
self.viewer.adjust_visual_transform(side, action)
|
||||
self._refresh_labels()
|
||||
try:
|
||||
self.viewer.save_current_layout()
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "保存失败", f"画面已调整,但无法保存旋转配置:\n{exc}")
|
||||
|
||||
def _refresh_labels(self):
|
||||
for side, title in (("left", "左侧光流"), ("right", "右侧光流")):
|
||||
@@ -2726,7 +2952,7 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
if pg is None:
|
||||
raise RuntimeError(f"pyqtgraph import failed: {_PYQTGRAPH_IMPORT_ERROR}")
|
||||
|
||||
self.setWindowTitle("Gripper Demo 02")
|
||||
self.setWindowTitle("Orisys 夹爪")
|
||||
self.setWindowIcon(_load_company_icon(64))
|
||||
self.setStyleSheet(STYLE)
|
||||
self.controller = None
|
||||
@@ -2745,6 +2971,7 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
self.argv = argv
|
||||
self.layout_path = GUI_LAYOUT_PATH
|
||||
self.visual_settings_dialog = None
|
||||
self.camera_discovery_dialog = None
|
||||
self._pending_runtime_speed = None
|
||||
self._pending_position_target = None
|
||||
self._pending_position_since = 0.0
|
||||
@@ -2898,6 +3125,91 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
config_path = REPO_ROOT / config_path
|
||||
return config_path
|
||||
|
||||
def camera_sources(self):
|
||||
sources = {"left": "", "right": ""}
|
||||
try:
|
||||
with open(self._control_config_path(), "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
sources["left"] = str(data.get("video1", ""))
|
||||
sources["right"] = str(data.get("video2", ""))
|
||||
except Exception:
|
||||
pass
|
||||
return sources
|
||||
|
||||
def open_camera_search(self):
|
||||
existing = self.camera_discovery_dialog
|
||||
if existing is not None and existing.isVisible():
|
||||
existing.raise_()
|
||||
existing.activateWindow()
|
||||
return
|
||||
dialog = CameraDiscoveryDialog(self)
|
||||
self.camera_discovery_dialog = dialog
|
||||
dialog.finished.connect(lambda _result: setattr(self, "camera_discovery_dialog", None))
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
|
||||
def _update_camera_argv(self, config_key, source):
|
||||
args = list(self.argv or [])
|
||||
option_names = {
|
||||
"video1": ("--video1", "-v1"),
|
||||
"video2": ("--video2", "-v2"),
|
||||
}[config_key]
|
||||
for index, arg in enumerate(args):
|
||||
if arg in option_names and index + 1 < len(args):
|
||||
args[index + 1] = source
|
||||
self.argv = args
|
||||
return
|
||||
for option in option_names:
|
||||
if str(arg).startswith(option + "="):
|
||||
args[index] = option + "=" + source
|
||||
self.argv = args
|
||||
return
|
||||
|
||||
def apply_camera_sources(self, left_source, right_source):
|
||||
if left_source == right_source:
|
||||
QMessageBox.warning(self, "摄像头冲突", "左右两侧需要选择不同的摄像头。")
|
||||
return False
|
||||
sources = self.camera_sources()
|
||||
if left_source == sources.get("left") and right_source == sources.get("right"):
|
||||
return True
|
||||
|
||||
config_path = self._control_config_path()
|
||||
temp_path = config_path.with_suffix(config_path.suffix + ".tmp")
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("配置文件根节点不是 JSON 对象")
|
||||
data["video1"] = left_source
|
||||
data["video2"] = right_source
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(temp_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
os.replace(temp_path, config_path)
|
||||
except Exception as exc:
|
||||
try:
|
||||
if temp_path.exists():
|
||||
temp_path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
QMessageBox.warning(self, "保存失败", f"无法保存摄像头配置:\n{exc}")
|
||||
return False
|
||||
|
||||
self._update_camera_argv("video1", left_source)
|
||||
self._update_camera_argv("video2", right_source)
|
||||
if self.visual_settings_dialog is not None:
|
||||
for config_key, source in (("video1", left_source), ("video2", right_source)):
|
||||
self.visual_settings_dialog.config_data[config_key] = source
|
||||
self.visual_settings_dialog._set_control_value(config_key, source)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"双摄像头已切换",
|
||||
f"左侧摄像头: {left_source}\n右侧摄像头: {right_source}\n控制程序将自动重启。",
|
||||
)
|
||||
self.restart_control()
|
||||
return True
|
||||
|
||||
def _load_position_config(self):
|
||||
open_pos = 0
|
||||
close_pos = 9000
|
||||
@@ -3007,6 +3319,8 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
self.right_flow = self.gripper_stage.right_flow
|
||||
self.left_flow.set_transform_label(self.visual_transforms["left"])
|
||||
self.right_flow.set_transform_label(self.visual_transforms["right"])
|
||||
self.left_flow.camera_search_button.clicked.connect(self.open_camera_search)
|
||||
self.right_flow.camera_search_button.clicked.connect(self.open_camera_search)
|
||||
visual_layout.addWidget(self.gripper_stage, 0, 0)
|
||||
visual_layout.setColumnStretch(0, 1)
|
||||
center_splitter.addWidget(visual_panel)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
620677510d421d76418c98366a4b57df7df930cda60e72f4efb214d96b4f05c7 GripperDemo02-ubuntu26.04-x86_64-lite-20260717.tar.gz
|
||||
@@ -0,0 +1 @@
|
||||
042c1f87a8c194239ccbd373ca55425091234e8540f995bc7efe6b2eb74779fa GripperDemo02-windows-x64-py311-venv-20260717.zip
|
||||
@@ -1 +0,0 @@
|
||||
3099df00132a5f515fde42617969397dcf71655df13cd28c196002435ca6cd7b GripperDemo02-windows-x64-lite-20260715.zip
|
||||