串口搜索和摄像头搜索
This commit is contained in:
+2
-2
@@ -99,8 +99,8 @@ def validate_lite_sensor_configs():
|
||||
raise SystemExit(f"Cannot validate lite build config '{control_path}': {exc}") from exc
|
||||
|
||||
config_names = {
|
||||
control_config.get("sensor_config1", "./config/ddjx01.json"),
|
||||
control_config.get("sensor_config2", "./config/ddjx01.json"),
|
||||
control_config.get("sensor_config1", "./config/ddjx01_q6a.json"),
|
||||
control_config.get("sensor_config2", "./config/ddjx01_q6a.json"),
|
||||
}
|
||||
for config_name in config_names:
|
||||
config_path = Path(config_name)
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import venv
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
BUILD_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_ROOT = BUILD_DIR.parent
|
||||
DIST_DIR = PROJECT_ROOT / "dist"
|
||||
RELEASE_DIR = PROJECT_ROOT / "release"
|
||||
LOCK_FILE = BUILD_DIR / "requirements-venv-win-x64.txt"
|
||||
RUNTIME_DIRS = (
|
||||
"gripper",
|
||||
"config",
|
||||
"sdk",
|
||||
"crt_ctag2f90d_gripper_visualization",
|
||||
"docs",
|
||||
)
|
||||
RUNTIME_FILES = (
|
||||
"README.md",
|
||||
"requirements.txt",
|
||||
"requirements-dev.txt",
|
||||
)
|
||||
|
||||
|
||||
def run(command, *, cwd=None, env=None, capture_output=False):
|
||||
print("+", subprocess.list2cmdline([str(item) for item in command]))
|
||||
return subprocess.run(
|
||||
[str(item) for item in command],
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=capture_output,
|
||||
)
|
||||
|
||||
|
||||
def require_supported_python():
|
||||
if sys.version_info[:2] != (3, 11):
|
||||
raise SystemExit(
|
||||
f"This bundle requires CPython 3.11; current interpreter is {sys.version.split()[0]}."
|
||||
)
|
||||
if os.name != "nt" or platform.machine().upper() not in {"AMD64", "X86_64"}:
|
||||
raise SystemExit(
|
||||
f"This lock targets Windows x64; current platform is {platform.platform()}."
|
||||
)
|
||||
|
||||
|
||||
def ensure_generated_path(path, parent):
|
||||
resolved = path.resolve()
|
||||
resolved_parent = parent.resolve()
|
||||
if resolved.parent != resolved_parent:
|
||||
raise SystemExit(f"Refusing to replace unexpected path: {resolved}")
|
||||
|
||||
|
||||
def remove_generated_dir(path, parent):
|
||||
ensure_generated_path(path, parent)
|
||||
if path.exists():
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
def copy_runtime_tree(source, target):
|
||||
if not source.exists():
|
||||
return False
|
||||
shutil.copytree(
|
||||
source,
|
||||
target,
|
||||
ignore=shutil.ignore_patterns(
|
||||
"__pycache__",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.tmp",
|
||||
"*.log",
|
||||
".git",
|
||||
".agents",
|
||||
".codex",
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def create_venv(venv_dir):
|
||||
print(f"Creating venv: {venv_dir}")
|
||||
venv.EnvBuilder(
|
||||
system_site_packages=False,
|
||||
clear=True,
|
||||
symlinks=False,
|
||||
with_pip=True,
|
||||
).create(venv_dir)
|
||||
python_exe = venv_dir / "Scripts" / "python.exe"
|
||||
if not python_exe.exists():
|
||||
raise SystemExit(f"venv interpreter was not created: {python_exe}")
|
||||
return python_exe
|
||||
|
||||
|
||||
def install_dependencies(python_exe, orisys_source):
|
||||
run(
|
||||
[
|
||||
python_exe,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"pip==26.1.2",
|
||||
"setuptools==78.1.0",
|
||||
"wheel==0.47.0",
|
||||
]
|
||||
)
|
||||
run(
|
||||
[
|
||||
python_exe,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--requirement",
|
||||
LOCK_FILE,
|
||||
]
|
||||
)
|
||||
run(
|
||||
[
|
||||
python_exe,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--no-deps",
|
||||
orisys_source,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def copy_application(bundle_dir):
|
||||
for name in RUNTIME_DIRS:
|
||||
source = PROJECT_ROOT / name
|
||||
if not copy_runtime_tree(source, bundle_dir / name):
|
||||
print(f"Warning: runtime directory does not exist: {source}")
|
||||
|
||||
for name in RUNTIME_FILES:
|
||||
source = PROJECT_ROOT / name
|
||||
if source.exists():
|
||||
shutil.copy2(source, bundle_dir / name)
|
||||
|
||||
shutil.copy2(LOCK_FILE, bundle_dir / "requirements-venv-lock.txt")
|
||||
|
||||
|
||||
def write_launchers(bundle_dir):
|
||||
launcher = bundle_dir / "start_gripper.cmd"
|
||||
launcher.write_text(
|
||||
"@echo off\n"
|
||||
"setlocal\n"
|
||||
"cd /d \"%~dp0\"\n"
|
||||
"set PYTHONUTF8=1\n"
|
||||
"set PYTHONNOUSERSITE=1\n"
|
||||
"if not exist logs mkdir logs\n"
|
||||
"\".venv\\Scripts\\python.exe\" -m gripper.visualizer --windowed %* "
|
||||
">> \"logs\\venv.log\" 2>&1\n"
|
||||
"if errorlevel 1 (\n"
|
||||
" echo Start failed. See logs\\venv.log\n"
|
||||
" pause\n"
|
||||
")\n",
|
||||
encoding="ascii",
|
||||
newline="\r\n",
|
||||
)
|
||||
|
||||
readme = bundle_dir / "README-VENV.txt"
|
||||
readme.write_text(
|
||||
"Gripper Demo 02 - Windows x64 Python 3.11 venv bundle\n"
|
||||
"========================================================\n\n"
|
||||
"1. Extract the complete archive to a writable directory.\n"
|
||||
"2. Edit config\\gripper.json: set video1, video2 and port.\n"
|
||||
"3. Run start_gripper.cmd. Add --dry-run for a serial-free test.\n"
|
||||
"4. Runtime output is appended to logs\\venv.log.\n\n"
|
||||
"This package uses a standard Python venv created with --copies. "
|
||||
"It is intended for the current machine or another Windows x64 "
|
||||
"machine with CPython 3.11 installed. Standard venv directories "
|
||||
"are not a fully standalone Python distribution. If the base "
|
||||
"interpreter differs, recreate .venv with Python 3.11 and install "
|
||||
"requirements-venv-lock.txt, then install the Orisys SDK.\n\n"
|
||||
"CUDA, Torch/RAFT, CuPy and Open3D are not included. The default "
|
||||
"application uses OpenCV DIS optical flow and the CPU Poisson backend.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def verify_bundle(python_exe, bundle_dir):
|
||||
verify_code = """
|
||||
import json
|
||||
import platform
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
import llvmlite
|
||||
import minimalmodbus
|
||||
import numba
|
||||
import numpy
|
||||
import OpenGL
|
||||
import orisys
|
||||
import pyqtgraph
|
||||
import scipy
|
||||
import serial
|
||||
from PyQt5 import QtCore
|
||||
from gripper.config.settings import Demo02Config
|
||||
from gripper.ui.visualizer import GripperDemo02Viewer
|
||||
|
||||
config = Demo02Config.from_args([
|
||||
"--control-config", "config/gripper.json", "--dry-run"
|
||||
])
|
||||
print(json.dumps({
|
||||
"python": sys.version.split()[0],
|
||||
"architecture": platform.machine(),
|
||||
"numpy": numpy.__version__,
|
||||
"opencv": cv2.__version__,
|
||||
"numba": numba.__version__,
|
||||
"llvmlite": llvmlite.__version__,
|
||||
"scipy": scipy.__version__,
|
||||
"pyqt": QtCore.PYQT_VERSION_STR,
|
||||
"qt": QtCore.QT_VERSION_STR,
|
||||
"pyqtgraph": pyqtgraph.__version__,
|
||||
"pyopengl": OpenGL.__version__,
|
||||
"orisys": getattr(orisys, "__version__", "unknown"),
|
||||
"dry_run": config.dry_run,
|
||||
}, indent=2))
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env["PYTHONNOUSERSITE"] = "1"
|
||||
result = run(
|
||||
[python_exe, "-c", verify_code],
|
||||
cwd=bundle_dir,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
)
|
||||
print(result.stdout.rstrip())
|
||||
(bundle_dir / "environment.json").write_text(
|
||||
result.stdout.strip() + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
freeze = run(
|
||||
[python_exe, "-m", "pip", "freeze", "--all"],
|
||||
cwd=bundle_dir,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
)
|
||||
(bundle_dir / "pip-freeze.txt").write_text(
|
||||
freeze.stdout,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def remove_bytecode(root):
|
||||
for cache_dir in sorted(root.rglob("__pycache__"), reverse=True):
|
||||
shutil.rmtree(cache_dir, ignore_errors=True)
|
||||
for pattern in ("*.pyc", "*.pyo"):
|
||||
for path in root.rglob(pattern):
|
||||
try:
|
||||
path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def directory_size(path):
|
||||
return sum(item.stat().st_size for item in path.rglob("*") if item.is_file())
|
||||
|
||||
|
||||
def create_zip(bundle_dir, archive_path):
|
||||
if archive_path.exists():
|
||||
archive_path.unlink()
|
||||
print(f"Creating archive: {archive_path}")
|
||||
with zipfile.ZipFile(
|
||||
archive_path,
|
||||
"w",
|
||||
compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=6,
|
||||
allowZip64=True,
|
||||
) as archive:
|
||||
for path in sorted(bundle_dir.rglob("*")):
|
||||
if path.is_file():
|
||||
archive.write(path, Path(bundle_dir.name) / path.relative_to(bundle_dir))
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
default="GripperDemo02Venv",
|
||||
help="Staging directory name under dist.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--orisys-source",
|
||||
type=Path,
|
||||
default=PROJECT_ROOT.parent / "orisys-sdk",
|
||||
help="Path to the private Orisys SDK source tree.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-archive",
|
||||
action="store_true",
|
||||
help="Create and verify the directory without writing a release zip.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
require_supported_python()
|
||||
if not LOCK_FILE.exists():
|
||||
raise SystemExit(f"Dependency lock does not exist: {LOCK_FILE}")
|
||||
orisys_source = args.orisys_source.resolve()
|
||||
if not (orisys_source / "pyproject.toml").exists():
|
||||
raise SystemExit(f"Orisys SDK source was not found: {orisys_source}")
|
||||
|
||||
DIST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
RELEASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
bundle_dir = DIST_DIR / args.name
|
||||
remove_generated_dir(bundle_dir, DIST_DIR)
|
||||
bundle_dir.mkdir(parents=True)
|
||||
|
||||
venv_dir = bundle_dir / ".venv"
|
||||
python_exe = create_venv(venv_dir)
|
||||
install_dependencies(python_exe, orisys_source)
|
||||
copy_application(bundle_dir)
|
||||
write_launchers(bundle_dir)
|
||||
verify_bundle(python_exe, bundle_dir)
|
||||
remove_bytecode(bundle_dir)
|
||||
|
||||
unpacked_mb = directory_size(bundle_dir) / (1024 * 1024)
|
||||
print(f"Unpacked bundle size: {unpacked_mb:.1f} MiB")
|
||||
|
||||
if args.no_archive:
|
||||
print(f"Bundle ready: {bundle_dir}")
|
||||
return 0
|
||||
|
||||
stamp = datetime.now().strftime("%Y%m%d")
|
||||
archive_path = RELEASE_DIR / f"GripperDemo02-windows-x64-py311-venv-{stamp}.zip"
|
||||
create_zip(bundle_dir, archive_path)
|
||||
digest = sha256_file(archive_path)
|
||||
checksum_path = archive_path.with_suffix(archive_path.suffix + ".sha256")
|
||||
checksum_path.write_text(f"{digest} {archive_path.name}\n", encoding="ascii")
|
||||
archive_mb = archive_path.stat().st_size / (1024 * 1024)
|
||||
|
||||
report = {
|
||||
"bundle": str(bundle_dir),
|
||||
"archive": str(archive_path),
|
||||
"checksum": digest,
|
||||
"unpacked_mib": round(unpacked_mb, 1),
|
||||
"archive_mib": round(archive_mb, 1),
|
||||
}
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Capture product-manual screenshots using the real visualizer entry path.
|
||||
|
||||
Equivalent to: python -m gripper.visualizer
|
||||
(fullscreen by default, user config/gripper.json, start_control)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
OUT = REPO / "docs" / "images" / "manual"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
sys.path.insert(0, str(REPO))
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QApplication, QFrame
|
||||
|
||||
from gripper.ui.visualizer import GripperDemo02Viewer, _load_company_icon
|
||||
|
||||
|
||||
def process(app, ms=200):
|
||||
end = time.monotonic() + ms / 1000.0
|
||||
while time.monotonic() < end:
|
||||
app.processEvents()
|
||||
time.sleep(0.02)
|
||||
|
||||
|
||||
def save(widget, name: str):
|
||||
path = OUT / name
|
||||
pix = widget.grab()
|
||||
if pix.isNull() or not pix.save(str(path), "PNG"):
|
||||
raise RuntimeError(f"save failed: {name}")
|
||||
print(f"saved {path.relative_to(REPO)}")
|
||||
return path
|
||||
|
||||
|
||||
def wait_ready(app, viewer, timeout=60.0):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
process(app, 80)
|
||||
with viewer.status_lock:
|
||||
st = viewer.latest_status
|
||||
if not st:
|
||||
continue
|
||||
left = st.get("left_sample") or {}
|
||||
right = st.get("right_sample") or {}
|
||||
if left and right and st.get("state") != "error":
|
||||
print(
|
||||
f"ready state={st.get('state')} action={st.get('action')} "
|
||||
f"fps=({(left or {}).get('fps')}, {(right or {}).get('fps')})"
|
||||
)
|
||||
return st
|
||||
if st.get("state") == "error":
|
||||
raise RuntimeError(f"controller error: {st.get('action')}")
|
||||
raise TimeoutError("sensors not ready")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
app = QApplication(sys.argv[:1])
|
||||
app.setWindowIcon(_load_company_icon(64))
|
||||
|
||||
# Same path as gripper.visualizer.main (no --windowed → fullscreen)
|
||||
viewer = GripperDemo02Viewer([])
|
||||
restored = viewer.restore_saved_layout(force_windowed=False)
|
||||
if not restored:
|
||||
viewer.showFullScreen()
|
||||
viewer.start_control()
|
||||
|
||||
print("waiting for live sensors...")
|
||||
wait_ready(app, viewer, timeout=75.0)
|
||||
# let curves accumulate
|
||||
process(app, 4000)
|
||||
|
||||
save(viewer, "01-main-overview.png")
|
||||
|
||||
viewer.left_flow.raise_()
|
||||
viewer.right_flow.raise_()
|
||||
process(app, 200)
|
||||
save(viewer, "02-flow-windows.png")
|
||||
|
||||
viewer.plot_panel.raise_()
|
||||
process(app, 200)
|
||||
save(viewer, "03-force-plots.png")
|
||||
|
||||
viewer.gripper_control_panel.raise_()
|
||||
process(app, 200)
|
||||
save(viewer, "04-gripper-control-smart.png")
|
||||
|
||||
viewer.set_control_mode("manual")
|
||||
process(app, 300)
|
||||
save(viewer, "05-gripper-control-manual.png")
|
||||
viewer.set_control_mode("smart")
|
||||
process(app, 200)
|
||||
|
||||
viewer.sensor_control_panel.raise_()
|
||||
process(app, 200)
|
||||
save(viewer, "06-sensor-control.png")
|
||||
|
||||
top = viewer.findChild(QFrame, "TopBar")
|
||||
if top is not None:
|
||||
save(top, "07-top-bar.png")
|
||||
|
||||
viewer.open_visual_settings()
|
||||
process(app, 500)
|
||||
dialog = viewer.visual_settings_dialog
|
||||
save(dialog, "08-settings-visual.png")
|
||||
|
||||
dialog.nav.setCurrentRow(1)
|
||||
process(app, 200)
|
||||
save(dialog, "09-settings-layout.png")
|
||||
|
||||
for row in range(dialog.nav.count()):
|
||||
title = dialog.nav.item(row).text()
|
||||
if title == "串口 / 设备":
|
||||
dialog.nav.setCurrentRow(row)
|
||||
process(app, 200)
|
||||
save(dialog, "11-settings-serial.png")
|
||||
elif title == "触发 / 夹持":
|
||||
dialog.nav.setCurrentRow(row)
|
||||
process(app, 200)
|
||||
save(dialog, "10-settings-trigger.png")
|
||||
|
||||
dialog.close()
|
||||
process(app, 300)
|
||||
# live hold_check may not be active; still save current live frame
|
||||
save(viewer, "12-hold-check-state.png")
|
||||
|
||||
print("stopping...")
|
||||
viewer.stop_control()
|
||||
process(app, 2000)
|
||||
viewer.close()
|
||||
app.processEvents()
|
||||
print(f"done → {OUT}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Capture live Gripper Demo 02 window using screen mouse clicks + PrintWindow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
import time
|
||||
from ctypes import wintypes
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
OUT = REPO / "docs" / "images" / "manual"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
gdi32 = ctypes.windll.gdi32
|
||||
user32.SetProcessDPIAware()
|
||||
PW_RENDERFULLCONTENT = 0x00000002
|
||||
|
||||
|
||||
class RECT(ctypes.Structure):
|
||||
_fields_ = [("left", wintypes.LONG), ("top", wintypes.LONG),
|
||||
("right", wintypes.LONG), ("bottom", wintypes.LONG)]
|
||||
|
||||
|
||||
class POINT(ctypes.Structure):
|
||||
_fields_ = [("x", wintypes.LONG), ("y", wintypes.LONG)]
|
||||
|
||||
|
||||
def find_hwnd(title="Gripper Demo 02"):
|
||||
found = []
|
||||
|
||||
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||||
def cb(hwnd, _):
|
||||
if not user32.IsWindowVisible(hwnd):
|
||||
return True
|
||||
n = user32.GetWindowTextLengthW(hwnd)
|
||||
if n <= 0:
|
||||
return True
|
||||
buf = ctypes.create_unicode_buffer(n + 1)
|
||||
user32.GetWindowTextW(hwnd, buf, n + 1)
|
||||
if title in buf.value:
|
||||
found.append(hwnd)
|
||||
return True
|
||||
|
||||
user32.EnumWindows(cb, 0)
|
||||
return found[0] if found else None
|
||||
|
||||
|
||||
def capture(hwnd, path: Path):
|
||||
rect = RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rect))
|
||||
w, h = rect.right - rect.left, rect.bottom - rect.top
|
||||
hdc = user32.GetWindowDC(hwnd)
|
||||
mem = gdi32.CreateCompatibleDC(hdc)
|
||||
bmp = gdi32.CreateCompatibleBitmap(hdc, w, h)
|
||||
old = gdi32.SelectObject(mem, bmp)
|
||||
if not user32.PrintWindow(hwnd, mem, PW_RENDERFULLCONTENT):
|
||||
screen = user32.GetDC(0)
|
||||
gdi32.BitBlt(mem, 0, 0, w, h, screen, rect.left, rect.top, 0x00CC0020)
|
||||
user32.ReleaseDC(0, screen)
|
||||
|
||||
class BIH(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("biSize", wintypes.DWORD), ("biWidth", wintypes.LONG),
|
||||
("biHeight", wintypes.LONG), ("biPlanes", wintypes.WORD),
|
||||
("biBitCount", wintypes.WORD), ("biCompression", wintypes.DWORD),
|
||||
("biSizeImage", wintypes.DWORD), ("biXPelsPerMeter", wintypes.LONG),
|
||||
("biYPelsPerMeter", wintypes.LONG), ("biClrUsed", wintypes.DWORD),
|
||||
("biClrImportant", wintypes.DWORD),
|
||||
]
|
||||
|
||||
class BI(ctypes.Structure):
|
||||
_fields_ = [("bmiHeader", BIH), ("bmiColors", wintypes.DWORD * 3)]
|
||||
|
||||
bi = BI()
|
||||
bi.bmiHeader.biSize = ctypes.sizeof(BIH)
|
||||
bi.bmiHeader.biWidth = w
|
||||
bi.bmiHeader.biHeight = -h
|
||||
bi.bmiHeader.biPlanes = 1
|
||||
bi.bmiHeader.biBitCount = 32
|
||||
buf = (ctypes.c_ubyte * (w * h * 4))()
|
||||
gdi32.GetDIBits(mem, bmp, 0, h, buf, ctypes.byref(bi), 0)
|
||||
gdi32.SelectObject(mem, old)
|
||||
gdi32.DeleteObject(bmp)
|
||||
gdi32.DeleteDC(mem)
|
||||
user32.ReleaseDC(hwnd, hdc)
|
||||
Image.frombuffer("RGBA", (w, h), bytes(buf), "raw", "BGRA", 0, 1).convert("RGB").save(path)
|
||||
print(f"saved {path.name} ({w}x{h})")
|
||||
return w, h
|
||||
|
||||
|
||||
def click_screen(x, y):
|
||||
user32.SetCursorPos(int(x), int(y))
|
||||
time.sleep(0.05)
|
||||
user32.mouse_event(0x0002, 0, 0, 0, 0) # LEFTDOWN
|
||||
time.sleep(0.04)
|
||||
user32.mouse_event(0x0004, 0, 0, 0, 0) # LEFTUP
|
||||
|
||||
|
||||
def main():
|
||||
hwnd = find_hwnd()
|
||||
if not hwnd:
|
||||
print("window not found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
||||
user32.SetForegroundWindow(hwnd)
|
||||
time.sleep(0.5)
|
||||
|
||||
rect = RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rect))
|
||||
left, top = rect.left, rect.top
|
||||
w, h = rect.right - rect.left, rect.bottom - rect.top
|
||||
print(f"window rect=({left},{top}) {w}x{h}")
|
||||
|
||||
# 1) main overview
|
||||
capture(hwnd, OUT / "01-main-overview.png")
|
||||
full = Image.open(OUT / "01-main-overview.png")
|
||||
|
||||
# panel crops from live layout
|
||||
crops = {
|
||||
"02-flow-windows.png": (0, int(h * 0.06), w, int(h * 0.55)),
|
||||
"03-force-plots.png": (int(w * 0.55), int(h * 0.08), w - 8, int(h * 0.72)),
|
||||
"04-gripper-control-smart.png": (8, int(h * 0.62), int(w * 0.32), h - 8),
|
||||
"06-sensor-control.png": (int(w * 0.68), int(h * 0.68), w - 8, h - 8),
|
||||
"07-top-bar.png": (0, 0, w, max(70, int(h * 0.065))),
|
||||
"12-hold-check-state.png": (0, 0, w, h),
|
||||
}
|
||||
for name, box in crops.items():
|
||||
full.crop(box).save(OUT / name)
|
||||
print(f"saved {name}")
|
||||
|
||||
# 2) switch to manual mode — button near bottom-left of gripper panel
|
||||
# "手动模式" is left of "智能模式" in gripper control panel
|
||||
manual_x = left + int(w * 0.06)
|
||||
manual_y = top + int(h * 0.68)
|
||||
print(f"click manual mode @ ({manual_x},{manual_y})")
|
||||
click_screen(manual_x, manual_y)
|
||||
time.sleep(0.6)
|
||||
capture(hwnd, OUT / "05-gripper-control-manual.png")
|
||||
|
||||
# switch back to smart
|
||||
smart_x = left + int(w * 0.14)
|
||||
smart_y = top + int(h * 0.68)
|
||||
click_screen(smart_x, smart_y)
|
||||
time.sleep(0.4)
|
||||
|
||||
# 3) open settings — measured from top-bar: first icon ~x=1640
|
||||
settings_x = left + 1640
|
||||
settings_y = top + 32
|
||||
print(f"click settings @ ({settings_x},{settings_y})")
|
||||
click_screen(settings_x, settings_y)
|
||||
time.sleep(1.5)
|
||||
capture(hwnd, OUT / "08-settings-visual.png")
|
||||
|
||||
# embedded settings panel (~72% of window, centered)
|
||||
# nav list is left column inside the panel
|
||||
dlg_left = left + int(w * 0.14)
|
||||
dlg_top = top + int(h * 0.11)
|
||||
nav_x = dlg_left + 90
|
||||
# nav rows: 画面(0), 布局(1), 相机(2), 串口(3), ... 触发(7)
|
||||
row_y0 = dlg_top + 95
|
||||
row_h = 34
|
||||
|
||||
click_screen(nav_x, row_y0 + 1 * row_h) # 布局
|
||||
time.sleep(0.6)
|
||||
capture(hwnd, OUT / "09-settings-layout.png")
|
||||
|
||||
click_screen(nav_x, row_y0 + 3 * row_h) # 串口 / 设备
|
||||
time.sleep(0.6)
|
||||
capture(hwnd, OUT / "11-settings-serial.png")
|
||||
|
||||
click_screen(nav_x, row_y0 + 7 * row_h) # 触发 / 夹持
|
||||
time.sleep(0.6)
|
||||
capture(hwnd, OUT / "10-settings-trigger.png")
|
||||
|
||||
# close settings panel (X on panel header)
|
||||
click_screen(left + int(w * 0.14) + int(w * 0.72) - 30, dlg_top + 28)
|
||||
time.sleep(0.3)
|
||||
|
||||
print("done")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Generate manual diagrams (SVG). Fast path — no Docker / LaTeX."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
GENERATOR = REPO / "build" / "generate_manual_diagrams.py"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("Generating manual diagrams via generate_manual_diagrams.py …")
|
||||
subprocess.run([sys.executable, str(GENERATOR)], check=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,217 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Generate manual SVG diagrams with correct UTF-8 encoding."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[1] / "docs" / "assets" / "diagrams"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
SYSTEM_ARCH = r'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 520" role="img">
|
||||
<defs>
|
||||
<linearGradient id="sensorGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#dbeafe"/><stop offset="100%" stop-color="#bfdbfe"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="procGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e0e7ff"/><stop offset="100%" stop-color="#c7d2fe"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="ctrlGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fef3c7"/><stop offset="100%" stop-color="#fde68a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="guiGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#d1fae5"/><stop offset="100%" stop-color="#a7f3d0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hwGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f1f5f9"/><stop offset="100%" stop-color="#e2e8f0"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
|
||||
<path d="M0,0 L8,4 L0,8 Z" fill="#475569"/>
|
||||
</marker>
|
||||
<filter id="shadow" x="-4%" y="-4%" width="108%" height="112%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#0f172a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<style>
|
||||
.t1{font:700 15px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#0f172a}
|
||||
.t2{font:400 12px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#475569}
|
||||
.tl{font:700 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#64748b;letter-spacing:.08em}
|
||||
.te{font:500 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#334155}
|
||||
.tp{font:600 10px Consolas,monospace;fill:#1e40af}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect x="24" y="52" width="832" height="108" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
|
||||
<text x="40" y="72" class="tl">感知层</text>
|
||||
<rect x="24" y="176" width="832" height="148" rx="8" fill="#fafafa" stroke="#e2e8f0"/>
|
||||
<text x="40" y="196" class="tl">软件处理层</text>
|
||||
<rect x="24" y="340" width="832" height="108" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
|
||||
<text x="40" y="360" class="tl">执行与呈现层</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="56" y="88" width="168" height="56" rx="8" fill="url(#sensorGrad)" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<text x="140" y="112" text-anchor="middle" class="t1">左触觉传感器</text>
|
||||
<text x="140" y="130" text-anchor="middle" class="t2">USB 摄像头 + 敏感面</text>
|
||||
</g>
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="656" y="88" width="168" height="56" rx="8" fill="url(#sensorGrad)" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<text x="740" y="112" text-anchor="middle" class="t1">右触觉传感器</text>
|
||||
<text x="740" y="130" text-anchor="middle" class="t2">USB 摄像头 + 敏感面</text>
|
||||
</g>
|
||||
|
||||
<path d="M224 116 H340" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<path d="M656 116 H540" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="440" y="108" text-anchor="middle" class="te">视频帧采集</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="300" y="212" width="280" height="76" rx="10" fill="url(#procGrad)" stroke="#6366f1" stroke-width="1.5"/>
|
||||
<text x="440" y="240" text-anchor="middle" class="t1">触觉力计算 / 光流处理</text>
|
||||
<text x="440" y="260" text-anchor="middle" class="t2">法向力 · 切向力 · 光流/幅值图像</text>
|
||||
<text x="440" y="276" text-anchor="middle" class="t2">Orisys SDK · OpenCV DIS</text>
|
||||
</g>
|
||||
|
||||
<path d="M140 144 V188 H300" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<path d="M740 144 V188 H580" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="56" y="220" width="200" height="60" rx="8" fill="url(#ctrlGrad)" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="156" y="246" text-anchor="middle" class="t1">智能夹持状态机</text>
|
||||
<text x="156" y="264" text-anchor="middle" class="t2">触发 · 闭合 · 加力 · 释放</text>
|
||||
</g>
|
||||
|
||||
<path d="M300 250 H256" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="278" y="242" text-anchor="middle" class="te">力信号</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="624" y="220" width="200" height="60" rx="8" fill="url(#hwGrad)" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="724" y="246" text-anchor="middle" class="t1">夹爪控制器</text>
|
||||
<text x="724" y="264" text-anchor="middle" class="t2">Modbus RTU 从站</text>
|
||||
</g>
|
||||
|
||||
<path d="M256 250 H420" fill="none" stroke="#64748b" stroke-width="1.5"/>
|
||||
<path d="M420 250 H624" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="440" y="242" text-anchor="middle" class="tp">Modbus RTU / 串口</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="624" y="368" width="200" height="56" rx="8" fill="url(#hwGrad)" stroke="#475569" stroke-width="1.5"/>
|
||||
<text x="724" y="392" text-anchor="middle" class="t1">智能夹爪(执行机构)</text>
|
||||
<text x="724" y="410" text-anchor="middle" class="t2">位置 · 力度 · 开/合/保持</text>
|
||||
</g>
|
||||
<path d="M724 280 V368" fill="none" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="738" y="328" class="te">驱动</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="240" y="356" width="280" height="80" rx="10" fill="url(#guiGrad)" stroke="#059669" stroke-width="1.5"/>
|
||||
<text x="380" y="384" text-anchor="middle" class="t1">GUI 监控界面</text>
|
||||
<text x="380" y="404" text-anchor="middle" class="t2">3D 模型 · 光流画面 · 力曲线 · 状态</text>
|
||||
<text x="380" y="422" text-anchor="middle" class="t2">PyQt5 · PyOpenGL · pyqtgraph</text>
|
||||
</g>
|
||||
|
||||
<path d="M156 280 V320 H380 V356" fill="none" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="168" y="312" class="te">状态 / 动作</text>
|
||||
<path d="M440 288 V356" fill="none" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="452" y="328" class="te">图像 / 力值</text>
|
||||
|
||||
<path d="M624 396 H520 V250 H256" fill="none" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="5 4" marker-end="url(#arrow)"/>
|
||||
<text x="572" y="388" text-anchor="middle" class="te">位置反馈</text>
|
||||
|
||||
<g transform="translate(56,468)">
|
||||
<line x1="0" y1="8" x2="28" y2="8" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="36" y="12" class="t2">数据 / 控制流</text>
|
||||
<line x1="140" y1="8" x2="168" y2="8" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="5 4" marker-end="url(#arrow)"/>
|
||||
<text x="176" y="12" class="t2">反馈</text>
|
||||
</g>
|
||||
</svg>
|
||||
'''
|
||||
|
||||
GRIP_FLOW = r'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 920 324" role="img">
|
||||
<defs>
|
||||
<linearGradient id="gIdle" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#dbeafe"/><stop offset="100%" stop-color="#bfdbfe"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gAct" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fef3c7"/><stop offset="100%" stop-color="#fde68a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gHold" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fce7f3"/><stop offset="100%" stop-color="#fbcfe8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gRec" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#d1fae5"/><stop offset="100%" stop-color="#a7f3d0"/>
|
||||
</linearGradient>
|
||||
<marker id="arr" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
|
||||
<path d="M0,0 L8,4 L0,8 Z" fill="#475569"/>
|
||||
</marker>
|
||||
<filter id="sh" x="-6%" y="-8%" width="112%" height="120%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2.5" flood-color="#0f172a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<style>
|
||||
.cn{font:700 14px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#0f172a}
|
||||
.en{font:600 11px Consolas,monospace;fill:#475569}
|
||||
.sub{font:400 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#64748b}
|
||||
.ed{font:500 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#334155}
|
||||
.nt{font:400 11px "Microsoft YaHei","Noto Sans SC",sans-serif;fill:#64748b}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<g filter="url(#sh)"><rect x="24" y="40" width="148" height="72" rx="10" fill="url(#gIdle)" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="98" y="66" text-anchor="middle" class="cn">等待触发</text>
|
||||
<text x="98" y="84" text-anchor="middle" class="en">open_wait</text>
|
||||
<text x="98" y="100" text-anchor="middle" class="sub">已就绪 armed</text></g>
|
||||
|
||||
<path d="M172 76 H208" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="190" y="68" text-anchor="middle" class="ed">侧边触发</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="208" y="40" width="148" height="72" rx="10" fill="url(#gAct)" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="282" y="66" text-anchor="middle" class="cn">闭合中</text>
|
||||
<text x="282" y="84" text-anchor="middle" class="en">closing</text>
|
||||
<text x="282" y="100" text-anchor="middle" class="sub">寻找物体</text></g>
|
||||
|
||||
<path d="M356 76 H392" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="374" y="68" text-anchor="middle" class="ed">双侧接触</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="392" y="40" width="148" height="72" rx="10" fill="url(#gAct)" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="466" y="66" text-anchor="middle" class="cn">夹持加力</text>
|
||||
<text x="466" y="84" text-anchor="middle" class="en">gripping</text>
|
||||
<text x="466" y="100" text-anchor="middle" class="sub">低力 → 基础力</text></g>
|
||||
|
||||
<path d="M540 76 H576" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="558" y="68" text-anchor="middle" class="ed">到达保持力</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="576" y="40" width="148" height="72" rx="10" fill="url(#gHold)" stroke="#db2777" stroke-width="1.5"/>
|
||||
<text x="650" y="66" text-anchor="middle" class="cn">夹持检测</text>
|
||||
<text x="650" y="84" text-anchor="middle" class="en">hold_check</text>
|
||||
<text x="650" y="100" text-anchor="middle" class="sub">自适应补力 / 释放检测</text></g>
|
||||
|
||||
<path d="M724 76 H760" stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<text x="742" y="68" text-anchor="middle" class="ed">取物释放</text>
|
||||
|
||||
<g filter="url(#sh)"><rect x="760" y="40" width="136" height="72" rx="10" fill="url(#gRec)" stroke="#059669" stroke-width="1.5"/>
|
||||
<text x="828" y="66" text-anchor="middle" class="cn">张开恢复</text>
|
||||
<text x="828" y="84" text-anchor="middle" class="en">open_recover</text>
|
||||
<text x="828" y="100" text-anchor="middle" class="sub">位置 / 力恢复</text></g>
|
||||
|
||||
<path d="M828 112 V204 H98 V112" fill="none" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arr)"/>
|
||||
<rect x="340" y="188" width="240" height="24" rx="4" fill="#eff6ff" stroke="#bfdbfe"/>
|
||||
<text x="460" y="204" text-anchor="middle" class="ed" fill="#1e40af">恢复完成 + 休息时间(rearm)→ 再次就绪</text>
|
||||
|
||||
<path d="M282 112 V152 H828" fill="none" stroke="#dc2626" stroke-width="1.2" stroke-dasharray="5 4"/>
|
||||
<rect x="200" y="128" width="164" height="22" rx="4" fill="#fef2f2" stroke="#fecaca"/>
|
||||
<text x="282" y="143" text-anchor="middle" class="ed" fill="#b91c1c">闭合超时 close_timeout → 张开</text>
|
||||
|
||||
<rect x="24" y="232" width="872" height="76" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
|
||||
<text x="40" y="254" class="cn" font-size="12">阶段说明</text>
|
||||
<text x="40" y="274" class="nt">① 等待触发:夹爪张开,单侧力超阈且为新上升沿时进入闭合。</text>
|
||||
<text x="40" y="294" class="nt">② 闭合~夹持:物体接触后低力保持,分步加力至 hold_force,并按切向负载自适应补力。</text>
|
||||
<text x="40" y="314" class="nt">③ 释放~恢复:力变化超阈则张开;满足恢复条件并经过 rearm_seconds 后,方可再次触发。</text>
|
||||
</svg>
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
(OUT / "system-architecture.svg").write_text(SYSTEM_ARCH, encoding="utf-8")
|
||||
(OUT / "grip-state-flow.svg").write_text(GRIP_FLOW, encoding="utf-8")
|
||||
print("Wrote", OUT / "system-architecture.svg")
|
||||
print("Wrote", OUT / "grip-state-flow.svg")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Render manual diagrams as high-resolution PNG.
|
||||
|
||||
LaTeX/TikZ sources: docs/diagrams/tex/*.tex
|
||||
Compile with: python build/compile_manual_diagrams.py (requires Docker or local XeLaTeX)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
|
||||
from matplotlib import font_manager as fm
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
OUT = REPO / "docs" / "assets" / "diagrams"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
FONT = next(
|
||||
(f.name for f in fm.fontManager.ttflist if "YaHei" in f.name or "Noto Sans CJK" in f.name),
|
||||
"sans-serif",
|
||||
)
|
||||
plt.rcParams.update(
|
||||
{
|
||||
"font.family": FONT,
|
||||
"font.sans-serif": [FONT, "Microsoft YaHei", "SimHei", "DejaVu Sans"],
|
||||
"axes.unicode_minus": False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _box(ax, xy, w, h, title, fc, ec, subtitle=None, fs=9):
|
||||
x, y = xy
|
||||
ax.add_patch(
|
||||
FancyBboxPatch(
|
||||
(x, y),
|
||||
w,
|
||||
h,
|
||||
boxstyle="round,pad=0.02,rounding_size=0.07",
|
||||
linewidth=1.2,
|
||||
edgecolor=ec,
|
||||
facecolor=fc,
|
||||
)
|
||||
)
|
||||
if subtitle:
|
||||
ax.text(x + w / 2, y + h * 0.65, title, ha="center", va="center", fontsize=fs, fontweight="bold")
|
||||
ax.text(x + w / 2, y + h * 0.3, subtitle, ha="center", va="center", fontsize=fs - 1.5, color="#475569")
|
||||
else:
|
||||
ax.text(x + w / 2, y + h / 2, title, ha="center", va="center", fontsize=fs, fontweight="bold")
|
||||
|
||||
|
||||
def _arr(ax, p0, p1, text=None, color="#64748b", dashed=False):
|
||||
style = "Simple,tail_width=0.45,head_width=3.8,head_length=5.5"
|
||||
ax.add_patch(
|
||||
FancyArrowPatch(
|
||||
p0,
|
||||
p1,
|
||||
arrowstyle=style,
|
||||
linewidth=1.0,
|
||||
linestyle="-" if not dashed else (0, (4, 3)),
|
||||
color=color,
|
||||
shrinkA=1,
|
||||
shrinkB=1,
|
||||
connectionstyle="arc3,rad=0.0",
|
||||
)
|
||||
)
|
||||
if text:
|
||||
mx, my = (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2
|
||||
ax.text(mx, my + 0.08, text, ha="center", va="bottom", fontsize=7.5, color="#334155")
|
||||
|
||||
|
||||
def render_system_architecture(path: Path) -> None:
|
||||
fig, ax = plt.subplots(figsize=(9.2, 5.2), dpi=240)
|
||||
ax.set_xlim(0, 9.2)
|
||||
ax.set_ylim(0, 5.2)
|
||||
ax.axis("off")
|
||||
|
||||
layers = [(3.55, 1.05, "感知层"), (2.05, 1.25, "软件处理层"), (0.35, 1.05, "执行与呈现层")]
|
||||
for y, h, name in layers:
|
||||
ax.add_patch(FancyBboxPatch((0.2, y), 8.8, h, boxstyle="round,pad=0.01,rounding_size=0.05", fc="#f8fafc", ec="#d1d5db", lw=0.8))
|
||||
ax.text(0.35, y + h - 0.15, name, fontsize=8, fontweight="bold", color="#64748b")
|
||||
|
||||
_box(ax, (0.45, 3.75), 1.85, 0.62, "左触觉传感器", "#eff6ff", "#3b82f6", "USB 摄像头")
|
||||
_box(ax, (7.0, 3.75), 1.85, 0.62, "右触觉传感器", "#eff6ff", "#3b82f6", "USB 摄像头")
|
||||
_box(ax, (3.1, 2.35), 3.0, 0.72, "触觉力计算 / 光流", "#eef2ff", "#6366f1", "法向力 · 切向力 · 图像")
|
||||
_box(ax, (0.45, 2.35), 2.0, 0.62, "智能夹持状态机", "#fff7ed", "#d97706")
|
||||
_box(ax, (6.85, 2.35), 2.0, 0.62, "夹爪控制器", "#f1f5f9", "#64748b", "Modbus RTU")
|
||||
_box(ax, (3.1, 0.55), 3.0, 0.62, "GUI 监控界面", "#ecfdf5", "#059669", "3D · 光流 · 曲线 · 状态")
|
||||
_box(ax, (6.85, 0.55), 2.0, 0.62, "智能夹爪", "#f1f5f9", "#475569")
|
||||
|
||||
_arr(ax, (1.37, 3.75), (3.5, 3.07), "视频帧")
|
||||
_arr(ax, (7.0, 3.75), (6.7, 3.07), "视频帧")
|
||||
_arr(ax, (3.1, 2.72), (2.45, 2.68), "力信号")
|
||||
_arr(ax, (2.45, 2.5), (6.85, 2.66), "串口命令")
|
||||
_arr(ax, (7.85, 2.35), (7.85, 1.17), "驱动")
|
||||
_arr(ax, (4.6, 2.35), (4.6, 1.17), "图像/力值")
|
||||
_arr(ax, (1.45, 2.35), (3.4, 0.9), "状态")
|
||||
# feedback: gripper → state machine (dotted, via bottom)
|
||||
ax.plot([7.85, 7.85, 0.15, 0.15], [0.55, 0.22, 0.22, 2.66], color="#94a3b8", lw=0.9, linestyle=(0, (4, 3)))
|
||||
ax.annotate("", xy=(0.45, 2.66), xytext=(0.15, 2.66),
|
||||
arrowprops=dict(arrowstyle="-|>", color="#94a3b8", lw=0.9))
|
||||
ax.text(4.0, 0.1, "位置反馈(虚线)", fontsize=7.5, color="#94a3b8", ha="center")
|
||||
ax.add_patch(
|
||||
FancyArrowPatch(
|
||||
(6.85, 0.86),
|
||||
(1.45, 2.2),
|
||||
arrowstyle="Simple,tail_width=0.4,head_width=3.5,head_length=5",
|
||||
linewidth=0.9,
|
||||
linestyle=(0, (4, 3)),
|
||||
color="#94a3b8",
|
||||
connectionstyle="arc3,rad=-0.15",
|
||||
shrinkA=2,
|
||||
shrinkB=2,
|
||||
)
|
||||
)
|
||||
ax.text(4.2, 1.45, "位置反馈", fontsize=7.5, color="#64748b", ha="center")
|
||||
|
||||
fig.savefig(path, bbox_inches="tight", facecolor="white", pad_inches=0.08)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def render_grip_flow(path: Path) -> None:
|
||||
fig, ax = plt.subplots(figsize=(10.5, 2.5), dpi=240)
|
||||
ax.set_xlim(0, 10.5)
|
||||
ax.set_ylim(0, 2.5)
|
||||
ax.axis("off")
|
||||
|
||||
specs = [
|
||||
(0.2, "等待触发", "open_wait", "#eff6ff", "#2563eb"),
|
||||
(2.1, "闭合中", "closing", "#fff7ed", "#d97706"),
|
||||
(4.0, "夹持加力", "gripping", "#fff7ed", "#d97706"),
|
||||
(5.9, "夹持检测", "hold_check", "#fdf2f8", "#db2777"),
|
||||
(7.8, "张开恢复", "open_recover", "#ecfdf5", "#059669"),
|
||||
]
|
||||
w, h, y = 1.55, 0.88, 1.35
|
||||
xs = []
|
||||
for x, cn, en, fc, ec in specs:
|
||||
_box(ax, (x, y), w, h, cn, fc, ec, en)
|
||||
xs.append(x + w / 2)
|
||||
|
||||
for i, label in enumerate(["侧边触发", "接触", "到保持力", "取物"]):
|
||||
_arr(ax, (xs[i] + w / 2 - 0.02, y + h / 2), (xs[i + 1] - w / 2 + 0.02, y + h / 2), label)
|
||||
|
||||
ax.plot([xs[-1], xs[-1], xs[0], xs[0]], [y, 0.55, 0.55, y], color="#2563eb", lw=1.0)
|
||||
ax.add_patch(FancyArrowPatch((xs[0], y), (xs[0], y + 0.01), arrowstyle="Simple,head_width=3.5,head_length=5", color="#2563eb"))
|
||||
ax.annotate("", xy=(xs[0], y), xytext=(xs[0], 0.55), arrowprops=dict(arrowstyle="-|>", color="#2563eb", lw=1.0))
|
||||
ax.text(4.9, 0.38, "恢复 + rearm", ha="center", fontsize=7.5, color="#1e40af")
|
||||
|
||||
ax.plot([xs[1], xs[1], xs[-1]], [y, 0.85, 0.85], color="#dc2626", lw=1.0, linestyle=(0, (4, 3)))
|
||||
ax.annotate("", xy=(xs[-1], 0.85), xytext=(xs[1], 0.85), arrowprops=dict(arrowstyle="-|>", color="#dc2626", lw=1.0, linestyle=(0, (4, 3))))
|
||||
ax.text(6.2, 0.72, "闭合超时", ha="center", fontsize=7.5, color="#b91c1c")
|
||||
|
||||
fig.savefig(path, bbox_inches="tight", facecolor="white", pad_inches=0.08)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
render_system_architecture(OUT / "system-architecture.png")
|
||||
render_grip_flow(OUT / "grip-state-flow.png")
|
||||
print("Wrote", OUT / "system-architecture.png")
|
||||
print("Wrote", OUT / "grip-state-flow.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
# Gripper Demo 02 Windows x64 / CPython 3.11 venv lock.
|
||||
# Optional CUDA, RAFT, CuPy and Open3D backends are intentionally excluded.
|
||||
|
||||
numpy==2.4.4
|
||||
opencv-python==5.0.0.93
|
||||
llvmlite==0.48.0
|
||||
numba==0.66.0
|
||||
scipy==1.17.1
|
||||
|
||||
PyQt5==5.15.11
|
||||
PyQt5-Qt5==5.15.2
|
||||
PyQt5_sip==12.18.0
|
||||
pyqtgraph==0.14.0
|
||||
|
||||
PyOpenGL==3.1.10
|
||||
PyOpenGL-accelerate==3.1.10
|
||||
|
||||
minimalmodbus==2.1.1
|
||||
pyserial==3.5
|
||||
|
||||
pyinstaller==6.21.0
|
||||
|
||||
Reference in New Issue
Block a user