diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc8a9cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Python caches +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ + +# Test and coverage output +.pytest_cache/ +.coverage +htmlcov/ + +# PyInstaller output +dist/ +build/_pyinstaller/ +build/*.spec + +# Runtime logs and local generated files +*.log +log/ +logs/ +debug.txt + +# IDE/editor files +.idea/ +.vscode/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db +desktop.ini + +# Local agent/tool metadata +.agents/ +.codex/ + +# Hardware vendor tool runtime logs +知行夹爪/**/log/ diff --git a/gripper/README.md b/README.md similarity index 92% rename from gripper/README.md rename to README.md index 32ad748..963b766 100644 --- a/gripper/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Gripper -夹爪02是新的展示程序,和原来的定时开合逻辑分开。它不是定时循环开合;程序只是常驻监听单侧触发,每次触发后执行一次“闭合夹住 -> 监测变化 -> 直接张开”的流程。 +程序只是常驻监听单侧触发,每次触发后执行一次“闭合夹住 -> 监测变化 -> 直接张开”的流程。 ## 展示流程 @@ -53,9 +53,39 @@ python -m gripper.visualizer 默认配置文件: ```text -config\gripper_demo_02.json +config\gripper.json ``` +## 打包 + +先在 `py311` 环境里安装 PyInstaller: + +```powershell +conda activate py311 +pip install pyinstaller +``` + +然后从项目根目录运行: + +```powershell +build\build_exe.bat +``` + +输出文件在: + +```text +dist\GripperDemo02\GripperDemo02.exe +``` + +打包完成后,脚本会把 `config` 和 `sdk` 复制到 exe 同级目录: + +```text +dist\GripperDemo02\config +dist\GripperDemo02\sdk +``` + +现场修改参数时,直接改 exe 同级的 `config\gripper.json`。 + ## 关键配置 - `trigger_normal_force`: 单侧法向力触发阈值。 @@ -89,8 +119,8 @@ config\gripper_demo_02.json - `open_position_tolerance`: 当前点位和 `open_pos` 差值小于它,就认为已经回到初始点位。 - `open_position_stable_seconds`: 回到初始点位后需要稳定多久,才重新等待触发。 - `open_position_check_interval`: `open_recover` 中读取夹爪当前位置的间隔。 -- `normal_force_calibration`: 法向力标定,配置在 `config\gripper_demo_02.json`。 -- `shear_force_calibration`: 切向力标定,配置在 `config\gripper_demo_02.json`。 +- `normal_force_calibration`: 法向力标定,配置在 `config\gripper.json`。 +- `shear_force_calibration`: 切向力标定,配置在 `config\gripper.json`。 ## 日志字段 diff --git a/build/build_exe.bat b/build/build_exe.bat new file mode 100644 index 0000000..0bed110 --- /dev/null +++ b/build/build_exe.bat @@ -0,0 +1,19 @@ +@echo off +setlocal +cd /d "%~dp0.." +call conda activate py311 +if errorlevel 1 ( + echo Failed to activate conda environment: py311 + pause + exit /b 1 +) +python "%~dp0build_exe.py" %* +if errorlevel 1 ( + echo. + echo Build failed. + pause + exit /b 1 +) +echo. +echo Build finished. +pause diff --git a/build/build_exe.ps1 b/build/build_exe.ps1 new file mode 100644 index 0000000..1eb730c --- /dev/null +++ b/build/build_exe.ps1 @@ -0,0 +1,19 @@ +param( + [string]$CondaEnv = "py311" +) + +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$ProjectRoot = Split-Path -Parent $ScriptDir + +Set-Location $ProjectRoot + +Write-Host "Project root: $ProjectRoot" +Write-Host "Conda env: $CondaEnv" + +$ForwardArgs = $args -join " " +cmd /d /s /c "call conda activate $CondaEnv && python `"$ScriptDir\build_exe.py`" $ForwardArgs" +if ($LASTEXITCODE -ne 0) { + throw "build failed" +} diff --git a/build/build_exe.py b/build/build_exe.py new file mode 100644 index 0000000..bd27b70 --- /dev/null +++ b/build/build_exe.py @@ -0,0 +1,123 @@ +import argparse +import importlib.util +import shutil +import subprocess +import sys +from pathlib import Path + + +BUILD_DIR = Path(__file__).resolve().parent +PROJECT_ROOT = BUILD_DIR.parent + + +def module_exists(name): + return importlib.util.find_spec(name) is not None + + +def require_module(name, install_hint): + if module_exists(name): + return + raise SystemExit(f"Missing module '{name}'. Install it first: {install_hint}") + + +def data_arg(source, target): + return f"{source};{target}" + + +def copy_runtime_dir(source, app_dir, name): + target = app_dir / name + if target.exists(): + shutil.rmtree(target) + shutil.copytree( + source, + target, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), + ) + return target + + +def build(app_name, clean_dist): + require_module("PyInstaller", "pip install pyinstaller") + + dist_dir = PROJECT_ROOT / "dist" + work_dir = BUILD_DIR / "_pyinstaller" + launcher = BUILD_DIR / "run_visualizer.py" + + if clean_dist: + app_dist = dist_dir / app_name + if app_dist.exists(): + shutil.rmtree(app_dist) + + args = [ + sys.executable, + "-m", + "PyInstaller", + "--noconfirm", + "--clean", + "--onedir", + "--windowed", + "--name", + app_name, + "--distpath", + str(dist_dir), + "--workpath", + str(work_dir), + "--specpath", + str(BUILD_DIR), + "--paths", + str(PROJECT_ROOT), + "--paths", + str(PROJECT_ROOT / "sdk"), + "--add-data", + data_arg(PROJECT_ROOT / "config", "config"), + "--add-data", + data_arg(PROJECT_ROOT / "sdk", "sdk"), + "--hidden-import", + "changingtek_p_rtu_Servo", + "--hidden-import", + "minimalmodbus", + "--hidden-import", + "serial", + "--hidden-import", + "cv2", + "--hidden-import", + "numpy", + "--collect-all", + "PyQt5", + "--collect-all", + "pyqtgraph", + ] + + if module_exists("orisys"): + args.extend(["--collect-submodules", "orisys"]) + else: + print("Warning: module 'orisys' was not found; camera runtime may fail unless it is installed.") + + args.append(str(launcher)) + + subprocess.run(args, cwd=PROJECT_ROOT, check=True) + + exe_path = dist_dir / app_name / f"{app_name}.exe" + if not exe_path.exists(): + raise SystemExit(f"Build finished but exe was not found: {exe_path}") + + app_dir = exe_path.parent + config_dir = copy_runtime_dir(PROJECT_ROOT / "config", app_dir, "config") + sdk_dir = copy_runtime_dir(PROJECT_ROOT / "sdk", app_dir, "sdk") + + print() + print(f"Build complete: {exe_path}") + print(f"Runtime config copied to: {config_dir}") + print(f"Runtime sdk copied to: {sdk_dir}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--name", default="GripperDemo02", help="Output exe/app folder name.") + parser.add_argument("--clean-dist", action="store_true", help="Delete dist/ before building.") + args = parser.parse_args() + build(args.name, args.clean_dist) + + +if __name__ == "__main__": + main() diff --git a/build/run_visualizer.py b/build/run_visualizer.py new file mode 100644 index 0000000..29e050f --- /dev/null +++ b/build/run_visualizer.py @@ -0,0 +1,15 @@ +import multiprocessing as mp +import os +import sys +from pathlib import Path + + +if getattr(sys, "frozen", False): + os.chdir(Path(sys.executable).resolve().parent) + +from gripper.visualizer import main + + +if __name__ == "__main__": + mp.freeze_support() + raise SystemExit(main(sys.argv[1:])) diff --git a/config/compound_eye.png b/config/compound_eye.png index b90a7c2..bd61a42 100644 Binary files a/config/compound_eye.png and b/config/compound_eye.png differ diff --git a/config/gripper.json b/config/gripper.json index 67315e9..53b3fb4 100644 --- a/config/gripper.json +++ b/config/gripper.json @@ -39,9 +39,9 @@ "grip_requires_both": true, "close_timeout_seconds": 5.0, "closing_command_interval": 0.2, - "force_ramp_step": 1, - "force_ramp_interval": 0.1, - "grip_settle_seconds": 0.1, + "force_ramp_step": 5, + "force_ramp_interval": 0.02, + "grip_settle_seconds": 0.05, "adaptive_grip_enabled": true, "adaptive_shear_start_force": 0.1, "adaptive_shear_full_force": 0.6, diff --git a/gripper/__pycache__/calibration.cpython-311.pyc b/gripper/__pycache__/calibration.cpython-311.pyc deleted file mode 100644 index 5b53cad..0000000 Binary files a/gripper/__pycache__/calibration.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/config_loader.cpython-311.pyc b/gripper/__pycache__/config_loader.cpython-311.pyc deleted file mode 100644 index 73af8de..0000000 Binary files a/gripper/__pycache__/config_loader.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/controller.cpython-311.pyc b/gripper/__pycache__/controller.cpython-311.pyc deleted file mode 100644 index f2cdaff..0000000 Binary files a/gripper/__pycache__/controller.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/feedback.cpython-311.pyc b/gripper/__pycache__/feedback.cpython-311.pyc deleted file mode 100644 index ade02c2..0000000 Binary files a/gripper/__pycache__/feedback.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/filters.cpython-311.pyc b/gripper/__pycache__/filters.cpython-311.pyc deleted file mode 100644 index 99200d7..0000000 Binary files a/gripper/__pycache__/filters.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/force_reader.cpython-311.pyc b/gripper/__pycache__/force_reader.cpython-311.pyc deleted file mode 100644 index 7ea4431..0000000 Binary files a/gripper/__pycache__/force_reader.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/hardware.cpython-311.pyc b/gripper/__pycache__/hardware.cpython-311.pyc deleted file mode 100644 index 97ea914..0000000 Binary files a/gripper/__pycache__/hardware.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/main.cpython-311.pyc b/gripper/__pycache__/main.cpython-311.pyc deleted file mode 100644 index 4faba43..0000000 Binary files a/gripper/__pycache__/main.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/paths.cpython-311.pyc b/gripper/__pycache__/paths.cpython-311.pyc deleted file mode 100644 index c559ae8..0000000 Binary files a/gripper/__pycache__/paths.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/rotation.cpython-311.pyc b/gripper/__pycache__/rotation.cpython-311.pyc deleted file mode 100644 index 4a4408c..0000000 Binary files a/gripper/__pycache__/rotation.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/settings.cpython-311.pyc b/gripper/__pycache__/settings.cpython-311.pyc deleted file mode 100644 index 97b5eba..0000000 Binary files a/gripper/__pycache__/settings.cpython-311.pyc and /dev/null differ diff --git a/gripper/__pycache__/visualizer.cpython-311.pyc b/gripper/__pycache__/visualizer.cpython-311.pyc index 6b647aa..3db6d90 100644 Binary files a/gripper/__pycache__/visualizer.cpython-311.pyc and b/gripper/__pycache__/visualizer.cpython-311.pyc differ diff --git a/gripper/app.py b/gripper/app.py new file mode 100644 index 0000000..5459d13 --- /dev/null +++ b/gripper/app.py @@ -0,0 +1,60 @@ +import multiprocessing as mp + +from .common.paths import ensure_project_paths +from .config.calibration import ForceConverter +from .config.settings import Demo02Config +from .control.controller import SideTriggerGripController +from .control.feedback import TactileFeedbackProcessor +from .hardware import GripperClient +from .sensors import TwoSensorForceReader + +def build_controller( + argv=None, + status_callback=None, + include_visuals=False, + visual_size=320, +): + ensure_project_paths() + config = Demo02Config.from_args(argv) + + normal_converter = ForceConverter.from_config( + config.normal_force_calibration, + "normal_force_calibration", + ) + shear_converter = ForceConverter.from_config( + config.shear_force_calibration, + "shear_force_calibration", + ) + + reader = TwoSensorForceReader( + video1=config.video1, + video2=config.video2, + config1=config.sensor_config1, + config2=config.sensor_config2, + rotation1=config.rotation1, + rotation2=config.rotation2, + target_fps=config.sensor_fps, + cuda=not config.sensor_cpu, + motion_threshold=config.motion_threshold, + include_visuals=include_visuals, + visual_size=visual_size, + ) + gripper = GripperClient(config) + feedback = TactileFeedbackProcessor(config, normal_converter, shear_converter) + return SideTriggerGripController( + config, + reader, + gripper, + feedback, + status_callback=status_callback, + ) + + +def main(argv=None): + controller = build_controller(argv) + controller.run() + + +if __name__ == "__main__": + mp.freeze_support() + main() diff --git a/gripper/common/__init__.py b/gripper/common/__init__.py new file mode 100644 index 0000000..d872f5c --- /dev/null +++ b/gripper/common/__init__.py @@ -0,0 +1 @@ +"""Shared path and utility helpers.""" diff --git a/gripper/filters.py b/gripper/common/filters.py similarity index 100% rename from gripper/filters.py rename to gripper/common/filters.py diff --git a/gripper/common/paths.py b/gripper/common/paths.py new file mode 100644 index 0000000..29ac6da --- /dev/null +++ b/gripper/common/paths.py @@ -0,0 +1,24 @@ +from pathlib import Path +import sys + + +def _runtime_root(): + if getattr(sys, "frozen", False): + exe_dir = Path(sys.executable).resolve().parent + if (exe_dir / "config").exists(): + return exe_dir + return Path(getattr(sys, "_MEIPASS", exe_dir)).resolve() + return Path(__file__).resolve().parents[2] + + +REPO_ROOT = _runtime_root() +PACKAGE_DIR = REPO_ROOT / "gripper" +SDK_DIR = REPO_ROOT / "sdk" +SRC_DIR = REPO_ROOT / "src" + + +def ensure_project_paths(): + for path in (SDK_DIR, SRC_DIR, REPO_ROOT): + path_text = str(path) + if path_text not in sys.path: + sys.path.insert(0, path_text) diff --git a/gripper/config/__init__.py b/gripper/config/__init__.py new file mode 100644 index 0000000..4a2f526 --- /dev/null +++ b/gripper/config/__init__.py @@ -0,0 +1,4 @@ +from .settings import Demo02Config + + +__all__ = ["Demo02Config"] diff --git a/gripper/calibration.py b/gripper/config/calibration.py similarity index 100% rename from gripper/calibration.py rename to gripper/config/calibration.py diff --git a/gripper/config_loader.py b/gripper/config/loader.py similarity index 100% rename from gripper/config_loader.py rename to gripper/config/loader.py diff --git a/gripper/rotation.py b/gripper/config/rotation.py similarity index 100% rename from gripper/rotation.py rename to gripper/config/rotation.py diff --git a/gripper/settings.py b/gripper/config/settings.py similarity index 99% rename from gripper/settings.py rename to gripper/config/settings.py index 43c608e..74170f0 100644 --- a/gripper/settings.py +++ b/gripper/config/settings.py @@ -6,8 +6,8 @@ from .calibration import ( DEFAULT_NORMAL_FORCE_CALIBRATION, DEFAULT_SHEAR_FORCE_CALIBRATION, ) -from .config_loader import load_control_config -from .paths import REPO_ROOT +from .loader import load_control_config +from ..common.paths import REPO_ROOT from .rotation import ( DEFAULT_ROTATION1, DEFAULT_ROTATION2, diff --git a/gripper/control/__init__.py b/gripper/control/__init__.py new file mode 100644 index 0000000..ffc49c0 --- /dev/null +++ b/gripper/control/__init__.py @@ -0,0 +1,5 @@ +from .controller import SideTriggerGripController +from .feedback import TactileFeedbackProcessor + + +__all__ = ["SideTriggerGripController", "TactileFeedbackProcessor"] diff --git a/gripper/controller.py b/gripper/control/controller.py similarity index 99% rename from gripper/controller.py rename to gripper/control/controller.py index f7d07b5..39f873d 100644 --- a/gripper/controller.py +++ b/gripper/control/controller.py @@ -1,7 +1,7 @@ import time import threading -from .filters import clamp +from ..common.filters import clamp class SideTriggerGripController: diff --git a/gripper/feedback.py b/gripper/control/feedback.py similarity index 99% rename from gripper/feedback.py rename to gripper/control/feedback.py index b0ceffc..e3dd8b2 100644 --- a/gripper/feedback.py +++ b/gripper/control/feedback.py @@ -1,7 +1,7 @@ from dataclasses import dataclass import time -from .filters import ExponentialFilter +from ..common.filters import ExponentialFilter def shear_magnitude(sample): diff --git a/gripper/hardware/__init__.py b/gripper/hardware/__init__.py new file mode 100644 index 0000000..f2e3c3d --- /dev/null +++ b/gripper/hardware/__init__.py @@ -0,0 +1,4 @@ +from .gripper_client import GripperClient + + +__all__ = ["GripperClient"] diff --git a/gripper/hardware.py b/gripper/hardware/gripper_client.py similarity index 97% rename from gripper/hardware.py rename to gripper/hardware/gripper_client.py index 27fde63..236e764 100644 --- a/gripper/hardware.py +++ b/gripper/hardware/gripper_client.py @@ -1,5 +1,5 @@ -from .filters import clamp -from .paths import ensure_project_paths +from ..common.filters import clamp +from ..common.paths import ensure_project_paths class GripperClient: diff --git a/gripper/main.py b/gripper/main.py index f0bca07..1c333da 100644 --- a/gripper/main.py +++ b/gripper/main.py @@ -1,60 +1,6 @@ import multiprocessing as mp -from .calibration import ForceConverter -from .feedback import TactileFeedbackProcessor -from .force_reader import TwoSensorForceReader -from .hardware import GripperClient -from .paths import ensure_project_paths - -from .controller import SideTriggerGripController -from .settings import Demo02Config - - -def build_controller( - argv=None, - status_callback=None, - include_visuals=False, - visual_size=320, -): - ensure_project_paths() - config = Demo02Config.from_args(argv) - - normal_converter = ForceConverter.from_config( - config.normal_force_calibration, - "normal_force_calibration", - ) - shear_converter = ForceConverter.from_config( - config.shear_force_calibration, - "shear_force_calibration", - ) - - reader = TwoSensorForceReader( - video1=config.video1, - video2=config.video2, - config1=config.sensor_config1, - config2=config.sensor_config2, - rotation1=config.rotation1, - rotation2=config.rotation2, - target_fps=config.sensor_fps, - cuda=not config.sensor_cpu, - motion_threshold=config.motion_threshold, - include_visuals=include_visuals, - visual_size=visual_size, - ) - gripper = GripperClient(config) - feedback = TactileFeedbackProcessor(config, normal_converter, shear_converter) - return SideTriggerGripController( - config, - reader, - gripper, - feedback, - status_callback=status_callback, - ) - - -def main(argv=None): - controller = build_controller(argv) - controller.run() +from .app import build_controller, main if __name__ == "__main__": diff --git a/gripper/paths.py b/gripper/paths.py deleted file mode 100644 index 5407f0e..0000000 --- a/gripper/paths.py +++ /dev/null @@ -1,16 +0,0 @@ -from pathlib import Path -import sys - - -PACKAGE_DIR = Path(__file__).resolve().parent -REPO_ROOT = PACKAGE_DIR.parent -SDK_DIR = REPO_ROOT / "sdk" -SRC_DIR = REPO_ROOT / "src" - - -def ensure_project_paths(): - for path in (SDK_DIR, SRC_DIR, REPO_ROOT): - path_text = str(path) - if path_text not in sys.path: - sys.path.insert(0, path_text) - diff --git a/gripper/sensors/__init__.py b/gripper/sensors/__init__.py new file mode 100644 index 0000000..0ba9cb3 --- /dev/null +++ b/gripper/sensors/__init__.py @@ -0,0 +1,4 @@ +from .force_reader import TwoSensorForceReader + + +__all__ = ["TwoSensorForceReader"] diff --git a/gripper/force_reader.py b/gripper/sensors/force_reader.py similarity index 99% rename from gripper/force_reader.py rename to gripper/sensors/force_reader.py index 28b7ae8..0ec328e 100644 --- a/gripper/force_reader.py +++ b/gripper/sensors/force_reader.py @@ -11,8 +11,8 @@ from multiprocessing import Process, Queue import cv2 import numpy as np -from .paths import ensure_project_paths -from .rotation import ( +from ..common.paths import ensure_project_paths +from ..config.rotation import ( DEFAULT_ROTATION1, DEFAULT_ROTATION2, normalize_rotation_config, diff --git a/gripper/ui/__init__.py b/gripper/ui/__init__.py new file mode 100644 index 0000000..9d8d22a --- /dev/null +++ b/gripper/ui/__init__.py @@ -0,0 +1 @@ +"""User interface package.""" diff --git a/gripper/ui/visualizer.py b/gripper/ui/visualizer.py new file mode 100644 index 0000000..bcb3a32 --- /dev/null +++ b/gripper/ui/visualizer.py @@ -0,0 +1,921 @@ +import argparse +import multiprocessing as mp +import sys +import threading +import time + +import cv2 +import numpy as np + + +def _fix_qt_plugin_path(): + try: + from PyQt5.QtCore import QLibraryInfo + + import os + + os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = QLibraryInfo.location( + QLibraryInfo.PluginsPath + ) + except Exception: + pass + + +_fix_qt_plugin_path() + +_QT_IMPORT_ERROR = None +try: + from PyQt5.QtCore import QEasingCurve, QPropertyAnimation, Qt, QTimer + from PyQt5.QtGui import QFont, QImage, QPixmap + from PyQt5.QtWidgets import ( + QApplication, + QDialog, + QFrame, + QGraphicsOpacityEffect, + QGridLayout, + QHBoxLayout, + QLabel, + QMainWindow, + QPushButton, + QSizePolicy, + QSplitter, + QVBoxLayout, + QWidget, + ) +except Exception as exc: + _QT_IMPORT_ERROR = exc + Qt = QTimer = QPropertyAnimation = QEasingCurve = QFont = QImage = QPixmap = QApplication = None + QGridLayout = QHBoxLayout = QLabel = QPushButton = QSizePolicy = QSplitter = None + QVBoxLayout = QWidget = None + QDialog = QFrame = QGraphicsOpacityEffect = QMainWindow = object + +try: + import pyqtgraph as pg +except Exception as exc: + pg = None + _PYQTGRAPH_IMPORT_ERROR = exc +else: + _PYQTGRAPH_IMPORT_ERROR = None + +from ..app import build_controller + + +PLOT_POINTS = 180 +PLOT_FORCE_Y_MIN_N = 0.0 +PLOT_FORCE_Y_MAX_N = 3.0 +PLOT_SHEAR_Y_MIN_N = -3.0 +PLOT_SHEAR_Y_MAX_N = 3.0 +FORCE_DISPLAY_ZERO_THRESHOLD_N = 0.05 +CURVE_COLOR = "#00E5FF" + + +def cv2_to_pixmap(img_bgr): + if img_bgr is None: + return QPixmap() + if len(img_bgr.shape) == 2: + img_bgr = cv2.cvtColor(img_bgr, cv2.COLOR_GRAY2BGR) + rgb = cv2.cvtColor(np.ascontiguousarray(img_bgr), cv2.COLOR_BGR2RGB) + h, w, ch = rgb.shape + qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy() + return QPixmap.fromImage(qimg) + + +def apply_visual_transform(img, transform): + if img is None: + return None + angle = int(transform.get("rotation_angle", 0)) % 360 + if angle == 90: + img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) + elif angle == 180: + img = cv2.rotate(img, cv2.ROTATE_180) + elif angle == 270: + img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) + + flip_x = bool(transform.get("flip_x")) + flip_y = bool(transform.get("flip_y")) + if flip_x and flip_y: + img = cv2.flip(img, -1) + elif flip_x: + img = cv2.flip(img, 1) + elif flip_y: + img = cv2.flip(img, 0) + return img + + +def force_display_value(value): + value = float(value) + if abs(value) < FORCE_DISPLAY_ZERO_THRESHOLD_N: + return 0.0 + return value + + +def fmt(value, unit="", width=0, precision=3): + if value is None: + text = "--" + else: + value = force_display_value(value) + text = "0" if value == 0.0 else f"{value:.{precision}f}" + if width: + text = text.rjust(width) + return f"{text}{unit}" + + +STATE_TEXT = { + "open_wait": "等待触发", + "closing": "闭合中", + "gripping": "夹持加力", + "hold_check": "夹持检测", + "open_recover": "张开恢复", + "manual_close": "手动闭合", + "manual_hold": "手动保持", + "error": "错误", +} + +ACTION_TEXT = { + "wait": "等待", + "waiting-for-sensor-samples": "等待传感器数据", + "open-wait-stabilizing": "等待稳定基准", + "open-wait-armed": "已就绪", + "side-trigger-close": "侧边触发闭合", + "closing-continue": "继续闭合", + "closing-to-grip": "闭合寻找物体", + "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-ready-position": "张开到位", + "open-recover-quiet": "力稳定恢复", + "open-recover-stabilizing": "恢复稳定中", + "open-recover-wait-force-clear": "等待力恢复", + "open-ready": "恢复完成", + "manual-open": "手动张开", + "manual-close": "手动闭合", + "manual-hold": "手动保持", + "manual-release": "手动释放", +} + + +def label_with_cn(value, mapping): + value = str(value or "--") + cn = mapping.get(value) + if cn is None: + for prefix, text in mapping.items(): + if value.startswith(f"{prefix} "): + cn = text + break + if cn is None: + return value + return f"{value}({cn})" + + +STYLE = """ +QMainWindow { + background-color: #000000; +} +QWidget { + background-color: #000000; + color: #9a9a9a; + font-family: "Microsoft YaHei", "SimHei"; +} +QLabel#Title { + color: #d7d7d7; + font-size: 18px; + font-weight: 600; +} +QLabel#Subtle { + color: #777777; + font-size: 12px; +} +QLabel#MetricLabel { + color: #888888; + font-size: 12px; +} +QLabel#MetricValue { + color: #f2f2f2; + font-family: "Cascadia Code", "Consolas"; + font-size: 22px; + font-weight: 600; +} +QLabel#SmallValue { + color: #d8d8d8; + font-family: "Cascadia Code", "Consolas"; + font-size: 15px; +} +QLabel#StatusChip { + background-color: #101010; + border: 1px solid #303030; + border-radius: 4px; + color: #00e5ff; + font-family: "Cascadia Code", "Consolas"; + font-size: 18px; + font-weight: 600; + padding: 8px 12px; +} +QFrame#StatusPanel { + background-color: #101010; + border: 1px solid #303030; + border-radius: 4px; +} +QLabel#StatusMain { + color: #00e5ff; + font-family: "Cascadia Code", "Consolas"; + font-size: 17px; + font-weight: 600; +} +QLabel#StatusSub { + color: #9a9a9a; + font-family: "Cascadia Code", "Consolas"; + font-size: 12px; +} +QLabel#ActionChip { + background-color: #0b0b0b; + border: 1px solid #262626; + border-radius: 4px; + color: #c8c8c8; + font-family: "Cascadia Code", "Consolas"; + font-size: 13px; + padding: 7px 10px; +} +QLabel#ImagePanel { + background-color: #0b0b0b; + border: 1px solid #262626; + border-radius: 4px; +} +QFrame#Panel { + background-color: #050505; + border: 1px solid #222222; + border-radius: 4px; +} +QPushButton { + background-color: #171717; + color: #d8d8d8; + border: 1px solid #363636; + border-radius: 4px; + padding: 7px 16px; + font-size: 13px; +} +QPushButton:hover { + background-color: #242424; + color: #ffffff; + border: 1px solid #5a5a5a; +} +QPushButton:pressed { + background-color: #0f0f0f; + border: 1px solid #8a8a8a; + padding-top: 8px; + padding-bottom: 6px; +} +QPushButton:disabled { + background-color: #101010; + color: #606060; + border: 1px solid #242424; +} +QPushButton#StartButton { + background-color: #171717; + color: #d8d8d8; + border: 1px solid #363636; +} +QPushButton#StopButton { + background-color: #171717; + color: #d8d8d8; + border: 1px solid #363636; +} +QPushButton#RestartButton { + background-color: #171717; + color: #d8d8d8; + border: 1px solid #363636; +} +QPushButton#ManualButton { + background-color: #171717; + color: #d8d8d8; + border: 1px solid #363636; +} +QPushButton#HoldButton { + background-color: #171717; + color: #d8d8d8; + border: 1px solid #363636; +} +QSplitter::handle { + background-color: #151515; +} +QSplitter::handle:hover { + background-color: #2a2a2a; +} +QSplitter::handle:horizontal { + width: 8px; +} +QSplitter::handle:vertical { + height: 8px; +} +""" + + +if QPushButton is not None: + class AnimatedButton(QPushButton): + def __init__(self, text=""): + super().__init__(text) + self._opacity = QGraphicsOpacityEffect(self) + self._opacity.setOpacity(1.0) + self.setGraphicsEffect(self._opacity) + self._animation = QPropertyAnimation(self._opacity, b"opacity", self) + self._animation.setEasingCurve(QEasingCurve.OutCubic) + + def _animate_opacity(self, target, duration): + self._animation.stop() + self._animation.setDuration(duration) + self._animation.setStartValue(self._opacity.opacity()) + self._animation.setEndValue(target) + self._animation.start() + + def mousePressEvent(self, event): + if self.isEnabled(): + self._animate_opacity(0.72, 70) + super().mousePressEvent(event) + + def mouseReleaseEvent(self, event): + if self.isEnabled(): + self._animate_opacity(1.0, 140) + super().mouseReleaseEvent(event) +else: + AnimatedButton = None + + +class MetricBox(QFrame): + def __init__(self, title, value="--"): + super().__init__() + self.setObjectName("Panel") + layout = QVBoxLayout(self) + layout.setContentsMargins(10, 8, 10, 8) + layout.setSpacing(4) + self.title_label = QLabel(title) + self.title_label.setObjectName("MetricLabel") + self.value_label = QLabel(value) + self.value_label.setObjectName("MetricValue") + layout.addWidget(self.title_label) + layout.addWidget(self.value_label) + + def set_value(self, value, sub=None): + self.value_label.setText(value) + + +class ImageBox(QFrame): + def __init__(self, title): + super().__init__() + self.title = title + self.setObjectName("Panel") + layout = QVBoxLayout(self) + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(6) + self.title_label = QLabel(title) + self.title_label.setObjectName("MetricLabel") + self.image_label = QLabel() + self.image_label.setObjectName("ImagePanel") + self.image_label.setAlignment(Qt.AlignCenter) + self.image_label.setMinimumSize(180, 180) + self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.image_label.setScaledContents(True) + layout.addWidget(self.title_label) + layout.addWidget(self.image_label, 1) + + def set_image(self, img): + self.image_label.setPixmap(cv2_to_pixmap(img)) + + def set_transform_label(self, transform): + angle = int(transform.get("rotation_angle", 0)) % 360 + flip_x = "X1" if transform.get("flip_x") else "X0" + flip_y = "Y1" if transform.get("flip_y") else "Y0" + self.title_label.setText(f"{self.title} {angle}deg {flip_x} {flip_y}") + + +class VisualSettingsDialog(QDialog): + def __init__(self, parent): + super().__init__(parent) + self.viewer = parent + self.setWindowTitle("画面设置") + self.setModal(False) + self.setMinimumWidth(360) + + layout = QVBoxLayout(self) + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(10) + + self.labels = {} + for side, title in (("left", "左侧光流"), ("right", "右侧光流")): + panel = QFrame() + panel.setObjectName("Panel") + panel_layout = QVBoxLayout(panel) + panel_layout.setContentsMargins(10, 8, 10, 10) + panel_layout.setSpacing(8) + + label = QLabel() + label.setObjectName("MetricLabel") + self.labels[side] = label + panel_layout.addWidget(label) + + row = QHBoxLayout() + row.setSpacing(6) + for text, action in ( + ("旋转", "rotate"), + ("水平", "flip_x"), + ("垂直", "flip_y"), + ("重置", "reset"), + ): + button = AnimatedButton(text) + button.setObjectName("ManualButton") + button.clicked.connect( + lambda checked=False, s=side, a=action: self._adjust(s, a) + ) + row.addWidget(button) + panel_layout.addLayout(row) + layout.addWidget(panel) + + self._refresh_labels() + + def _adjust(self, side, action): + self.viewer.adjust_visual_transform(side, action) + self._refresh_labels() + + def _refresh_labels(self): + for side, title in (("left", "左侧光流"), ("right", "右侧光流")): + transform = self.viewer.visual_transforms[side] + angle = int(transform.get("rotation_angle", 0)) % 360 + flip_x = "开" if transform.get("flip_x") else "关" + flip_y = "开" if transform.get("flip_y") else "关" + self.labels[side].setText( + f"{title}: 旋转 {angle}deg / 水平镜像 {flip_x} / 垂直镜像 {flip_y}" + ) + + +class GripperDemo02Viewer(QMainWindow): + def __init__(self, argv=None): + super().__init__() + if _QT_IMPORT_ERROR is not None: + raise RuntimeError(f"PyQt5 import failed: {_QT_IMPORT_ERROR}") + if pg is None: + raise RuntimeError(f"pyqtgraph import failed: {_PYQTGRAPH_IMPORT_ERROR}") + + self.setWindowTitle("Gripper Demo 02") + self.setStyleSheet(STYLE) + self.controller = None + self.control_thread = None + self.latest_status = None + self.status_lock = threading.Lock() + self.controller_finished = False + self.restart_pending = False + self.button_state = "idle" + self.start_time = time.time() + self.force_history = [] + self.argv = argv + self.visual_settings_dialog = None + self.visual_transforms = { + "left": {"rotation_angle": 0, "flip_x": False, "flip_y": False}, + "right": {"rotation_angle": 0, "flip_x": False, "flip_y": False}, + } + + self._build_ui() + self._configure_plot() + + self.timer = QTimer(self) + self.timer.timeout.connect(self._refresh) + self.timer.start(33) + + def _build_ui(self): + root = QWidget() + self.setCentralWidget(root) + outer = QVBoxLayout(root) + outer.setContentsMargins(8, 8, 8, 8) + outer.setSpacing(8) + + top = QHBoxLayout() + top.setSpacing(8) + title = QLabel("Orisys 夹爪") + title.setObjectName("Title") + self.status_panel = QFrame() + self.status_panel.setObjectName("StatusPanel") + status_layout = QVBoxLayout(self.status_panel) + status_layout.setContentsMargins(10, 6, 10, 6) + status_layout.setSpacing(1) + self.status_chip = QLabel("state=--") + self.status_chip.setObjectName("StatusMain") + self.action_label = QLabel("action=--") + self.action_label.setObjectName("StatusSub") + status_layout.addWidget(self.status_chip) + status_layout.addWidget(self.action_label) + self.start_button = AnimatedButton("启动") + self.start_button.setObjectName("StartButton") + self.start_button.clicked.connect(self.start_control) + self.stop_button = AnimatedButton("停止") + self.stop_button.setObjectName("StopButton") + self.stop_button.clicked.connect(self.stop_control) + self.stop_button.setEnabled(False) + self.restart_button = AnimatedButton("重启") + self.restart_button.setObjectName("RestartButton") + self.restart_button.clicked.connect(self.restart_control) + self.manual_open_button = AnimatedButton("夹爪张开") + self.manual_open_button.setObjectName("ManualButton") + self.manual_open_button.clicked.connect(lambda: self.send_manual_command("open")) + self.manual_close_button = AnimatedButton("夹爪闭合") + self.manual_close_button.setObjectName("ManualButton") + self.manual_close_button.clicked.connect(lambda: self.send_manual_command("close")) + self.manual_hold_button = AnimatedButton("保持") + self.manual_hold_button.setObjectName("HoldButton") + self.manual_hold_button.clicked.connect(self.toggle_manual_hold) + self.visual_settings_button = AnimatedButton("画面设置") + self.visual_settings_button.setObjectName("ManualButton") + self.visual_settings_button.clicked.connect(self.open_visual_settings) + self._set_button_state("idle") + top.addWidget(title) + top.addWidget(self.status_panel) + top.addStretch(1) + top.addWidget(self.start_button) + top.addWidget(self.stop_button) + top.addWidget(self.restart_button) + top.addWidget(self.manual_open_button) + top.addWidget(self.manual_close_button) + top.addWidget(self.manual_hold_button) + top.addWidget(self.visual_settings_button) + outer.addLayout(top) + + main_splitter = QSplitter(Qt.Horizontal) + main_splitter.setChildrenCollapsible(False) + outer.addWidget(main_splitter, 1) + + flow_splitter = QSplitter(Qt.Vertical) + flow_splitter.setChildrenCollapsible(False) + self.left_flow = ImageBox("左侧光流") + self.right_flow = ImageBox("右侧光流") + self.left_flow.set_transform_label(self.visual_transforms["left"]) + self.right_flow.set_transform_label(self.visual_transforms["right"]) + flow_splitter.addWidget(self.left_flow) + flow_splitter.addWidget(self.right_flow) + flow_splitter.setStretchFactor(0, 1) + flow_splitter.setStretchFactor(1, 1) + flow_splitter.setSizes([360, 360]) + main_splitter.addWidget(flow_splitter) + + side_widget = QWidget() + side = QVBoxLayout(side_widget) + side.setContentsMargins(0, 0, 0, 0) + side.setSpacing(8) + main_splitter.addWidget(side_widget) + main_splitter.setStretchFactor(0, 2) + main_splitter.setStretchFactor(1, 4) + main_splitter.setSizes([520, 980]) + + metrics_grid = QGridLayout() + metrics_grid.setSpacing(8) + side.addLayout(metrics_grid) + self.left_force = MetricBox("左法向力", "--") + self.right_force = MetricBox("右法向力", "--") + self.left_shear_box = MetricBox("左切向力", "--") + self.right_shear_box = MetricBox("右切向力", "--") + self.force_pct_box = MetricBox("夹爪当前力比例(%)", "--") + self.target_force_box = MetricBox("夹爪目标力比例(%)", "--") + self.speed_box = MetricBox("速度", "--") + self.trigger_box = MetricBox("是否触发/系统是否允许触发", "--") + self.contact_box = MetricBox("接触", "--") + self.release_box = MetricBox("释放检测", "--") + boxes = [ + self.left_force, + self.right_force, + self.left_shear_box, + self.right_shear_box, + self.force_pct_box, + self.target_force_box, + self.speed_box, + self.trigger_box, + self.contact_box, + self.release_box, + ] + for idx, box in enumerate(boxes): + metrics_grid.addWidget(box, idx // 2, idx % 2) + + pg.setConfigOption("background", "#000000") + pg.setConfigOption("foreground", "#777777") + pg.setConfigOptions(antialias=True) + self.plot_widget = pg.GraphicsLayoutWidget() + self.plot_widget.setMinimumHeight(240) + side.addWidget(self.plot_widget, 1) + + def _configure_plot(self): + self.plots = [] + self.curves = {} + plot_specs = [ + ("left_normal", "左法向力", "#00E5FF", PLOT_FORCE_Y_MIN_N, PLOT_FORCE_Y_MAX_N), + ("right_normal", "右法向力", "#ffb000", PLOT_FORCE_Y_MIN_N, PLOT_FORCE_Y_MAX_N), + ("left_shear_x", "左切向 X", "#61d394", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N), + ("left_shear_y", "左切向 Y", "#f07178", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N), + ("right_shear_x", "右切向 X", "#8ab4f8", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N), + ("right_shear_y", "右切向 Y", "#d19a66", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N), + ] + for idx, (key, title, color, y_min, y_max) in enumerate(plot_specs): + row = idx // 2 + col = idx % 2 + plot = self.plot_widget.addPlot(row=row, col=col) + plot.setTitle(title, color="#999999", size="11pt") + plot.titleLabel.setFont(QFont("SimHei", 11)) + plot.showAxis("top", False) + plot.showAxis("right", False) + plot.showGrid(x=False, y=True, alpha=0.12) + plot.setLabel("bottom", "时间", units="s") + plot.setLabel("left", "力", units="N") + plot.getAxis("bottom").setTickSpacing(major=1.0, minor=1.0) + plot.setYRange(y_min, y_max, padding=0) + plot.setLimits(yMin=y_min, yMax=y_max) + plot.enableAutoRange(axis=pg.ViewBox.YAxis, enable=False) + self.plots.append(plot) + self.curves[key] = plot.plot(pen=pg.mkPen(color, width=2)) + + def _set_button_state(self, state): + labels = { + "idle": ("启动", "停止", "重启"), + "starting": ("启动中", "停止", "重启"), + "running": ("启动", "停止", "重启"), + "stopping": ("启动", "停止中", "重启"), + "restarting": ("启动", "停止", "重启中"), + } + enabled = { + "idle": (True, False, True), + "starting": (False, True, False), + "running": (False, True, True), + "stopping": (False, False, False), + "restarting": (False, False, False), + } + self.button_state = state + start_text, stop_text, restart_text = labels[state] + start_enabled, stop_enabled, restart_enabled = enabled[state] + self.start_button.setText(start_text) + self.stop_button.setText(stop_text) + self.restart_button.setText(restart_text) + self.start_button.setEnabled(start_enabled) + self.stop_button.setEnabled(stop_enabled) + self.restart_button.setEnabled(restart_enabled) + manual_enabled = state == "running" + self.manual_open_button.setEnabled(manual_enabled) + self.manual_close_button.setEnabled(manual_enabled) + self.manual_hold_button.setEnabled(manual_enabled) + + def open_visual_settings(self): + if self.visual_settings_dialog is None: + self.visual_settings_dialog = VisualSettingsDialog(self) + self.visual_settings_dialog._refresh_labels() + self.visual_settings_dialog.show() + self.visual_settings_dialog.raise_() + self.visual_settings_dialog.activateWindow() + + def adjust_visual_transform(self, side, action): + transform = self.visual_transforms[side] + if action == "rotate": + transform["rotation_angle"] = (int(transform["rotation_angle"]) + 90) % 360 + elif action == "flip_x": + transform["flip_x"] = not bool(transform["flip_x"]) + elif action == "flip_y": + transform["flip_y"] = not bool(transform["flip_y"]) + elif action == "reset": + transform.update({"rotation_angle": 0, "flip_x": False, "flip_y": False}) + + if side == "left": + self.left_flow.set_transform_label(transform) + else: + self.right_flow.set_transform_label(transform) + + def send_manual_command(self, command): + if self.controller is None or self.control_thread is None or not self.control_thread.is_alive(): + return + try: + self.controller.request_manual_command(command) + except Exception as exc: + print(f"manual command failed: {exc}") + + def toggle_manual_hold(self): + command = "release" if self.manual_hold_button.text() == "释放" else "hold" + self.send_manual_command(command) + + def start_control(self, from_restart=False): + if self.control_thread is not None and self.control_thread.is_alive(): + return + self.force_history.clear() + self.start_time = time.time() + self._set_button_state("restarting" if from_restart else "starting") + self.controller_finished = False + + self.controller = build_controller( + self.argv, + status_callback=self._on_status, + include_visuals=True, + visual_size=320, + ) + self.control_thread = threading.Thread( + target=self._run_controller, + daemon=True, + ) + self.control_thread.start() + + def stop_control(self): + if self.controller is not None: + self.controller.stop() + self._set_button_state("stopping") + + def restart_control(self): + if self.control_thread is not None and self.control_thread.is_alive(): + self.restart_pending = True + self._set_button_state("restarting") + if self.controller is not None: + self.controller.stop() + return + self.restart_pending = False + self.start_control(from_restart=True) + + def _run_controller(self): + try: + self.controller.run() + except Exception as exc: + self._on_status({"state": "error", "action": str(exc), "timestamp": time.time()}) + finally: + self.controller_finished = True + + def _on_status(self, status): + with self.status_lock: + self.latest_status = status + + def _refresh(self): + with self.status_lock: + status = self.latest_status + if not status: + self._update_thread_buttons() + return + self._update_thread_buttons() + if ( + self.control_thread is not None + and self.control_thread.is_alive() + and not self.restart_pending + and self.button_state in ("starting", "restarting") + ): + self._set_button_state("running") + + state = status.get("state", "--") + action = status.get("action", "--") + unit = status.get("normal_unit", "N") + shear_unit = status.get("shear_unit", "N") + self.status_chip.setText(f"state={label_with_cn(state, STATE_TEXT)}") + self.action_label.setText(f"action={label_with_cn(action, ACTION_TEXT)}") + self.manual_hold_button.setText( + "释放" if bool(status.get("manual_hold_active")) else "保持" + ) + + self.left_force.set_value( + fmt(status.get("left_normal"), unit), + ) + self.right_force.set_value( + fmt(status.get("right_normal"), unit), + ) + self.left_shear_box.set_value( + fmt(status.get("left_shear"), shear_unit), + ) + self.right_shear_box.set_value( + fmt(status.get("right_shear"), shear_unit), + ) + self.force_pct_box.set_value( + str(status.get("force_pct", "--")), + ) + self.target_force_box.set_value( + str(status.get("adaptive_target_force", "--")), + ) + self.speed_box.set_value( + str(status.get("speed_pct", "--")), + ) + self.trigger_box.set_value( + f"{int(bool(status.get('trigger')))} / {int(bool(status.get('trigger_armed')))}", + ) + self.contact_box.set_value( + str(int(bool(status.get("grip_contact")))), + ) + self.release_box.set_value( + str(int(bool(status.get("release_armed")))), + ) + + left_sample = status.get("left_sample") or {} + right_sample = status.get("right_sample") or {} + self.left_flow.set_image( + apply_visual_transform(left_sample.get("flow_view"), self.visual_transforms["left"]) + ) + self.right_flow.set_image( + apply_visual_transform(right_sample.get("flow_view"), self.visual_transforms["right"]) + ) + + elapsed = time.time() - self.start_time + self.force_history.append( + ( + elapsed, + force_display_value(status.get("left_normal", 0.0) or 0.0), + force_display_value(status.get("right_normal", 0.0) or 0.0), + force_display_value(status.get("left_shear_x", 0.0) or 0.0), + force_display_value(status.get("left_shear_y", 0.0) or 0.0), + force_display_value(status.get("right_shear_x", 0.0) or 0.0), + force_display_value(status.get("right_shear_y", 0.0) or 0.0), + ) + ) + if len(self.force_history) > PLOT_POINTS: + self.force_history = self.force_history[-PLOT_POINTS:] + self._update_plot() + + def _update_plot(self): + if not self.force_history: + return + data = np.asarray(self.force_history, dtype=float) + x = data[:, 0] + keys = [ + "left_normal", + "right_normal", + "left_shear_x", + "left_shear_y", + "right_shear_x", + "right_shear_y", + ] + for idx, key in enumerate(keys, start=1): + self.curves[key].setData(x, data[:, idx]) + if len(x) >= 2: + x_max = max(6.0, float(np.ceil(x[-1]))) + x_min = max(0.0, x_max - 6.0) + for plot in self.plots: + plot.setXRange(x_min, x_max, padding=0) + + def _update_thread_buttons(self): + if not self.controller_finished: + return + self.controller_finished = False + if self.control_thread is not None: + self.control_thread.join(timeout=0.1) + self.control_thread = None + if self.restart_pending: + self.restart_pending = False + self.start_control(from_restart=True) + return + self._set_button_state("idle") + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Q: + self.close() + return + if event.key() == Qt.Key_Escape: + if self.isFullScreen(): + self.showNormal() + else: + self.showFullScreen() + return + super().keyPressEvent(event) + + def closeEvent(self, event): + self.timer.stop() + self.restart_pending = False + self.stop_control() + if self.control_thread is not None: + self.control_thread.join(timeout=3.0) + event.accept() + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument("--windowed", action="store_true") + ui_args, controller_argv = parser.parse_known_args(argv) + + if _QT_IMPORT_ERROR is not None: + print( + "PyQt5 is required for the visualizer. " + "Install it in the active environment, for example: pip install PyQt5 pyqtgraph" + ) + print(f"Import error: {_QT_IMPORT_ERROR}") + return 1 + if pg is None: + print( + "pyqtgraph is required for the visualizer. " + "Install it in the active environment, for example: pip install pyqtgraph" + ) + print(f"Import error: {_PYQTGRAPH_IMPORT_ERROR}") + return 1 + + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + app = QApplication(sys.argv[:1]) + window = GripperDemo02Viewer(controller_argv) + if ui_args.windowed: + window.resize(1500, 900) + window.show() + else: + window.showFullScreen() + window.start_control() + return app.exec_() + + +if __name__ == "__main__": + mp.freeze_support() + raise SystemExit(main(sys.argv[1:])) diff --git a/gripper/visualizer.py b/gripper/visualizer.py index b0480da..b47a467 100644 --- a/gripper/visualizer.py +++ b/gripper/visualizer.py @@ -1,875 +1,10 @@ -import argparse import multiprocessing as mp import sys -import threading -import time -import cv2 -import numpy as np +from .ui.visualizer import GripperDemo02Viewer, main -def _fix_qt_plugin_path(): - try: - from PyQt5.QtCore import QLibraryInfo - - import os - - os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = QLibraryInfo.location( - QLibraryInfo.PluginsPath - ) - except Exception: - pass - - -_fix_qt_plugin_path() - -_QT_IMPORT_ERROR = None -try: - from PyQt5.QtCore import QEasingCurve, QPropertyAnimation, Qt, QTimer - from PyQt5.QtGui import QFont, QImage, QPixmap - from PyQt5.QtWidgets import ( - QApplication, - QFrame, - QGraphicsOpacityEffect, - QGridLayout, - QHBoxLayout, - QLabel, - QMainWindow, - QPushButton, - QSizePolicy, - QSplitter, - QVBoxLayout, - QWidget, - ) -except Exception as exc: - _QT_IMPORT_ERROR = exc - Qt = QTimer = QPropertyAnimation = QEasingCurve = QFont = QImage = QPixmap = QApplication = None - QGridLayout = QHBoxLayout = QLabel = QPushButton = QSizePolicy = QSplitter = None - QVBoxLayout = QWidget = None - QFrame = QGraphicsOpacityEffect = QMainWindow = object - -try: - import pyqtgraph as pg -except Exception as exc: - pg = None - _PYQTGRAPH_IMPORT_ERROR = exc -else: - _PYQTGRAPH_IMPORT_ERROR = None - -from .main import build_controller - - -PLOT_POINTS = 180 -PLOT_FORCE_Y_MIN_N = 0.0 -PLOT_FORCE_Y_MAX_N = 3.0 -PLOT_SHEAR_Y_MIN_N = -3.0 -PLOT_SHEAR_Y_MAX_N = 3.0 -FORCE_DISPLAY_ZERO_THRESHOLD_N = 0.05 -CURVE_COLOR = "#00E5FF" - - -def cv2_to_pixmap(img_bgr): - if img_bgr is None: - return QPixmap() - if len(img_bgr.shape) == 2: - img_bgr = cv2.cvtColor(img_bgr, cv2.COLOR_GRAY2BGR) - rgb = cv2.cvtColor(np.ascontiguousarray(img_bgr), cv2.COLOR_BGR2RGB) - h, w, ch = rgb.shape - qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy() - return QPixmap.fromImage(qimg) - - -def apply_visual_transform(img, transform): - if img is None: - return None - angle = int(transform.get("rotation_angle", 0)) % 360 - if angle == 90: - img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) - elif angle == 180: - img = cv2.rotate(img, cv2.ROTATE_180) - elif angle == 270: - img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) - - flip_x = bool(transform.get("flip_x")) - flip_y = bool(transform.get("flip_y")) - if flip_x and flip_y: - img = cv2.flip(img, -1) - elif flip_x: - img = cv2.flip(img, 1) - elif flip_y: - img = cv2.flip(img, 0) - return img - - -def force_display_value(value): - value = float(value) - if abs(value) < FORCE_DISPLAY_ZERO_THRESHOLD_N: - return 0.0 - return value - - -def fmt(value, unit="", width=0, precision=3): - if value is None: - text = "--" - else: - value = force_display_value(value) - text = "0" if value == 0.0 else f"{value:.{precision}f}" - if width: - text = text.rjust(width) - return f"{text}{unit}" - - -STATE_TEXT = { - "open_wait": "等待触发", - "closing": "闭合中", - "gripping": "夹持加力", - "hold_check": "夹持检测", - "open_recover": "张开恢复", - "manual_close": "手动闭合", - "manual_hold": "手动保持", - "error": "错误", -} - -ACTION_TEXT = { - "wait": "等待", - "waiting-for-sensor-samples": "等待传感器数据", - "open-wait-stabilizing": "等待稳定基准", - "open-wait-armed": "已就绪", - "side-trigger-close": "侧边触发闭合", - "closing-continue": "继续闭合", - "closing-to-grip": "闭合寻找物体", - "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-ready-position": "张开到位", - "open-recover-quiet": "力稳定恢复", - "open-recover-stabilizing": "恢复稳定中", - "open-recover-wait-force-clear": "等待力恢复", - "open-ready": "恢复完成", - "manual-open": "手动张开", - "manual-close": "手动闭合", - "manual-hold": "手动保持", - "manual-release": "手动释放", -} - - -def label_with_cn(value, mapping): - value = str(value or "--") - cn = mapping.get(value) - if cn is None: - for prefix, text in mapping.items(): - if value.startswith(f"{prefix} "): - cn = text - break - if cn is None: - return value - return f"{value}({cn})" - - -STYLE = """ -QMainWindow { - background-color: #000000; -} -QWidget { - background-color: #000000; - color: #9a9a9a; - font-family: "Microsoft YaHei", "SimHei"; -} -QLabel#Title { - color: #d7d7d7; - font-size: 18px; - font-weight: 600; -} -QLabel#Subtle { - color: #777777; - font-size: 12px; -} -QLabel#MetricLabel { - color: #888888; - font-size: 12px; -} -QLabel#MetricValue { - color: #f2f2f2; - font-family: "Cascadia Code", "Consolas"; - font-size: 22px; - font-weight: 600; -} -QLabel#SmallValue { - color: #d8d8d8; - font-family: "Cascadia Code", "Consolas"; - font-size: 15px; -} -QLabel#StatusChip { - background-color: #101010; - border: 1px solid #303030; - border-radius: 4px; - color: #00e5ff; - font-family: "Cascadia Code", "Consolas"; - font-size: 18px; - font-weight: 600; - padding: 8px 12px; -} -QFrame#StatusPanel { - background-color: #101010; - border: 1px solid #303030; - border-radius: 4px; -} -QLabel#StatusMain { - color: #00e5ff; - font-family: "Cascadia Code", "Consolas"; - font-size: 17px; - font-weight: 600; -} -QLabel#StatusSub { - color: #9a9a9a; - font-family: "Cascadia Code", "Consolas"; - font-size: 12px; -} -QLabel#ActionChip { - background-color: #0b0b0b; - border: 1px solid #262626; - border-radius: 4px; - color: #c8c8c8; - font-family: "Cascadia Code", "Consolas"; - font-size: 13px; - padding: 7px 10px; -} -QLabel#ImagePanel { - background-color: #0b0b0b; - border: 1px solid #262626; - border-radius: 4px; -} -QFrame#Panel { - background-color: #050505; - border: 1px solid #222222; - border-radius: 4px; -} -QPushButton { - background-color: #171717; - color: #d8d8d8; - border: 1px solid #363636; - border-radius: 4px; - padding: 7px 16px; - font-size: 13px; -} -QPushButton:hover { - background-color: #242424; - color: #ffffff; - border: 1px solid #5a5a5a; -} -QPushButton:pressed { - background-color: #0f0f0f; - border: 1px solid #8a8a8a; - padding-top: 8px; - padding-bottom: 6px; -} -QPushButton:disabled { - background-color: #101010; - color: #606060; - border: 1px solid #242424; -} -QPushButton#StartButton { - background-color: #171717; - color: #d8d8d8; - border: 1px solid #363636; -} -QPushButton#StopButton { - background-color: #171717; - color: #d8d8d8; - border: 1px solid #363636; -} -QPushButton#RestartButton { - background-color: #171717; - color: #d8d8d8; - border: 1px solid #363636; -} -QPushButton#ManualButton { - background-color: #171717; - color: #d8d8d8; - border: 1px solid #363636; -} -QPushButton#HoldButton { - background-color: #171717; - color: #d8d8d8; - border: 1px solid #363636; -} -QSplitter::handle { - background-color: #151515; -} -QSplitter::handle:hover { - background-color: #2a2a2a; -} -QSplitter::handle:horizontal { - width: 8px; -} -QSplitter::handle:vertical { - height: 8px; -} -""" - - -if QPushButton is not None: - class AnimatedButton(QPushButton): - def __init__(self, text=""): - super().__init__(text) - self._opacity = QGraphicsOpacityEffect(self) - self._opacity.setOpacity(1.0) - self.setGraphicsEffect(self._opacity) - self._animation = QPropertyAnimation(self._opacity, b"opacity", self) - self._animation.setEasingCurve(QEasingCurve.OutCubic) - - def _animate_opacity(self, target, duration): - self._animation.stop() - self._animation.setDuration(duration) - self._animation.setStartValue(self._opacity.opacity()) - self._animation.setEndValue(target) - self._animation.start() - - def mousePressEvent(self, event): - if self.isEnabled(): - self._animate_opacity(0.72, 70) - super().mousePressEvent(event) - - def mouseReleaseEvent(self, event): - if self.isEnabled(): - self._animate_opacity(1.0, 140) - super().mouseReleaseEvent(event) -else: - AnimatedButton = None - - -class MetricBox(QFrame): - def __init__(self, title, value="--"): - super().__init__() - self.setObjectName("Panel") - layout = QVBoxLayout(self) - layout.setContentsMargins(10, 8, 10, 8) - layout.setSpacing(4) - self.title_label = QLabel(title) - self.title_label.setObjectName("MetricLabel") - self.value_label = QLabel(value) - self.value_label.setObjectName("MetricValue") - layout.addWidget(self.title_label) - layout.addWidget(self.value_label) - - def set_value(self, value, sub=None): - self.value_label.setText(value) - - -class ImageBox(QFrame): - def __init__(self, title, on_rotate=None, on_flip_x=None, on_flip_y=None, on_reset=None): - super().__init__() - self.title = title - self.setObjectName("Panel") - layout = QVBoxLayout(self) - layout.setContentsMargins(8, 8, 8, 8) - layout.setSpacing(6) - self.title_label = QLabel(title) - self.title_label.setObjectName("MetricLabel") - self.image_label = QLabel() - self.image_label.setObjectName("ImagePanel") - self.image_label.setAlignment(Qt.AlignCenter) - self.image_label.setMinimumSize(180, 180) - self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.image_label.setScaledContents(True) - layout.addWidget(self.title_label) - layout.addWidget(self.image_label, 1) - controls = QHBoxLayout() - controls.setSpacing(6) - self.rotate_button = AnimatedButton("旋转") - self.flip_x_button = AnimatedButton("水平") - self.flip_y_button = AnimatedButton("垂直") - self.reset_button = AnimatedButton("重置") - for button, callback in ( - (self.rotate_button, on_rotate), - (self.flip_x_button, on_flip_x), - (self.flip_y_button, on_flip_y), - (self.reset_button, on_reset), - ): - button.setObjectName("ManualButton") - if callback is not None: - button.clicked.connect(callback) - controls.addWidget(button) - layout.addLayout(controls) - - def set_image(self, img): - self.image_label.setPixmap(cv2_to_pixmap(img)) - - def set_transform_label(self, transform): - angle = int(transform.get("rotation_angle", 0)) % 360 - flip_x = "X1" if transform.get("flip_x") else "X0" - flip_y = "Y1" if transform.get("flip_y") else "Y0" - self.title_label.setText(f"{self.title} {angle}deg {flip_x} {flip_y}") - - -class GripperDemo02Viewer(QMainWindow): - def __init__(self, argv=None): - super().__init__() - if _QT_IMPORT_ERROR is not None: - raise RuntimeError(f"PyQt5 import failed: {_QT_IMPORT_ERROR}") - if pg is None: - raise RuntimeError(f"pyqtgraph import failed: {_PYQTGRAPH_IMPORT_ERROR}") - - self.setWindowTitle("Gripper Demo 02") - self.setStyleSheet(STYLE) - self.controller = None - self.control_thread = None - self.latest_status = None - self.status_lock = threading.Lock() - self.controller_finished = False - self.restart_pending = False - self.button_state = "idle" - self.start_time = time.time() - self.force_history = [] - self.argv = argv - self.visual_transforms = { - "left": {"rotation_angle": 0, "flip_x": False, "flip_y": False}, - "right": {"rotation_angle": 0, "flip_x": False, "flip_y": False}, - } - - self._build_ui() - self._configure_plot() - - self.timer = QTimer(self) - self.timer.timeout.connect(self._refresh) - self.timer.start(33) - - def _build_ui(self): - root = QWidget() - self.setCentralWidget(root) - outer = QVBoxLayout(root) - outer.setContentsMargins(8, 8, 8, 8) - outer.setSpacing(8) - - top = QHBoxLayout() - top.setSpacing(8) - title = QLabel("Orisys 夹爪") - title.setObjectName("Title") - self.status_panel = QFrame() - self.status_panel.setObjectName("StatusPanel") - status_layout = QVBoxLayout(self.status_panel) - status_layout.setContentsMargins(10, 6, 10, 6) - status_layout.setSpacing(1) - self.status_chip = QLabel("state=--") - self.status_chip.setObjectName("StatusMain") - self.action_label = QLabel("action=--") - self.action_label.setObjectName("StatusSub") - status_layout.addWidget(self.status_chip) - status_layout.addWidget(self.action_label) - self.start_button = AnimatedButton("启动") - self.start_button.setObjectName("StartButton") - self.start_button.clicked.connect(self.start_control) - self.stop_button = AnimatedButton("停止") - self.stop_button.setObjectName("StopButton") - self.stop_button.clicked.connect(self.stop_control) - self.stop_button.setEnabled(False) - self.restart_button = AnimatedButton("重启") - self.restart_button.setObjectName("RestartButton") - self.restart_button.clicked.connect(self.restart_control) - self.manual_open_button = AnimatedButton("夹爪张开") - self.manual_open_button.setObjectName("ManualButton") - self.manual_open_button.clicked.connect(lambda: self.send_manual_command("open")) - self.manual_close_button = AnimatedButton("夹爪闭合") - self.manual_close_button.setObjectName("ManualButton") - self.manual_close_button.clicked.connect(lambda: self.send_manual_command("close")) - self.manual_hold_button = AnimatedButton("保持") - self.manual_hold_button.setObjectName("HoldButton") - self.manual_hold_button.clicked.connect(self.toggle_manual_hold) - self._set_button_state("idle") - top.addWidget(title) - top.addWidget(self.status_panel) - top.addStretch(1) - top.addWidget(self.start_button) - top.addWidget(self.stop_button) - top.addWidget(self.restart_button) - top.addWidget(self.manual_open_button) - top.addWidget(self.manual_close_button) - top.addWidget(self.manual_hold_button) - outer.addLayout(top) - - main_splitter = QSplitter(Qt.Horizontal) - main_splitter.setChildrenCollapsible(False) - outer.addWidget(main_splitter, 1) - - flow_splitter = QSplitter(Qt.Vertical) - flow_splitter.setChildrenCollapsible(False) - self.left_flow = ImageBox( - "左侧光流", - on_rotate=lambda: self.adjust_visual_transform("left", "rotate"), - on_flip_x=lambda: self.adjust_visual_transform("left", "flip_x"), - on_flip_y=lambda: self.adjust_visual_transform("left", "flip_y"), - on_reset=lambda: self.adjust_visual_transform("left", "reset"), - ) - self.right_flow = ImageBox( - "右侧光流", - on_rotate=lambda: self.adjust_visual_transform("right", "rotate"), - on_flip_x=lambda: self.adjust_visual_transform("right", "flip_x"), - on_flip_y=lambda: self.adjust_visual_transform("right", "flip_y"), - on_reset=lambda: self.adjust_visual_transform("right", "reset"), - ) - self.left_flow.set_transform_label(self.visual_transforms["left"]) - self.right_flow.set_transform_label(self.visual_transforms["right"]) - flow_splitter.addWidget(self.left_flow) - flow_splitter.addWidget(self.right_flow) - flow_splitter.setStretchFactor(0, 1) - flow_splitter.setStretchFactor(1, 1) - flow_splitter.setSizes([360, 360]) - main_splitter.addWidget(flow_splitter) - - side_widget = QWidget() - side = QVBoxLayout(side_widget) - side.setContentsMargins(0, 0, 0, 0) - side.setSpacing(8) - main_splitter.addWidget(side_widget) - main_splitter.setStretchFactor(0, 2) - main_splitter.setStretchFactor(1, 4) - main_splitter.setSizes([520, 980]) - - metrics_grid = QGridLayout() - metrics_grid.setSpacing(8) - side.addLayout(metrics_grid) - self.left_force = MetricBox("左法向力", "--") - self.right_force = MetricBox("右法向力", "--") - self.left_shear_box = MetricBox("左切向力", "--") - self.right_shear_box = MetricBox("右切向力", "--") - self.force_pct_box = MetricBox("夹爪当前力比例(%)", "--") - self.target_force_box = MetricBox("夹爪目标力比例(%)", "--") - self.speed_box = MetricBox("速度", "--") - self.trigger_box = MetricBox("是否触发/系统是否允许触发", "--") - self.contact_box = MetricBox("接触", "--") - self.release_box = MetricBox("释放检测", "--") - boxes = [ - self.left_force, - self.right_force, - self.left_shear_box, - self.right_shear_box, - self.force_pct_box, - self.target_force_box, - self.speed_box, - self.trigger_box, - self.contact_box, - self.release_box, - ] - for idx, box in enumerate(boxes): - metrics_grid.addWidget(box, idx // 2, idx % 2) - - pg.setConfigOption("background", "#000000") - pg.setConfigOption("foreground", "#777777") - pg.setConfigOptions(antialias=True) - self.plot_widget = pg.GraphicsLayoutWidget() - self.plot_widget.setMinimumHeight(240) - side.addWidget(self.plot_widget, 1) - - def _configure_plot(self): - self.plots = [] - self.curves = {} - plot_specs = [ - ("left_normal", "左法向力", "#00E5FF", PLOT_FORCE_Y_MIN_N, PLOT_FORCE_Y_MAX_N), - ("right_normal", "右法向力", "#ffb000", PLOT_FORCE_Y_MIN_N, PLOT_FORCE_Y_MAX_N), - ("left_shear_x", "左切向 X", "#61d394", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N), - ("left_shear_y", "左切向 Y", "#f07178", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N), - ("right_shear_x", "右切向 X", "#8ab4f8", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N), - ("right_shear_y", "右切向 Y", "#d19a66", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N), - ] - for idx, (key, title, color, y_min, y_max) in enumerate(plot_specs): - row = idx // 2 - col = idx % 2 - plot = self.plot_widget.addPlot(row=row, col=col) - plot.setTitle(title, color="#999999", size="11pt") - plot.titleLabel.setFont(QFont("SimHei", 11)) - plot.showAxis("top", False) - plot.showAxis("right", False) - plot.showGrid(x=False, y=True, alpha=0.12) - plot.setLabel("bottom", "时间", units="s") - plot.setLabel("left", "力", units="N") - plot.getAxis("bottom").setTickSpacing(major=1.0, minor=1.0) - plot.setYRange(y_min, y_max, padding=0) - plot.setLimits(yMin=y_min, yMax=y_max) - plot.enableAutoRange(axis=pg.ViewBox.YAxis, enable=False) - self.plots.append(plot) - self.curves[key] = plot.plot(pen=pg.mkPen(color, width=2)) - - def _set_button_state(self, state): - labels = { - "idle": ("启动", "停止", "重启"), - "starting": ("启动中", "停止", "重启"), - "running": ("启动", "停止", "重启"), - "stopping": ("启动", "停止中", "重启"), - "restarting": ("启动", "停止", "重启中"), - } - enabled = { - "idle": (True, False, True), - "starting": (False, True, False), - "running": (False, True, True), - "stopping": (False, False, False), - "restarting": (False, False, False), - } - self.button_state = state - start_text, stop_text, restart_text = labels[state] - start_enabled, stop_enabled, restart_enabled = enabled[state] - self.start_button.setText(start_text) - self.stop_button.setText(stop_text) - self.restart_button.setText(restart_text) - self.start_button.setEnabled(start_enabled) - self.stop_button.setEnabled(stop_enabled) - self.restart_button.setEnabled(restart_enabled) - manual_enabled = state == "running" - self.manual_open_button.setEnabled(manual_enabled) - self.manual_close_button.setEnabled(manual_enabled) - self.manual_hold_button.setEnabled(manual_enabled) - - def adjust_visual_transform(self, side, action): - transform = self.visual_transforms[side] - if action == "rotate": - transform["rotation_angle"] = (int(transform["rotation_angle"]) + 90) % 360 - elif action == "flip_x": - transform["flip_x"] = not bool(transform["flip_x"]) - elif action == "flip_y": - transform["flip_y"] = not bool(transform["flip_y"]) - elif action == "reset": - transform.update({"rotation_angle": 0, "flip_x": False, "flip_y": False}) - - if side == "left": - self.left_flow.set_transform_label(transform) - else: - self.right_flow.set_transform_label(transform) - - def send_manual_command(self, command): - if self.controller is None or self.control_thread is None or not self.control_thread.is_alive(): - return - try: - self.controller.request_manual_command(command) - except Exception as exc: - print(f"manual command failed: {exc}") - - def toggle_manual_hold(self): - command = "release" if self.manual_hold_button.text() == "释放" else "hold" - self.send_manual_command(command) - - def start_control(self, from_restart=False): - if self.control_thread is not None and self.control_thread.is_alive(): - return - self.force_history.clear() - self.start_time = time.time() - self._set_button_state("restarting" if from_restart else "starting") - self.controller_finished = False - - self.controller = build_controller( - self.argv, - status_callback=self._on_status, - include_visuals=True, - visual_size=320, - ) - self.control_thread = threading.Thread( - target=self._run_controller, - daemon=True, - ) - self.control_thread.start() - - def stop_control(self): - if self.controller is not None: - self.controller.stop() - self._set_button_state("stopping") - - def restart_control(self): - if self.control_thread is not None and self.control_thread.is_alive(): - self.restart_pending = True - self._set_button_state("restarting") - if self.controller is not None: - self.controller.stop() - return - self.restart_pending = False - self.start_control(from_restart=True) - - def _run_controller(self): - try: - self.controller.run() - except Exception as exc: - self._on_status({"state": "error", "action": str(exc), "timestamp": time.time()}) - finally: - self.controller_finished = True - - def _on_status(self, status): - with self.status_lock: - self.latest_status = status - - def _refresh(self): - with self.status_lock: - status = self.latest_status - if not status: - self._update_thread_buttons() - return - self._update_thread_buttons() - if ( - self.control_thread is not None - and self.control_thread.is_alive() - and not self.restart_pending - and self.button_state in ("starting", "restarting") - ): - self._set_button_state("running") - - state = status.get("state", "--") - action = status.get("action", "--") - unit = status.get("normal_unit", "N") - shear_unit = status.get("shear_unit", "N") - self.status_chip.setText(f"state={label_with_cn(state, STATE_TEXT)}") - self.action_label.setText(f"action={label_with_cn(action, ACTION_TEXT)}") - self.manual_hold_button.setText( - "释放" if bool(status.get("manual_hold_active")) else "保持" - ) - - self.left_force.set_value( - fmt(status.get("left_normal"), unit), - ) - self.right_force.set_value( - fmt(status.get("right_normal"), unit), - ) - self.left_shear_box.set_value( - fmt(status.get("left_shear"), shear_unit), - ) - self.right_shear_box.set_value( - fmt(status.get("right_shear"), shear_unit), - ) - self.force_pct_box.set_value( - str(status.get("force_pct", "--")), - ) - self.target_force_box.set_value( - str(status.get("adaptive_target_force", "--")), - ) - self.speed_box.set_value( - str(status.get("speed_pct", "--")), - ) - self.trigger_box.set_value( - f"{int(bool(status.get('trigger')))} / {int(bool(status.get('trigger_armed')))}", - ) - self.contact_box.set_value( - str(int(bool(status.get("grip_contact")))), - ) - self.release_box.set_value( - str(int(bool(status.get("release_armed")))), - ) - - left_sample = status.get("left_sample") or {} - right_sample = status.get("right_sample") or {} - self.left_flow.set_image( - apply_visual_transform(left_sample.get("flow_view"), self.visual_transforms["left"]) - ) - self.right_flow.set_image( - apply_visual_transform(right_sample.get("flow_view"), self.visual_transforms["right"]) - ) - - elapsed = time.time() - self.start_time - self.force_history.append( - ( - elapsed, - force_display_value(status.get("left_normal", 0.0) or 0.0), - force_display_value(status.get("right_normal", 0.0) or 0.0), - force_display_value(status.get("left_shear_x", 0.0) or 0.0), - force_display_value(status.get("left_shear_y", 0.0) or 0.0), - force_display_value(status.get("right_shear_x", 0.0) or 0.0), - force_display_value(status.get("right_shear_y", 0.0) or 0.0), - ) - ) - if len(self.force_history) > PLOT_POINTS: - self.force_history = self.force_history[-PLOT_POINTS:] - self._update_plot() - - def _update_plot(self): - if not self.force_history: - return - data = np.asarray(self.force_history, dtype=float) - x = data[:, 0] - keys = [ - "left_normal", - "right_normal", - "left_shear_x", - "left_shear_y", - "right_shear_x", - "right_shear_y", - ] - for idx, key in enumerate(keys, start=1): - self.curves[key].setData(x, data[:, idx]) - if len(x) >= 2: - x_max = max(6.0, float(np.ceil(x[-1]))) - x_min = max(0.0, x_max - 6.0) - for plot in self.plots: - plot.setXRange(x_min, x_max, padding=0) - - def _update_thread_buttons(self): - if not self.controller_finished: - return - self.controller_finished = False - if self.control_thread is not None: - self.control_thread.join(timeout=0.1) - self.control_thread = None - if self.restart_pending: - self.restart_pending = False - self.start_control(from_restart=True) - return - self._set_button_state("idle") - - def keyPressEvent(self, event): - if event.key() == Qt.Key_Q: - self.close() - return - if event.key() == Qt.Key_Escape: - if self.isFullScreen(): - self.showNormal() - else: - self.showFullScreen() - return - super().keyPressEvent(event) - - def closeEvent(self, event): - self.timer.stop() - self.restart_pending = False - self.stop_control() - if self.control_thread is not None: - self.control_thread.join(timeout=3.0) - event.accept() - - -def main(argv=None): - parser = argparse.ArgumentParser() - parser.add_argument("--windowed", action="store_true") - ui_args, controller_argv = parser.parse_known_args(argv) - - if _QT_IMPORT_ERROR is not None: - print( - "PyQt5 is required for the visualizer. " - "Install it in the active environment, for example: pip install PyQt5 pyqtgraph" - ) - print(f"Import error: {_QT_IMPORT_ERROR}") - return 1 - if pg is None: - print( - "pyqtgraph is required for the visualizer. " - "Install it in the active environment, for example: pip install pyqtgraph" - ) - print(f"Import error: {_PYQTGRAPH_IMPORT_ERROR}") - return 1 - - QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) - QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) - app = QApplication(sys.argv[:1]) - window = GripperDemo02Viewer(controller_argv) - if ui_args.windowed: - window.resize(1500, 900) - window.show() - else: - window.showFullScreen() - window.start_control() - return app.exec_() +__all__ = ["GripperDemo02Viewer", "main"] if __name__ == "__main__": diff --git a/知行夹爪/UServoController_1.0.0.8Beta/CommonCommunication.dll b/知行夹爪/UServoController_1.0.0.8Beta/CommonCommunication.dll new file mode 100644 index 0000000..d4f065f Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/CommonCommunication.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/CommonUI.dll b/知行夹爪/UServoController_1.0.0.8Beta/CommonUI.dll new file mode 100644 index 0000000..c434545 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/CommonUI.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/D3Dcompiler_47.dll b/知行夹爪/UServoController_1.0.0.8Beta/D3Dcompiler_47.dll new file mode 100644 index 0000000..56512f5 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/D3Dcompiler_47.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6Charts.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Charts.dll new file mode 100644 index 0000000..ab73fee Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Charts.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6Core.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Core.dll new file mode 100644 index 0000000..05fbf30 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Core.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6Gui.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Gui.dll new file mode 100644 index 0000000..bc75267 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Gui.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6Network.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Network.dll new file mode 100644 index 0000000..50ff106 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Network.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6OpenGL.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6OpenGL.dll new file mode 100644 index 0000000..952a4e9 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6OpenGL.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6OpenGLWidgets.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6OpenGLWidgets.dll new file mode 100644 index 0000000..f8e7629 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6OpenGLWidgets.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6Pdf.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Pdf.dll new file mode 100644 index 0000000..1adf7ce Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Pdf.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6SerialPort.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6SerialPort.dll new file mode 100644 index 0000000..8cf3ed3 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6SerialPort.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6Sql.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Sql.dll new file mode 100644 index 0000000..ad31c8a Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Sql.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6Svg.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Svg.dll new file mode 100644 index 0000000..de03706 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Svg.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Qt6Widgets.dll b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Widgets.dll new file mode 100644 index 0000000..4698210 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/Qt6Widgets.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/Settings.ini b/知行夹爪/UServoController_1.0.0.8Beta/Settings.ini new file mode 100644 index 0000000..551c830 --- /dev/null +++ b/知行夹爪/UServoController_1.0.0.8Beta/Settings.ini @@ -0,0 +1,6 @@ +[AppSettings] +Language=zh_CN +BootAnimationFlag=1 + +[AppWindow] +Theme=0 diff --git a/知行夹爪/UServoController_1.0.0.8Beta/UServoController.exe b/知行夹爪/UServoController_1.0.0.8Beta/UServoController.exe new file mode 100644 index 0000000..2ba9501 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/UServoController.exe differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/database/database.db b/知行夹爪/UServoController_1.0.0.8Beta/database/database.db new file mode 100644 index 0000000..324c43f Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/database/database.db differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/generic/qtuiotouchplugin.dll b/知行夹爪/UServoController_1.0.0.8Beta/generic/qtuiotouchplugin.dll new file mode 100644 index 0000000..d95f1e9 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/generic/qtuiotouchplugin.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/iconengines/qsvgicon.dll b/知行夹爪/UServoController_1.0.0.8Beta/iconengines/qsvgicon.dll new file mode 100644 index 0000000..8da16eb Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/iconengines/qsvgicon.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qgif.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qgif.dll new file mode 100644 index 0000000..769f809 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qgif.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qicns.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qicns.dll new file mode 100644 index 0000000..d8a368d Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qicns.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qico.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qico.dll new file mode 100644 index 0000000..139b283 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qico.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qjpeg.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qjpeg.dll new file mode 100644 index 0000000..e6b5c25 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qjpeg.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qpdf.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qpdf.dll new file mode 100644 index 0000000..7a784a0 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qpdf.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qsvg.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qsvg.dll new file mode 100644 index 0000000..af214f9 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qsvg.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qtga.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qtga.dll new file mode 100644 index 0000000..dd9658b Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qtga.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qtiff.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qtiff.dll new file mode 100644 index 0000000..c9aa7ae Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qtiff.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qwbmp.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qwbmp.dll new file mode 100644 index 0000000..2511337 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qwbmp.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qwebp.dll b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qwebp.dll new file mode 100644 index 0000000..d1dfd3f Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/imageformats/qwebp.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/networkinformation/qnetworklistmanager.dll b/知行夹爪/UServoController_1.0.0.8Beta/networkinformation/qnetworklistmanager.dll new file mode 100644 index 0000000..1406676 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/networkinformation/qnetworklistmanager.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/opengl32sw.dll b/知行夹爪/UServoController_1.0.0.8Beta/opengl32sw.dll new file mode 100644 index 0000000..1bf3581 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/opengl32sw.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/platforms/qwindows.dll b/知行夹爪/UServoController_1.0.0.8Beta/platforms/qwindows.dll new file mode 100644 index 0000000..e46e0ea Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/platforms/qwindows.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/readme.txt b/知行夹爪/UServoController_1.0.0.8Beta/readme.txt new file mode 100644 index 0000000..e594254 --- /dev/null +++ b/知行夹爪/UServoController_1.0.0.8Beta/readme.txt @@ -0,0 +1,17 @@ +1.0.0.5_Alpha 7.24 +1. 内部版本发布 + +1.0.0.6_Alpha 7.28 +1. 中英文翻译适配与软件配置持久化 + +1.0.0.7_Alpha 9.11 +1. 多段点位运行状态(0x0421)延时状态值更新为0x11 +2. 完善速度映射、电流映射在多段点位表与多段点位示教功能中的应用 +3. 往复运行在运行过程中断开连接后,再次点击运行,第一次点击运行失效问题处理 +4. 修复产品ID字符截断错误问题(UUID可能出现0000情况,无法通过特定字符进行截断),已固定6个寄存器24个十六进制数字字符 +5. 增加配置文件导出与导入功能(实验性功能),导入前需要关闭高级标签中的数据读取功能 + +1.0.0.8_Alpha 10.24 +1. 增加示波器时间轴功能(精确到ms),增加鼠标标尺交互功能,停止读取逻辑梳理 +2. 优化配置文件导入功能(稳定版本),无需在导入前关闭高级标签中的数据读取功能 +3. About页面新增产品SN码读写功能 \ No newline at end of file diff --git a/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlite.dll b/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlite.dll new file mode 100644 index 0000000..9efa4d7 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlite.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlmimer.dll b/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlmimer.dll new file mode 100644 index 0000000..fb320e1 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlmimer.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlodbc.dll b/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlodbc.dll new file mode 100644 index 0000000..fb8ead8 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlodbc.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlpsql.dll b/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlpsql.dll new file mode 100644 index 0000000..2933d69 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/sqldrivers/qsqlpsql.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/styles/qwindowsvistastyle.dll b/知行夹爪/UServoController_1.0.0.8Beta/styles/qwindowsvistastyle.dll new file mode 100644 index 0000000..c50226b Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/styles/qwindowsvistastyle.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/tls/qcertonlybackend.dll b/知行夹爪/UServoController_1.0.0.8Beta/tls/qcertonlybackend.dll new file mode 100644 index 0000000..56978b9 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/tls/qcertonlybackend.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/tls/qopensslbackend.dll b/知行夹爪/UServoController_1.0.0.8Beta/tls/qopensslbackend.dll new file mode 100644 index 0000000..98ae3d7 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/tls/qopensslbackend.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/tls/qschannelbackend.dll b/知行夹爪/UServoController_1.0.0.8Beta/tls/qschannelbackend.dll new file mode 100644 index 0000000..6e8b777 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/tls/qschannelbackend.dll differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ar.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ar.qm new file mode 100644 index 0000000..32861b8 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ar.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_bg.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_bg.qm new file mode 100644 index 0000000..faeb167 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_bg.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ca.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ca.qm new file mode 100644 index 0000000..78c3625 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ca.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_cs.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_cs.qm new file mode 100644 index 0000000..459ef26 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_cs.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_da.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_da.qm new file mode 100644 index 0000000..4ede24b Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_da.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_de.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_de.qm new file mode 100644 index 0000000..cb330ef Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_de.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_en.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_en.qm new file mode 100644 index 0000000..937ea3e Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_en.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_es.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_es.qm new file mode 100644 index 0000000..a0ab256 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_es.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_fa.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_fa.qm new file mode 100644 index 0000000..aadc0c1 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_fa.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_fi.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_fi.qm new file mode 100644 index 0000000..934aecd Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_fi.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_fr.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_fr.qm new file mode 100644 index 0000000..2e977a4 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_fr.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_gd.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_gd.qm new file mode 100644 index 0000000..3fe3841 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_gd.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_he.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_he.qm new file mode 100644 index 0000000..95ed0c7 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_he.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_hr.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_hr.qm new file mode 100644 index 0000000..4ed06fb Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_hr.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_hu.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_hu.qm new file mode 100644 index 0000000..1691a02 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_hu.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_it.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_it.qm new file mode 100644 index 0000000..3785a6f Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_it.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ja.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ja.qm new file mode 100644 index 0000000..0291699 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ja.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ko.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ko.qm new file mode 100644 index 0000000..ce9aaca Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ko.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_lv.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_lv.qm new file mode 100644 index 0000000..f88a761 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_lv.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_nl.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_nl.qm new file mode 100644 index 0000000..de4e74a Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_nl.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_nn.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_nn.qm new file mode 100644 index 0000000..506ec45 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_nn.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_pl.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_pl.qm new file mode 100644 index 0000000..7edf706 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_pl.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_pt_BR.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_pt_BR.qm new file mode 100644 index 0000000..6fabd0e Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_pt_BR.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ru.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ru.qm new file mode 100644 index 0000000..b608e1d Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_ru.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_sk.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_sk.qm new file mode 100644 index 0000000..55a377e Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_sk.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_tr.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_tr.qm new file mode 100644 index 0000000..ce17f3d Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_tr.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_uk.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_uk.qm new file mode 100644 index 0000000..160c7db Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_uk.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_zh_CN.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_zh_CN.qm new file mode 100644 index 0000000..7866a38 Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_zh_CN.qm differ diff --git a/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_zh_TW.qm b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_zh_TW.qm new file mode 100644 index 0000000..f32a72f Binary files /dev/null and b/知行夹爪/UServoController_1.0.0.8Beta/translations/qt_zh_TW.qm differ diff --git a/知行夹爪/串口驱动CH341/CH341SER.EXE b/知行夹爪/串口驱动CH341/CH341SER.EXE new file mode 100644 index 0000000..61ffc32 Binary files /dev/null and b/知行夹爪/串口驱动CH341/CH341SER.EXE differ