串口搜索和摄像头搜索

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
+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