init
This commit is contained in:
+44
@@ -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/
|
||||
@@ -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`。
|
||||
|
||||
## 日志字段
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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/<name> before building.")
|
||||
args = parser.parse_args()
|
||||
build(args.name, args.clean_dist)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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:]))
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.2 MiB |
+3
-3
@@ -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,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
"""Shared path and utility helpers."""
|
||||
@@ -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)
|
||||
@@ -0,0 +1,4 @@
|
||||
from .settings import Demo02Config
|
||||
|
||||
|
||||
__all__ = ["Demo02Config"]
|
||||
@@ -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,
|
||||
@@ -0,0 +1,5 @@
|
||||
from .controller import SideTriggerGripController
|
||||
from .feedback import TactileFeedbackProcessor
|
||||
|
||||
|
||||
__all__ = ["SideTriggerGripController", "TactileFeedbackProcessor"]
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
from .filters import clamp
|
||||
from ..common.filters import clamp
|
||||
|
||||
|
||||
class SideTriggerGripController:
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
|
||||
from .filters import ExponentialFilter
|
||||
from ..common.filters import ExponentialFilter
|
||||
|
||||
|
||||
def shear_magnitude(sample):
|
||||
@@ -0,0 +1,4 @@
|
||||
from .gripper_client import GripperClient
|
||||
|
||||
|
||||
__all__ = ["GripperClient"]
|
||||
@@ -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:
|
||||
+1
-55
@@ -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__":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .force_reader import TwoSensorForceReader
|
||||
|
||||
|
||||
__all__ = ["TwoSensorForceReader"]
|
||||
@@ -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,
|
||||
@@ -0,0 +1 @@
|
||||
"""User interface package."""
|
||||
@@ -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:]))
|
||||
+2
-867
@@ -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__":
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
[AppSettings]
|
||||
Language=zh_CN
|
||||
BootAnimationFlag=1
|
||||
|
||||
[AppWindow]
|
||||
Theme=0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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码读写功能
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user