串口搜索和摄像头搜索

This commit is contained in:
cheng
2026-07-23 17:50:39 +08:00
parent b3fe4d976f
commit daa0ec8aec
44 changed files with 4705 additions and 45 deletions
+16
View File
@@ -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 轻量包。
+2 -2
View File
@@ -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)
+367
View File
@@ -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())
+143
View File
@@ -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())
+189
View File
@@ -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())
+22
View File
@@ -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())
+217
View File
@@ -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()
+167
View File
@@ -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()
+22
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

+44
View File
@@ -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
}
}
+28 -28
View File
@@ -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,
+749
View File
@@ -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`。
- [ ] 已重启控制程序加载新参数。
- [ ] 布局已单独执行“保存当前布局”。
- [ ] 最近一次运行日志可以正常生成和定位。
---
本版本为快速初版,后续版本可补充带编号的主界面截图、设备接线图、参数调试示例和现场验收记录表。
+920
View File
@@ -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>
+29
View File
@@ -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
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+81
View File
@@ -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

Binary file not shown.

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

File diff suppressed because it is too large Load Diff
+38
View File
@@ -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}
+49
View File
@@ -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}
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

+2 -2
View File
@@ -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
+162
View File
@@ -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
+4 -4
View File
@@ -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)
+319 -5
View File
@@ -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
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
042c1f87a8c194239ccbd373ca55425091234e8540f995bc7efe6b2eb74779fa GripperDemo02-windows-x64-py311-venv-20260717.zip
-1
View File
@@ -1 +0,0 @@
3099df00132a5f515fde42617969397dcf71655df13cd28c196002435ca6cd7b GripperDemo02-windows-x64-lite-20260715.zip