update sensor bench

This commit is contained in:
ykcheungab
2026-09-01 11:14:56 +08:00
parent ef3ee84a68
commit 8cacb12abc
55 changed files with 4032 additions and 323 deletions
+57
View File
@@ -0,0 +1,57 @@
# Sensor Bench Operator (Internal)
Private operator / inspection workflow UI. **Do not publish** this folder or its
resources in customer EXEs, public docs, or release zips.
## What lives here
- Workflow UI (`sensor_bench_operator.app.MainWindow`)
- Inspection steps (`steps/default.json`)
- Lab component options (`config/options.json`, including Chinese lab labels)
- Operator locale catalogs (`config/locales/`)
The public exhibition app lives under the repository root (`run.py`,
`src/sensor_bench/exhibition_app.py`) and must not import this package.
## Run (source)
From the repository root:
```powershell
python internal/run_operator.py
```
Or:
```bat
.\internal\run_operator.bat
```
Requires the public `sensor-bench` package sources on `PYTHONPATH` (the launcher
adds `src/` and `internal/src/` automatically) and an importable `orisys`.
## Build private EXE
Uses the same conda env as the public build (`sensor-bench-build-py310`):
```bat
.\build\create_env.bat
.\build\build_operator_exe.bat
```
Output (internal only — do not ship to customers):
```text
dist/SensorBenchOperator/SensorBenchOperator.exe
```
The build copies public sensor configs plus operator `options.json`, locales, and
`steps/` beside the EXE. See [`../build/README.md`](../build/README.md).
## Relationship to public Sensor Bench
```text
orisys (public SDK)
├── sensor-bench → dist/SensorBench/ (public exhibition EXE)
└── internal/ → dist/SensorBenchOperator/ (private operator EXE)
```
+166
View File
@@ -0,0 +1,166 @@
{
"app": {
"title": "Sensor Bench",
"language": "Language",
"lang_zh": "中文",
"lang_en": "English"
},
"panels": {
"instructions": "Instructions",
"options": "Sensor Component Types",
"camera": "Camera (Orisys SDK)",
"recording": "Recording",
"raw": "Raw Image (sensor.frame)",
"processed": "Processed Image (Sensor.get_img)",
"force": "Force Curves"
},
"labels": {
"sensor_type": "Sensor type",
"elastomer": "Elastomer",
"cmos": "CMOS",
"marker": "Marker",
"indentor": "Indenter",
"platform": "Platform",
"camera_index": "Camera index",
"backend": "Backend",
"fixed_mode": "Fixed mode: {mode}",
"fixed_mode_unset": "Fixed mode: set config path",
"fixed_mode_invalid": "Fixed mode: invalid config ({error})",
"requested": "Requested",
"actual": "Actual",
"capture_fps": "Capture FPS",
"render_fps": "Render FPS",
"proc_ms": "Proc ms",
"fn": "FN",
"fx": "FX",
"fy": "FY",
"no_preview": "No preview",
"processed_placeholder": "Start Preview to show Orisys output",
"not_recording": "Not recording",
"force_plot": "Force",
"config_dir_tip": "Installed configs dir: {path}\nSelected: {selected}",
"config_dir_missing": "Installed Orisys configs directory not found. Use Config… to browse a local JSON."
},
"buttons": {
"config": "Config…",
"cal": "Cal…",
"start_preview": "Start Preview",
"stop_preview": "Stop Preview",
"browse": "Browse…",
"start_record": "Start Record",
"stop_record": "Stop Record",
"next": "Next",
"revert": "Revert",
"reset": "New Sample / Reset"
},
"placeholders": {
"config": "Select an installed Orisys config, or browse a JSON file",
"cal": "Optional calibration .npy"
},
"dialogs": {
"select_config": "Select config JSON",
"select_cal": "Select calibration",
"select_recordings": "Select recordings folder",
"orisys_title": "Orisys",
"orisys_required": "orisys SDK is required for preview.\n\n{detail}",
"config_title": "Config",
"config_required": "Select a DDJX01 config JSON first.",
"config_invalid": "Config could not be loaded.",
"capture_title": "Capture",
"recording_title": "Recording",
"no_frame": "No frame yet."
},
"status": {
"ready": "Ready — Start Preview uses Orisys DDJX01 fixed format.",
"orisys_ok": "orisys available (version {version})",
"orisys_missing": "orisys not importable — add orisys-sdk/src to PYTHONPATH.",
"opening": "Opening Orisys ({mode})…",
"preview_stopped": "Preview stopped.",
"capture_error": "Capture error: {message}",
"frame_info": "{raw_w}x{raw_h} raw / {proc_w}x{proc_h} processed @ {fps:.1f} FPS",
"recording": "Recording… {name}\nOptions: sensor={sensor_type}, elastomer={elastomer}, cmos={cmos}, marker={marker}, indenter={indentor}, platform={platform}",
"recording_frames": "Recording… {name}\nFrames: {count}",
"saved": "Saved {count} frames → {path}",
"workflow_done": "All steps completed. Click New Sample / Reset for the next unit."
},
"exhibition": {
"title": "Sensor Bench Exhibition",
"btn_search": "Search",
"btn_camera": "Camera",
"btn_config": "Config",
"btn_start": "Start",
"btn_starting": "Starting…",
"btn_stop": "Pause",
"btn_reset": "Reset",
"btn_start_record": "Start Record",
"btn_stop_record": "Stop Record",
"show_log": "Show log panel",
"view": "View",
"view_pure_color": "Arrows on solid bg",
"view_captured": "Arrows on captured",
"view_processed": "Processed image",
"view_raw": "Raw image",
"sensor_type": "Sensor",
"sensor_orisys": "ORY-BOX/ORY-MINI",
"sensor_dd06": "ORY-X",
"sensor_switched": "Switched sensor type: {sensor}",
"placeholder_camera": "Camera index or video path",
"dialog_video": "Select video file",
"camera_found": "Found {count} camera(s): {names}",
"camera_none": "No usable camera found. Click Search to rescan, or use Camera to pick a video file.",
"plot_fn": "Normal Force FN",
"plot_fx": "Shear Force FX",
"plot_fy": "Shear Force FY",
"reset_done": "Tracker reset.",
"reset_need_start": "Start the sensor before resetting the tracker.",
"reset_failed": "Reset failed: {message}",
"force_fn_n": "Normal force (N)={value:.6f}",
"recording_started": "Recording started: {name}",
"hints": "Keys: Q quit | R reset tracker | S start record | E stop record | ESC fullscreen",
"menu_help": "Help",
"menu_about": "About",
"about_title": "About Sensor Bench",
"about_body": "Sensor Bench Exhibition\nVersion: {app_version}\nOrisys: {orisys_version}\n\nOrisys-Tech",
"config_dir_tip": "Installed configs dir: {path}\nSelected: {selected}",
"config_dir_missing": "Installed Orisys configs directory not found. Use Config… to browse a local JSON."
},
"workflow": {
"title": "Sensor Bench Inspection Workflow",
"current_marker": "Current",
"done_marker": "Done",
"steps": {
"power_on": {
"title": "Power on and connect",
"body": "Connect the sensor USB cable and wait until Windows recognizes the device."
},
"select_camera": {
"title": "Start Orisys preview",
"body": "Confirm camera index (usually 0) and DDJX01 config, then click Start Preview. Format is fixed by the Orisys config (1920x1080 @ 30)."
},
"set_options": {
"title": "Set sensor options",
"body": "Choose elastomer type, CMOS type, and marker type that match the physical unit under test."
},
"check_preview": {
"title": "Check raw and processed preview",
"body": "Confirm raw frame is ~1920x1080 and processed Orisys image is ~365x365. Inspect focus, lighting, and image cleanliness."
},
"record": {
"title": "Record raw data",
"body": "Choose a storage folder, start recording, exercise the sensor, then stop recording."
},
"review_curves": {
"title": "Review performance curves",
"body": "Watch capture/render FPS and force curves for drops or unstable exposure before signing off."
},
"sensitivity": {
"title": "Sensitivity test",
"body": "Choose a small indenter and apply a 2g weight, then watch for a clear force-curve change."
},
"max_load": {
"title": "Maximum load test",
"body": "Choose a flat plate indenter and apply weights from 10g to 500g to watch for curve changes. Use 10g to 500g for MINI and 2kg for BOX."
}
}
}
}
+166
View File
@@ -0,0 +1,166 @@
{
"app": {
"title": "传感器检测台",
"language": "语言",
"lang_zh": "中文",
"lang_en": "English"
},
"panels": {
"instructions": "操作步骤",
"options": "传感器组件类型",
"camera": "相机(Orisys SDK)",
"recording": "录制",
"raw": "原始图像(sensor.frame)",
"processed": "处理后图像(Sensor.get_img)",
"force": "力曲线"
},
"labels": {
"sensor_type": "传感器类型",
"elastomer": "弹性体",
"cmos": "CMOS",
"marker": "图案",
"indentor": "压头",
"platform": "平台",
"camera_index": "相机索引",
"backend": "后端",
"fixed_mode": "固定模式:{mode}",
"fixed_mode_unset": "固定模式:请设置配置路径",
"fixed_mode_invalid": "固定模式:配置无效({error})",
"requested": "请求",
"actual": "实际",
"capture_fps": "采集 FPS",
"render_fps": "渲染 FPS",
"proc_ms": "处理 ms",
"fn": "FN",
"fx": "FX",
"fy": "FY",
"no_preview": "暂无预览",
"processed_placeholder": "点击开始预览以显示 Orisys 输出",
"not_recording": "未在录制",
"force_plot": "力",
"config_dir_tip": "已安装配置目录:{path}\n当前:{selected}",
"config_dir_missing": "未找到已安装的 Orisys 配置目录。可用“配置…”浏览本地 JSON。"
},
"buttons": {
"config": "配置…",
"cal": "标定…",
"start_preview": "开始预览",
"stop_preview": "停止预览",
"browse": "浏览…",
"start_record": "开始录制",
"stop_record": "停止录制",
"next": "下一步",
"revert": "上一步",
"reset": "新样品 / 重置"
},
"placeholders": {
"config": "从已安装 Orisys 配置中选择,或浏览 JSON",
"cal": "可选标定文件 .npy"
},
"dialogs": {
"select_config": "选择配置 JSON",
"select_cal": "选择标定文件",
"select_recordings": "选择录制文件夹",
"orisys_title": "Orisys",
"orisys_required": "预览需要 Orisys SDK。\n\n{detail}",
"config_title": "配置",
"config_required": "请先选择 DDJX01 配置 JSON。",
"config_invalid": "无法加载配置。",
"capture_title": "采集",
"recording_title": "录制",
"no_frame": "尚无可用帧。"
},
"status": {
"ready": "就绪 — 开始预览将使用 Orisys DDJX01 固定格式。",
"orisys_ok": "orisys 可用(版本 {version})",
"orisys_missing": "无法导入 orisys — 请将 orisys-sdk/src 加入 PYTHONPATH。",
"opening": "正在打开 Orisys({mode})…",
"preview_stopped": "预览已停止。",
"capture_error": "采集错误:{message}",
"frame_info": "{raw_w}x{raw_h} 原始 / {proc_w}x{proc_h} 处理 @ {fps:.1f} FPS",
"recording": "正在录制… {name}\n选项:传感器={sensor_type},弹性体={elastomer},CMOS={cmos},标记={marker},压头={indentor},平台={platform}",
"recording_frames": "正在录制… {name}\n帧数:{count}",
"saved": "已保存 {count} 帧 → {path}",
"workflow_done": "全部步骤已完成。可点击“新样品 / 重置”开始下一个样品。"
},
"exhibition": {
"title": "Sensor Bench 展示端",
"btn_search": "搜索",
"btn_camera": "摄像机",
"btn_config": "配置",
"btn_start": "启动",
"btn_starting": "启动中…",
"btn_stop": "暂停",
"btn_reset": "重置",
"btn_start_record": "开始录像",
"btn_stop_record": "结束录像",
"show_log": "显示日志面板",
"view": "画面",
"view_pure_color": "纯色背景箭头",
"view_captured": "采集图像箭头",
"view_processed": "处理后图像",
"view_raw": "原始图像",
"sensor_type": "传感器",
"sensor_orisys": "ORY-BOX/ORY-MINI",
"sensor_dd06": "ORY-X",
"sensor_switched": "已切换传感器类型:{sensor}",
"placeholder_camera": "摄像头编号或视频路径",
"dialog_video": "选择视频文件",
"camera_found": "已发现 {count} 路摄像机:{names}",
"camera_none": "未发现可用摄像机。可点击“搜索”重扫,或通过“摄像机”选择视频文件。",
"plot_fn": "法向力 FN",
"plot_fx": "切向力 FX",
"plot_fy": "切向力 FY",
"reset_done": "追踪器已重置。",
"reset_need_start": "请先启动传感器后再重置追踪器。",
"reset_failed": "重置失败:{message}",
"force_fn_n": "法向力(N)={value:.6f}",
"recording_started": "开始录像:{name}",
"hints": "按键:Q 退出 | R 重置追踪器 | S 开始录像 | E 结束录像 | ESC 全屏",
"menu_help": "帮助",
"menu_about": "关于",
"about_title": "关于 Sensor Bench",
"about_body": "Sensor Bench 展示端\n版本:{app_version}\nOrisys:{orisys_version}\n\nOrisys-Tech",
"config_dir_tip": "已安装配置目录:{path}\n当前:{selected}",
"config_dir_missing": "未找到已安装的 Orisys 配置目录。可用“配置…”浏览本地 JSON。"
},
"workflow": {
"title": "传感器检测流程",
"current_marker": "当前",
"done_marker": "已完成",
"steps": {
"power_on": {
"title": "开机并连接",
"body": "连接传感器 USB 线缆,等待 Windows 识别设备。"
},
"select_camera": {
"title": "启动 Orisys 预览",
"body": "确认相机索引(通常为 0)和 DDJX01 配置,然后点击开始预览。格式由 Orisys 配置固定(1920x1080 @ 30)。"
},
"set_options": {
"title": "设置传感器选项",
"body": "选择与当前被测实物一致的弹性体类型、CMOS 类型和标记类型。"
},
"check_preview": {
"title": "检查原始与处理后预览",
"body": "确认原始画面约为 1920x1080,处理后 Orisys 图像约为 365x365。检查对焦、光照和画面清洁度。"
},
"record": {
"title": "录制原始数据",
"body": "选择存储文件夹,开始录制,操作传感器,然后停止录制。"
},
"review_curves": {
"title": "查看性能曲线",
"body": "观察采集/渲染 FPS 与力曲线,确认无明显掉帧或曝光不稳定后再签字确认。"
},
"sensitivity": {
"title": "灵敏度测试",
"body": "选择小压头,并加载 2g 砝码,观察力曲线是否有清晰变化。"
},
"max_load": {
"title": "最大载荷测试",
"body": "选择平板压头,并加载 10g 到 500g 的砝码观察曲线变化;MINI 使用 10g 到 500g,BOX 使用 2kg。"
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"sensor_types": [
{"id": "BOX", "label": "BOX"},
{"id": "MINI", "label": "MINI"}
],
"elastomer_types": [
{"id": "shoreA20", "label": "冯总20度"},
{"id": "shore0030", "label": "0度"},
{"id": "solaris", "label": "铂金"}
],
"cmos_types": [
{"id": "ov2710", "label": "OV2710"},
{"id": "ov9745", "label": "OV9745"}
],
"marker_types": [
{"id": "4color", "label": "4Color"}
],
"indentor_types": [
{"id": "1mm_circle", "label": "1mm圆柱"},
{"id": "1mm_ball", "label": "1mm球"}
],
"platoform_types": [
{"id": "manual", "label": "手动"},
{"id": "single_axis", "label": "单轴"},
{"id": "six_axis", "label": "六轴"}
]
}
+3
View File
@@ -0,0 +1,3 @@
@echo off
cd /d "%~dp0.."
python "%~dp0run_operator.py" %*
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Launch the private Sensor Bench operator workflow UI.
Usage (from the repository root or this folder):
python internal/run_operator.py
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
INTERNAL = Path(__file__).resolve().parent
REPO = INTERNAL.parent
PUBLIC_SRC = REPO / "src"
OPERATOR_SRC = INTERNAL / "src"
for path in (OPERATOR_SRC, PUBLIC_SRC):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
# Prefer sibling orisys-sdk checkout for Sensor-owned preview.
ORISYS_SRC = REPO.parent / "orisys-sdk" / "src"
if ORISYS_SRC.is_dir() and str(ORISYS_SRC) not in sys.path:
sys.path.insert(0, str(ORISYS_SRC))
from sensor_bench_operator.__main__ import main # noqa: E402
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,5 @@
"""Private Sensor Bench operator workflow UI (internal use only)."""
from .app import MainWindow
__all__ = ["MainWindow"]
@@ -0,0 +1,26 @@
"""python -m sensor_bench_operator"""
from __future__ import annotations
import os
import sys
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
from PyQt5.QtWidgets import QApplication
from .app import MainWindow
def main(argv: list[str] | None = None) -> int:
args = list(sys.argv if argv is None else argv)
qt_app = QApplication(args)
qt_app.setApplicationName("Sensor Bench Operator")
qt_app.setOrganizationName("Orisys-Tech")
window = MainWindow()
window.show()
return qt_app.exec_()
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
"""Resolve internal operator resource paths (steps, options, locales)."""
from __future__ import annotations
import sys
from pathlib import Path
def is_frozen() -> bool:
return bool(getattr(sys, "frozen", False))
def app_root() -> Path:
"""Editable ``internal/`` directory, or frozen executable directory."""
if is_frozen():
return Path(sys.executable).resolve().parent
return Path(__file__).resolve().parents[2]
def resource_root() -> Path:
"""Bundled resource root for editable and PyInstaller runs."""
if is_frozen():
return Path(getattr(sys, "_MEIPASS", app_root())).resolve()
return app_root()
def operator_root() -> Path:
"""Compatibility alias for ``app_root()``."""
return app_root()
def default_options_path() -> Path:
external = app_root() / "config" / "options.json"
if external.exists():
return external
return resource_root() / "config" / "options.json"
def default_steps_path() -> Path:
external = app_root() / "steps" / "default.json"
if external.exists():
return external
return resource_root() / "steps" / "default.json"
def default_locales_dir() -> Path:
external = app_root() / "config" / "locales"
if external.exists():
return external
return resource_root() / "config" / "locales"
+13
View File
@@ -0,0 +1,13 @@
{
"title_key": "workflow.title",
"steps": [
{"id": "power_on"},
{"id": "select_camera"},
{"id": "set_options"},
{"id": "check_preview"},
{"id": "record"},
{"id": "review_curves"},
{"id": "sensitivity"},
{"id": "max_load"}
]
}
+82
View File
@@ -0,0 +1,82 @@
"""Smoke tests for the internal Sensor Bench operator UI."""
from __future__ import annotations
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(ROOT / "internal" / "src"))
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
def test_mainwindow_offscreen() -> None:
from PyQt5.QtWidgets import QApplication
from sensor_bench.acquisition import FixedCameraMode
from sensor_bench.i18n import DEFAULT_LOCALE
from sensor_bench_operator.app import MainWindow
app = QApplication.instance() or QApplication([])
win = MainWindow()
assert win.tr.locale == DEFAULT_LOCALE == "zh_CN"
assert "传感器" in win.windowTitle()
assert win.btn_start.text() == "开始预览"
assert win.camera_index.value() == 0
assert win.config_combo is not None
assert win.config_combo.count() >= 1
assert win.btn_next is not None
assert win.btn_revert is not None
assert win.btn_reset is not None
assert win.sensor_type_combo.count() == 2
assert win.indentor_combo.count() == 2
assert win.platform_combo.count() == 3
assert win.selected_sensor_options()["sensor_type"] == "BOX"
assert win.selected_sensor_options()["indentor"] == "1mm_circle"
assert win.selected_sensor_options()["platform"] == "manual"
assert not hasattr(win, "resolution_combo")
assert not hasattr(win, "btn_search")
assert win._current_step_index == 0
assert not win.btn_revert.isEnabled()
assert win.btn_next.isEnabled()
# Language switch refreshes labels without restart.
win.language_combo.setCurrentIndex(1)
assert win.tr.locale == "en_US"
assert win.windowTitle() == "Sensor Bench"
assert win.btn_start.text() == "Start Preview"
# Workflow next / revert / reset.
total = len(win.workflow.steps)
win._on_step_next()
assert win._current_step_index == 1
assert win.btn_revert.isEnabled()
win._on_step_revert()
assert win._current_step_index == 0
for _ in range(total):
win._on_step_next()
assert win._current_step_index == total
assert not win.btn_next.isEnabled()
assert "completed" in win.workflow_status.text().lower() or "完成" in win.workflow_status.text()
win._on_step_reset()
assert win._current_step_index == 0
assert win.btn_next.isEnabled()
win._fixed_mode = FixedCameraMode(
width=1920,
height=1080,
fps=30.0,
config_ref=str(ROOT / "tests" / "fixtures" / "ddjx01_min.json"),
stitch_width=365,
stitch_height=365,
)
win.mode_label.setText(win.t("labels.fixed_mode", mode=win._fixed_mode.label()))
assert "1920x1080" in win.mode_label.text()
win.close()
if __name__ == "__main__":
test_mainwindow_offscreen()
print("ok")