update plot to force units
This commit is contained in:
+5
-1
@@ -9,6 +9,7 @@ $DistDir = Join-Path $RepoRoot "dist"
|
||||
$WorkDir = Join-Path $RepoRoot "build\pyinstaller_work"
|
||||
$SpecPath = Join-Path $PSScriptRoot "sensor_bench.spec"
|
||||
$RepoConfig = Join-Path $RepoRoot "config"
|
||||
$RepoSensors = Join-Path $RepoRoot "sensors"
|
||||
|
||||
if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
||||
throw "conda was not found on PATH. Open an Anaconda Prompt or initialize conda for PowerShell."
|
||||
@@ -42,8 +43,11 @@ try {
|
||||
conda run -n $EnvName python -m PyInstaller --noconfirm --clean --distpath "$DistDir" --workpath "$WorkDir" "$SpecPath"
|
||||
if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed." }
|
||||
|
||||
# Public package only: copy exhibition config/. Never ship internal/ operator assets.
|
||||
# Public package only: copy exhibition config/ and sensors/. Never ship internal/ operator assets.
|
||||
Copy-Item -Recurse -Force $RepoConfig (Join-Path $DistDir "SensorBench")
|
||||
if (Test-Path $RepoSensors) {
|
||||
Copy-Item -Recurse -Force $RepoSensors (Join-Path $DistDir "SensorBench")
|
||||
}
|
||||
$InternalLeak = Join-Path $DistDir "SensorBench\internal"
|
||||
if (Test-Path $InternalLeak) {
|
||||
throw "Public dist must not contain internal/: $InternalLeak"
|
||||
|
||||
@@ -11,6 +11,7 @@ work_dir = repo_root / "build" / "pyinstaller_work"
|
||||
|
||||
datas = [
|
||||
(str(repo_root / "config"), "config"),
|
||||
(str(repo_root / "sensors"), "sensors"),
|
||||
]
|
||||
binaries = []
|
||||
hiddenimports = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Copy Orisys wheel sensor JSON configs into repo config/ (exclude options.json)."""
|
||||
"""Copy Orisys wheel sensor JSON configs and sensors/ bundles into the repo."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,6 +17,16 @@ def wheel_config_dir() -> Path:
|
||||
return inner_pkg.parent / "configs"
|
||||
|
||||
|
||||
def wheel_sensors_dir() -> Path | None:
|
||||
import orisys
|
||||
|
||||
inner_pkg = Path(orisys.__file__).resolve().parent
|
||||
for candidate in (inner_pkg / "sensors", inner_pkg.parent / "sensors"):
|
||||
if candidate.is_dir() and any(candidate.iterdir()):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def sync_configs(repo_root: Path) -> int:
|
||||
src = wheel_config_dir()
|
||||
if not src.is_dir():
|
||||
@@ -37,9 +47,33 @@ def sync_configs(repo_root: Path) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def sync_sensor_bundles(repo_root: Path) -> int:
|
||||
src = wheel_sensors_dir()
|
||||
if src is None or not src.is_dir():
|
||||
print("Orisys wheel sensors/ directory not found; skipping bundle sync")
|
||||
return 0
|
||||
|
||||
dst = repo_root / "sensors"
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
copied = 0
|
||||
for bundle_dir in sorted(p for p in src.iterdir() if p.is_dir()):
|
||||
target = dst / bundle_dir.name
|
||||
if target.exists():
|
||||
shutil.rmtree(target)
|
||||
shutil.copytree(bundle_dir, target)
|
||||
copied += 1
|
||||
|
||||
print(f"Synced {copied} sensor bundle(s) from {src} to {dst}")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
return sync_configs(repo_root)
|
||||
code = sync_configs(repo_root)
|
||||
if code != 0:
|
||||
return code
|
||||
return sync_sensor_bundles(repo_root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -99,6 +99,8 @@ def main() -> int:
|
||||
require(DIST / "config" / "locales" / "zh_CN.json")
|
||||
require(DIST / "config" / "locales" / "en_US.json")
|
||||
require(DIST / "config" / "ddjx01.json")
|
||||
require(DIST / "sensors" / "box" / "sensor.json")
|
||||
require(DIST / "sensors" / "box" / "force_calibration.json")
|
||||
require(DIST / "_internal")
|
||||
|
||||
for rel in FORBIDDEN_RELATIVE_PATHS:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 814 KiB After Width: | Height: | Size: 1.9 MiB |
@@ -67,6 +67,9 @@
|
||||
"plot_fn": "Normal Force FN",
|
||||
"plot_fx": "Shear Force FX",
|
||||
"plot_fy": "Shear Force FY",
|
||||
"plot_with_unit": "{title} ({unit})",
|
||||
"force_calibrated": "FN={fn:.4f} {unit} (raw={fn_raw:.4f}) | FX={fx:.4f} {unit} | FY={fy:.4f} {unit}",
|
||||
"force_raw": "FN={fn:.4f} | FX={fx:.4f} | FY={fy:.4f} (raw)",
|
||||
"reset_done": "Tracker reset.",
|
||||
"reset_need_start": "Start the sensor before resetting the tracker.",
|
||||
"reset_failed": "Reset failed: {message}",
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
"plot_fn": "法向力 FN",
|
||||
"plot_fx": "切向力 FX",
|
||||
"plot_fy": "切向力 FY",
|
||||
"plot_with_unit": "{title}({unit})",
|
||||
"force_calibrated": "FN={fn:.4f} {unit}(raw={fn_raw:.4f})| FX={fx:.4f} {unit} | FY={fy:.4f} {unit}",
|
||||
"force_raw": "FN={fn:.4f} | FX={fx:.4f} | FY={fy:.4f}(原始量)",
|
||||
"reset_done": "追踪器已重置。",
|
||||
"reset_need_start": "请先启动传感器后再重置追踪器。",
|
||||
"reset_failed": "重置失败:{message}",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"unit": "N",
|
||||
"channels": {
|
||||
"normal": {
|
||||
"method": "piecewise_linear",
|
||||
"raw_key": "fnormal",
|
||||
"extrapolate": true,
|
||||
"clamp_output_min": 0.0,
|
||||
"points": [
|
||||
[1000.0, 0.0],
|
||||
[10000.0, 0.377],
|
||||
[30000.0, 1.377],
|
||||
[62000.0, 2.377]
|
||||
]
|
||||
},
|
||||
"shear": {
|
||||
"method": "piecewise_linear",
|
||||
"input": "components",
|
||||
"signed": true,
|
||||
"extrapolate": true,
|
||||
"clamp_output_min": 0.0,
|
||||
"points": [
|
||||
[1000.0, 0.0],
|
||||
[36000.0, 1.19],
|
||||
[68000.0, 2.06]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "prototype",
|
||||
"version": "1.0",
|
||||
"camera": {
|
||||
"resolution_raw": [1920, 1080],
|
||||
"fps": 30,
|
||||
"units": 15,
|
||||
"units_used": 9,
|
||||
"resolution_stitch": [365, 365],
|
||||
"stitch_res_list": [365,263,123,63]
|
||||
},
|
||||
"stitcher": {
|
||||
"min_area_frac": 0.01,
|
||||
"max_area_frac": 0.05,
|
||||
"max_aspect_ratio": 50,
|
||||
"index": [1,2,3,6,7,8,11,12,13],
|
||||
"margin": 10,
|
||||
"nominal_ov_pos": 100,
|
||||
"n": [3,3],
|
||||
"row_tol": 0.6,
|
||||
"box_size": [250, 250],
|
||||
"blend":"numba",
|
||||
"blend_list": ["numpy", "numba","none"]
|
||||
},
|
||||
"tracker": {
|
||||
"adaptive": true,
|
||||
"lower_threshold": 12.0,
|
||||
"upper_threshold": 18.0,
|
||||
"loop_closure_interval": 1,
|
||||
"algorithm": "dis",
|
||||
"dis_preset": "medium",
|
||||
"dis_mode": 3,
|
||||
"algorithm_list": ["raft", "dis"],
|
||||
"hhd_downsample": 3
|
||||
},
|
||||
"contact": {
|
||||
"min_contour_area": 3000,
|
||||
"flow_threshold":5,
|
||||
"div_threshold":0.05,
|
||||
"expansion_threshold": 0.01,
|
||||
"kernel_size": [5,5],
|
||||
"denoise_sigma": 12
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Place the ORY-FINGER runtime asset bundle files in this directory before
|
||||
building release wheels:
|
||||
|
||||
- `mesh.npz`
|
||||
- `camera_intrinsics.npz`
|
||||
- `world_to_cam.npy`
|
||||
|
||||
These binary assets are validated by the wheel build and are intentionally not
|
||||
generated by the package build step.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
Place visualization camera assets in this directory before building release
|
||||
wheels:
|
||||
|
||||
- `world_to_cam.npy`
|
||||
|
||||
The 3D dashboard uses this pose as the default view preset when available.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "finger",
|
||||
"sensor_json": "sensor.json",
|
||||
"assets_dir": "assets/30fps-0703",
|
||||
"viz_pose": "assets/visualization/world_to_cam.npy"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "dd02-ov",
|
||||
"type": "single_camera",
|
||||
"version": "1.0",
|
||||
"camera": {
|
||||
"resolution_raw": [1280, 720],
|
||||
"fps": 30,
|
||||
"resolution_stitch": [280, 300],
|
||||
"source_scale": 0.4,
|
||||
"old":[672, 728, 1,0.5],
|
||||
"work": [504, 546,1],
|
||||
"work2": [420, 450,0.8],
|
||||
"work3": [350, 375,4,0.7],
|
||||
"work4": [280, 300,4,0.4,0.2]
|
||||
},
|
||||
"tracker": {
|
||||
"adaptive": true,
|
||||
"lower_threshold": 12.0,
|
||||
"upper_threshold": 18.0,
|
||||
"loop_closure_interval": 1,
|
||||
"algorithm": "dis",
|
||||
"dis_preset": "medium",
|
||||
"dis_mode": 1,
|
||||
"algorithm_list": ["raft", "dis"],
|
||||
"hhd_downsample": 1
|
||||
},
|
||||
"contact": {
|
||||
"min_contour_area": 3000,
|
||||
"flow_threshold": 0.2,
|
||||
"div_threshold": 0.022,
|
||||
"expansion_threshold": 0.01,
|
||||
"kernel_size": [5, 5],
|
||||
"denoise_sigma": 12
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"unit": "N",
|
||||
"channels": {
|
||||
"normal": {
|
||||
"method": "piecewise_linear",
|
||||
"raw_key": "fnormal",
|
||||
"extrapolate": true,
|
||||
"clamp_output_min": 0.0,
|
||||
"points": [
|
||||
[1000.0, 0.0],
|
||||
[10000.0, 0.377],
|
||||
[30000.0, 1.377],
|
||||
[62000.0, 2.377]
|
||||
]
|
||||
},
|
||||
"shear": {
|
||||
"method": "piecewise_linear",
|
||||
"input": "components",
|
||||
"signed": true,
|
||||
"extrapolate": true,
|
||||
"clamp_output_min": 0.0,
|
||||
"points": [
|
||||
[1000.0, 0.0],
|
||||
[36000.0, 1.19],
|
||||
[68000.0, 2.06]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "prototype",
|
||||
"version": "1.0",
|
||||
"camera": {
|
||||
"resolution_raw": [1920, 1080],
|
||||
"fps": 30,
|
||||
"units": 15,
|
||||
"units_used": 9,
|
||||
"resolution_stitch": [365, 365],
|
||||
"stitch_res_list": [365,263,123,63]
|
||||
},
|
||||
"stitcher": {
|
||||
"min_area_frac": 0.01,
|
||||
"max_area_frac": 0.05,
|
||||
"max_aspect_ratio": 50,
|
||||
"index": [1,2,3,6,7,8,11,12,13],
|
||||
"margin": 10,
|
||||
"nominal_ov_pos": 100,
|
||||
"n": [3,3],
|
||||
"row_tol": 0.6,
|
||||
"box_size": [250, 250],
|
||||
"blend":"numba",
|
||||
"blend_list": ["numpy", "numba","none"]
|
||||
},
|
||||
"tracker": {
|
||||
"adaptive": true,
|
||||
"lower_threshold": 12.0,
|
||||
"upper_threshold": 18.0,
|
||||
"loop_closure_interval": 1,
|
||||
"algorithm": "dis",
|
||||
"dis_preset": "medium",
|
||||
"dis_mode": 3,
|
||||
"algorithm_list": ["raft", "dis"],
|
||||
"hhd_downsample": 3
|
||||
},
|
||||
"contact": {
|
||||
"min_contour_area": 3000,
|
||||
"flow_threshold":5,
|
||||
"div_threshold":0.05,
|
||||
"expansion_threshold": 0.01,
|
||||
"kernel_size": [5,5],
|
||||
"denoise_sigma": 12
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+132
-22
@@ -18,6 +18,11 @@ import cv2
|
||||
import numpy as np
|
||||
|
||||
from .force_calibration import FORCE_SAMPLE_RATE_HZ, LOWPASS_CUTOFF_HZ
|
||||
from .paths import (
|
||||
bundle_has_force_calibration,
|
||||
normalize_sensor_config_ref,
|
||||
resolve_force_bundle_from_config,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -35,6 +40,21 @@ class FixedCameraMode:
|
||||
return f"{self.width}x{self.height} @{self.fps:g} (DDJX01 / MJPG)"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForceReadout:
|
||||
"""Calibrated and raw force values from one Orisys Sensor tick."""
|
||||
|
||||
normal_force: float = 0.0
|
||||
shear_x: float = 0.0
|
||||
shear_y: float = 0.0
|
||||
normal_force_raw: float = 0.0
|
||||
shear_x_raw: float = 0.0
|
||||
shear_y_raw: float = 0.0
|
||||
force_calibrated: bool = False
|
||||
force_unit: str = "raw"
|
||||
flow: Optional[np.ndarray] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SdkPacket:
|
||||
"""One acquisition tick from the Orisys Sensor."""
|
||||
@@ -51,11 +71,109 @@ class SdkPacket:
|
||||
normal_force: float = 0.0
|
||||
shear_x: float = 0.0
|
||||
shear_y: float = 0.0
|
||||
normal_force_raw: float = 0.0
|
||||
shear_x_raw: float = 0.0
|
||||
shear_y_raw: float = 0.0
|
||||
force_unit: str = "raw"
|
||||
force_calibrated: bool = False
|
||||
session_id: int = 0
|
||||
mode: Optional[FixedCameraMode] = None
|
||||
flow: Optional[np.ndarray] = None
|
||||
|
||||
|
||||
def sensor_force_calibrated(sensor, *, config_ref: str | Path | None = None) -> bool:
|
||||
"""True when Orisys resolved sensors/<name>/force_calibration.json."""
|
||||
if getattr(sensor, "_force_cal_sensor_name", None) is not None:
|
||||
return True
|
||||
bundle = resolve_force_bundle_from_config(config_ref)
|
||||
return bundle is not None and bundle_has_force_calibration(bundle)
|
||||
|
||||
|
||||
def read_forces_from_sensor(
|
||||
sensor,
|
||||
*,
|
||||
include_flow: bool = True,
|
||||
config_ref: str | Path | None = None,
|
||||
) -> ForceReadout:
|
||||
"""Read FN/FX/FY plus *_RAW from Orisys, with legacy SDK fallback."""
|
||||
info = sensor.info
|
||||
fn = fx = fy = 0.0
|
||||
fn_raw = fx_raw = fy_raw = 0.0
|
||||
flow = None
|
||||
read_raw = all(
|
||||
hasattr(info, name)
|
||||
for name in ("FNORMAL_RAW", "FSHEARX_RAW", "FSHEARY_RAW")
|
||||
)
|
||||
has_flow = include_flow and hasattr(info, "VRAW")
|
||||
try:
|
||||
if read_raw and has_flow:
|
||||
fn, fx, fy, fn_raw, fx_raw, fy_raw, flow = sensor.read_info(
|
||||
info.FNORMAL,
|
||||
info.FSHEARX,
|
||||
info.FSHEARY,
|
||||
info.FNORMAL_RAW,
|
||||
info.FSHEARX_RAW,
|
||||
info.FSHEARY_RAW,
|
||||
info.VRAW,
|
||||
)
|
||||
elif read_raw:
|
||||
fn, fx, fy, fn_raw, fx_raw, fy_raw = sensor.read_info(
|
||||
info.FNORMAL,
|
||||
info.FSHEARX,
|
||||
info.FSHEARY,
|
||||
info.FNORMAL_RAW,
|
||||
info.FSHEARX_RAW,
|
||||
info.FSHEARY_RAW,
|
||||
)
|
||||
elif has_flow:
|
||||
fn, fx, fy, flow = sensor.read_info(
|
||||
info.FNORMAL,
|
||||
info.FSHEARX,
|
||||
info.FSHEARY,
|
||||
info.VRAW,
|
||||
)
|
||||
fn_raw, fx_raw, fy_raw = fn, fx, fy
|
||||
else:
|
||||
fn, fx, fy = sensor.read_info(
|
||||
info.FNORMAL,
|
||||
info.FSHEARX,
|
||||
info.FSHEARY,
|
||||
)
|
||||
fn_raw, fx_raw, fy_raw = fn, fx, fy
|
||||
except Exception: # noqa: BLE001
|
||||
try:
|
||||
if has_flow:
|
||||
fn, fx, fy, flow = sensor.read_info(
|
||||
info.FNORMAL,
|
||||
info.FSHEARX,
|
||||
info.FSHEARY,
|
||||
info.VRAW,
|
||||
)
|
||||
else:
|
||||
fn, fx, fy = sensor.read_info(
|
||||
info.FNORMAL,
|
||||
info.FSHEARX,
|
||||
info.FSHEARY,
|
||||
)
|
||||
fn_raw, fx_raw, fy_raw = fn, fx, fy
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
calibrated = sensor_force_calibrated(sensor, config_ref=config_ref)
|
||||
unit = "N" if calibrated else "raw"
|
||||
return ForceReadout(
|
||||
normal_force=float(fn),
|
||||
shear_x=float(fx),
|
||||
shear_y=float(fy),
|
||||
normal_force_raw=float(fn_raw),
|
||||
shear_x_raw=float(fx_raw),
|
||||
shear_y_raw=float(fy_raw),
|
||||
force_calibrated=calibrated,
|
||||
force_unit=unit,
|
||||
flow=flow,
|
||||
)
|
||||
|
||||
|
||||
def orisys_importable() -> tuple[bool, str]:
|
||||
try:
|
||||
import orisys # noqa: F401
|
||||
@@ -255,7 +373,7 @@ class SdkAcquisitionWorker:
|
||||
try:
|
||||
self._status(f"Opening Orisys Sensor ({mode.label()})…")
|
||||
kwargs = {
|
||||
"config_name": mode.config_ref,
|
||||
"config_name": normalize_sensor_config_ref(mode.config_ref),
|
||||
"verbose": False,
|
||||
"backend": self._backend,
|
||||
}
|
||||
@@ -297,24 +415,11 @@ class SdkAcquisitionWorker:
|
||||
frame_times = [t for t in frame_times if t >= cutoff]
|
||||
fps = float(len(frame_times)) if frame_times else 0.0
|
||||
|
||||
fn = fx = fy = 0.0
|
||||
flow = None
|
||||
try:
|
||||
fn, fx, fy, flow = self._sensor.read_info(
|
||||
self._sensor.info.FNORMAL,
|
||||
self._sensor.info.FSHEARX,
|
||||
self._sensor.info.FSHEARY,
|
||||
self._sensor.info.VRAW,
|
||||
forces = read_forces_from_sensor(
|
||||
self._sensor,
|
||||
include_flow=True,
|
||||
config_ref=mode.config_ref,
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
try:
|
||||
fn, fx, fy = self._sensor.read_info(
|
||||
self._sensor.info.FNORMAL,
|
||||
self._sensor.info.FSHEARX,
|
||||
self._sensor.info.FSHEARY,
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
raw_bgr = _ensure_bgr(raw)
|
||||
proc_bgr = _ensure_bgr(processed)
|
||||
@@ -328,12 +433,17 @@ class SdkAcquisitionWorker:
|
||||
height=int(raw_bgr.shape[0]),
|
||||
processed_width=int(proc_bgr.shape[1]),
|
||||
processed_height=int(proc_bgr.shape[0]),
|
||||
normal_force=float(fn),
|
||||
shear_x=float(fx),
|
||||
shear_y=float(fy),
|
||||
normal_force=forces.normal_force,
|
||||
shear_x=forces.shear_x,
|
||||
shear_y=forces.shear_y,
|
||||
normal_force_raw=forces.normal_force_raw,
|
||||
shear_x_raw=forces.shear_x_raw,
|
||||
shear_y_raw=forces.shear_y_raw,
|
||||
force_unit=forces.force_unit,
|
||||
force_calibrated=forces.force_calibrated,
|
||||
session_id=session,
|
||||
mode=mode,
|
||||
flow=flow,
|
||||
flow=forces.flow,
|
||||
)
|
||||
with self._lock:
|
||||
self._latest = packet
|
||||
|
||||
@@ -7,8 +7,16 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
from .acquisition import FixedCameraMode, SdkPacket, _ensure_bgr, load_fixed_mode, orisys_importable
|
||||
from .acquisition import (
|
||||
FixedCameraMode,
|
||||
SdkPacket,
|
||||
_ensure_bgr,
|
||||
load_fixed_mode,
|
||||
orisys_importable,
|
||||
read_forces_from_sensor,
|
||||
)
|
||||
from .force_calibration import FORCE_SAMPLE_RATE_HZ, LOWPASS_CUTOFF_HZ
|
||||
from .paths import normalize_sensor_config_ref
|
||||
from .roi_geometry import (
|
||||
build_roi_warp,
|
||||
load_json_config,
|
||||
@@ -138,6 +146,7 @@ class Dd06AcquisitionWorker:
|
||||
config = load_json_config(config_ref)
|
||||
roi_points, roi_image_size = load_roi_polygon(config)
|
||||
out_size = rectified_output_size(config, roi_points)
|
||||
config_ref = normalize_sensor_config_ref(config_ref)
|
||||
self._sensor = orisys.Sensor("external", config_name=config_ref, verbose=False)
|
||||
self._sensor.configure(
|
||||
lowpass_cutoff_hz=LOWPASS_CUTOFF_HZ,
|
||||
@@ -190,24 +199,11 @@ class Dd06AcquisitionWorker:
|
||||
frame_times = [t for t in frame_times if t >= cutoff]
|
||||
fps = float(len(frame_times)) if frame_times else 0.0
|
||||
|
||||
fn = fx = fy = 0.0
|
||||
flow = None
|
||||
try:
|
||||
fn, fx, fy, flow = self._sensor.read_info(
|
||||
self._sensor.info.FNORMAL,
|
||||
self._sensor.info.FSHEARX,
|
||||
self._sensor.info.FSHEARY,
|
||||
self._sensor.info.VRAW,
|
||||
forces = read_forces_from_sensor(
|
||||
self._sensor,
|
||||
include_flow=True,
|
||||
config_ref=self._config_ref,
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
try:
|
||||
fn, fx, fy = self._sensor.read_info(
|
||||
self._sensor.info.FNORMAL,
|
||||
self._sensor.info.FSHEARX,
|
||||
self._sensor.info.FSHEARY,
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
raw_bgr = _ensure_bgr(frame)
|
||||
# Prefer sensor.img / get_img result for processed visualization.
|
||||
@@ -225,12 +221,17 @@ class Dd06AcquisitionWorker:
|
||||
height=int(raw_bgr.shape[0]),
|
||||
processed_width=int(proc_bgr.shape[1]),
|
||||
processed_height=int(proc_bgr.shape[0]),
|
||||
normal_force=float(fn),
|
||||
shear_x=float(fx),
|
||||
shear_y=float(fy),
|
||||
normal_force=forces.normal_force,
|
||||
shear_x=forces.shear_x,
|
||||
shear_y=forces.shear_y,
|
||||
normal_force_raw=forces.normal_force_raw,
|
||||
shear_x_raw=forces.shear_x_raw,
|
||||
shear_y_raw=forces.shear_y_raw,
|
||||
force_unit=forces.force_unit,
|
||||
force_calibrated=forces.force_calibrated,
|
||||
session_id=session,
|
||||
mode=mode,
|
||||
flow=flow,
|
||||
flow=forces.flow,
|
||||
)
|
||||
with self._lock:
|
||||
self._latest = packet
|
||||
|
||||
@@ -47,12 +47,13 @@ from .acquisition import (
|
||||
from .camera_discovery import list_cameras, pick_default_camera
|
||||
from .dd06_acquisition import Dd06AcquisitionWorker
|
||||
from .force_calibration import (
|
||||
CALIBRATED_FORCE_Y_MAX,
|
||||
CALIBRATED_FORCE_Y_MIN,
|
||||
FORCE_SAMPLE_RATE_HZ,
|
||||
PLOT_BUFFER_POINTS,
|
||||
PLOT_WINDOW_SEC,
|
||||
Y_AXIS_MIN_SPAN,
|
||||
Y_RANGE_ALPHA,
|
||||
calibrate_normal_force,
|
||||
)
|
||||
from .i18n import DEFAULT_LOCALE, Translator
|
||||
from .paths import (
|
||||
@@ -226,6 +227,8 @@ class ExhibitionWindow(QMainWindow):
|
||||
self._view_mode = "pure_color"
|
||||
self._sensor_type = "orisys"
|
||||
self._log_panel_visible = False
|
||||
self._force_unit = "raw"
|
||||
self._force_calibrated = False
|
||||
self._fixed_mode: Optional[FixedCameraMode] = None
|
||||
self._orisys_config = str(default_ddjx01_config())
|
||||
self._dd06_config = str(default_dd06_config())
|
||||
@@ -448,12 +451,23 @@ class ExhibitionWindow(QMainWindow):
|
||||
self.view_combo.blockSignals(False)
|
||||
|
||||
for plot, title_key in zip(self.plots, self.plot_titles):
|
||||
plot.setTitle(self.t(title_key), color="#9aa0a6", size="12pt")
|
||||
plot.setTitle(self._plot_title(title_key), color="#9aa0a6", size="12pt")
|
||||
|
||||
if self._source_pixmap is None:
|
||||
self.image_label.setText(self.t("labels.no_preview"))
|
||||
self.status_label.setText(self.t("status.ready"))
|
||||
|
||||
def _plot_title(self, title_key: str) -> str:
|
||||
return self.t(
|
||||
"exhibition.plot_with_unit",
|
||||
title=self.t(title_key),
|
||||
unit=self._force_unit,
|
||||
)
|
||||
|
||||
def _update_force_plot_titles(self) -> None:
|
||||
for plot, title_key in zip(self.plots, self.plot_titles):
|
||||
plot.setTitle(self._plot_title(title_key), color="#9aa0a6", size="12pt")
|
||||
|
||||
def _selected_config(self) -> str:
|
||||
data = self.config_combo.currentData()
|
||||
if data:
|
||||
@@ -646,6 +660,9 @@ class ExhibitionWindow(QMainWindow):
|
||||
return
|
||||
|
||||
self.force_history.reset()
|
||||
self._force_unit = "raw"
|
||||
self._force_calibrated = False
|
||||
self._update_force_plot_titles()
|
||||
self._awaiting_first_frame = True
|
||||
self._source_pixmap = None
|
||||
self.image_label.clear()
|
||||
@@ -794,10 +811,27 @@ class ExhibitionWindow(QMainWindow):
|
||||
)
|
||||
self.status_label.setText(info)
|
||||
self._log(info)
|
||||
self._force_unit = packet.force_unit
|
||||
self._force_calibrated = packet.force_calibrated
|
||||
self._update_force_plot_titles()
|
||||
if packet.force_calibrated:
|
||||
self._log(
|
||||
self.t(
|
||||
"exhibition.force_fn_n",
|
||||
value=calibrate_normal_force(packet.normal_force),
|
||||
"exhibition.force_calibrated",
|
||||
fn=packet.normal_force,
|
||||
fn_raw=packet.normal_force_raw,
|
||||
fx=packet.shear_x,
|
||||
fy=packet.shear_y,
|
||||
unit=packet.force_unit,
|
||||
)
|
||||
)
|
||||
else:
|
||||
self._log(
|
||||
self.t(
|
||||
"exhibition.force_raw",
|
||||
fn=packet.normal_force,
|
||||
fx=packet.shear_x,
|
||||
fy=packet.shear_y,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -870,8 +904,11 @@ class ExhibitionWindow(QMainWindow):
|
||||
x = state.times
|
||||
y = state.values[:, i]
|
||||
curve.setData(x, y)
|
||||
if self._force_calibrated:
|
||||
ymin, ymax = CALIBRATED_FORCE_Y_MIN, CALIBRATED_FORCE_Y_MAX
|
||||
else:
|
||||
ymin, ymax = state.y_ranges[i]
|
||||
self.plots[i].setYRange(ymin, ymax)
|
||||
self.plots[i].setYRange(ymin, ymax, padding=0)
|
||||
self.dot_cores[i].setData(x[-1:], y[-1:])
|
||||
x0, x1 = state.x_range
|
||||
for plot in self.plots:
|
||||
|
||||
@@ -11,6 +11,8 @@ FORCE_SAMPLE_RATE_HZ = 30.0
|
||||
PLOT_BUFFER_POINTS = 120
|
||||
PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ
|
||||
Y_AXIS_MIN_SPAN = 20000.0
|
||||
CALIBRATED_FORCE_Y_MIN = -20.0
|
||||
CALIBRATED_FORCE_Y_MAX = 20.0
|
||||
Y_RANGE_ALPHA = 0.5
|
||||
LOWPASS_CUTOFF_HZ = 10.0
|
||||
|
||||
|
||||
+174
-16
@@ -8,6 +8,31 @@ from pathlib import Path
|
||||
|
||||
NON_SENSOR_CONFIG_STEMS = {"options", "bench_theme"}
|
||||
|
||||
# Product bundles with optional force_calibration.json (orisys-sdk layout).
|
||||
PRODUCT_BUNDLES = ("box", "mini", "finger")
|
||||
|
||||
# Legacy flat config stems mapped to a product bundle for force calibration lookup.
|
||||
LEGACY_STEM_TO_BUNDLE: dict[str, str] = {
|
||||
"ddjx01": "box",
|
||||
"ddjx01_480p": "box",
|
||||
"ddjx01_q6a": "box",
|
||||
"ddjx01_wuxi": "box",
|
||||
"box": "box",
|
||||
"prototype": "box",
|
||||
"prototype_of3": "box",
|
||||
"prototype_of3_480p": "box",
|
||||
"prototype_of3_720p": "box",
|
||||
"prototype_raft": "box",
|
||||
"mini": "mini",
|
||||
"mini-fast": "mini",
|
||||
"finger": "finger",
|
||||
"dd02-ov": "finger",
|
||||
"dd02": "finger",
|
||||
"dd06-ov": "finger",
|
||||
"dd06-ov-0810": "finger",
|
||||
"x-ov": "finger",
|
||||
}
|
||||
|
||||
|
||||
def is_frozen() -> bool:
|
||||
return bool(getattr(sys, "frozen", False))
|
||||
@@ -58,6 +83,11 @@ def local_sensor_configs_dir() -> Path:
|
||||
return app_root() / "config"
|
||||
|
||||
|
||||
def local_sensor_bundles_dir() -> Path:
|
||||
"""Per-sensor bundle root: ``sensors/<product>/sensor.json``."""
|
||||
return app_root() / "sensors"
|
||||
|
||||
|
||||
def _is_sensor_config_json(path: Path) -> bool:
|
||||
if path.suffix.lower() != ".json":
|
||||
return False
|
||||
@@ -76,10 +106,114 @@ def list_local_sensor_config_files() -> list[Path]:
|
||||
return sorted(p for p in root.glob("*.json") if _is_sensor_config_json(p))
|
||||
|
||||
|
||||
def local_sensor_bundle_sensor_json(name: str) -> Path | None:
|
||||
"""Return ``sensors/<name>/sensor.json`` beside the app when present."""
|
||||
path = local_sensor_bundles_dir() / name / "sensor.json"
|
||||
return path if path.is_file() else None
|
||||
|
||||
|
||||
def local_sensor_bundle_force_calibration(name: str) -> Path | None:
|
||||
path = local_sensor_bundles_dir() / name / "force_calibration.json"
|
||||
return path if path.is_file() else None
|
||||
|
||||
|
||||
def list_local_sensor_bundle_names() -> list[str]:
|
||||
"""Product bundle folder names under ``sensors/`` with a ``sensor.json``."""
|
||||
root = local_sensor_bundles_dir()
|
||||
if not root.is_dir():
|
||||
return []
|
||||
found: list[str] = []
|
||||
for name in PRODUCT_BUNDLES:
|
||||
if local_sensor_bundle_sensor_json(name) is not None:
|
||||
found.append(name)
|
||||
return found
|
||||
|
||||
|
||||
def _orisys_bundle_has_force_calibration(name: str) -> bool:
|
||||
try:
|
||||
import orisys
|
||||
|
||||
return orisys.configs.load_force_calibration(name) is not None
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
|
||||
def bundle_has_force_calibration(name: str) -> bool:
|
||||
"""True when local or packaged Orisys bundle includes force_calibration.json."""
|
||||
if local_sensor_bundle_force_calibration(name) is not None:
|
||||
return True
|
||||
return _orisys_bundle_has_force_calibration(name)
|
||||
|
||||
|
||||
def resolve_force_bundle_from_config(config_ref: str | Path | None) -> str | None:
|
||||
"""Map a legacy flat config path/stem to a product bundle with calibration."""
|
||||
if config_ref is None:
|
||||
return None
|
||||
text = str(config_ref).strip().replace("\\", "/")
|
||||
if not text:
|
||||
return None
|
||||
|
||||
path = Path(text)
|
||||
candidates: list[str] = []
|
||||
if path.suffix.lower() == ".json":
|
||||
candidates.append(path.stem.lower())
|
||||
stem = path.stem.lower() if path.suffix else text.lower()
|
||||
if stem:
|
||||
candidates.append(stem)
|
||||
if text.lower() in PRODUCT_BUNDLES:
|
||||
candidates.insert(0, text.lower())
|
||||
parts = [p for p in text.split("/") if p]
|
||||
if "sensors" in parts:
|
||||
idx = parts.index("sensors")
|
||||
if idx + 1 < len(parts):
|
||||
candidates.insert(0, parts[idx + 1].lower())
|
||||
|
||||
seen: set[str] = set()
|
||||
for candidate in candidates:
|
||||
if not candidate or candidate in seen:
|
||||
continue
|
||||
seen.add(candidate)
|
||||
if candidate in PRODUCT_BUNDLES and local_sensor_bundle_sensor_json(candidate):
|
||||
if bundle_has_force_calibration(candidate):
|
||||
return candidate
|
||||
mapped = LEGACY_STEM_TO_BUNDLE.get(candidate)
|
||||
if mapped and bundle_has_force_calibration(mapped):
|
||||
return mapped
|
||||
return None
|
||||
|
||||
|
||||
def normalize_sensor_config_ref(config_ref: str | Path) -> str:
|
||||
"""Prefer product bundle names so Orisys loads ``sensors/<name>/`` calibration."""
|
||||
text = str(config_ref).strip()
|
||||
if not text:
|
||||
return text
|
||||
|
||||
path = Path(text)
|
||||
if path.is_file() and path.name.lower() == "sensor.json":
|
||||
parent = path.parent.name.lower()
|
||||
if parent in PRODUCT_BUNDLES:
|
||||
return parent
|
||||
|
||||
bundle = resolve_force_bundle_from_config(text)
|
||||
if bundle is not None:
|
||||
local_json = local_sensor_bundle_sensor_json(bundle)
|
||||
if local_json is not None:
|
||||
return str(local_json.resolve())
|
||||
return bundle
|
||||
|
||||
if text.lower() in PRODUCT_BUNDLES:
|
||||
local_json = local_sensor_bundle_sensor_json(text)
|
||||
if local_json is not None:
|
||||
return str(local_json.resolve())
|
||||
return text
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def orisys_configs_dir() -> Path | None:
|
||||
"""Prefer local sensor configs dir when present; else installed package dir."""
|
||||
local = local_sensor_configs_dir()
|
||||
if list_local_sensor_config_files():
|
||||
if list_local_sensor_config_files() or list_local_sensor_bundle_names():
|
||||
return local
|
||||
try:
|
||||
import orisys
|
||||
@@ -93,37 +227,54 @@ def orisys_configs_dir() -> Path | None:
|
||||
def list_orisys_configs() -> list[str]:
|
||||
"""Return selectable sensor configs.
|
||||
|
||||
Local ``config/*.json`` files (excluding non-sensor stems such as
|
||||
``bench_theme``) are listed first as
|
||||
absolute paths so Start uses the editable copy beside the EXE. Package stems
|
||||
from the installed Orisys wheel are appended when not already covered.
|
||||
Product bundles under ``sensors/<name>/sensor.json`` and Orisys bundle
|
||||
names are listed before legacy flat ``config/*.json`` files so force
|
||||
calibration resolves correctly.
|
||||
"""
|
||||
entries: list[str] = []
|
||||
seen_stems: set[str] = set()
|
||||
|
||||
for path in list_local_sensor_config_files():
|
||||
stem = path.stem.lower()
|
||||
if stem in seen_stems:
|
||||
continue
|
||||
for name in list_local_sensor_bundle_names():
|
||||
path = local_sensor_bundle_sensor_json(name)
|
||||
assert path is not None
|
||||
entries.append(str(path.resolve()))
|
||||
seen_stems.add(stem)
|
||||
seen_stems.add(name.lower())
|
||||
|
||||
try:
|
||||
import orisys
|
||||
|
||||
for stem in orisys.configs.list_config():
|
||||
key = str(stem).strip()
|
||||
if not key:
|
||||
continue
|
||||
if key.lower() in NON_SENSOR_CONFIG_STEMS:
|
||||
for name in orisys.configs.list_sensor_bundles():
|
||||
key = str(name).strip()
|
||||
if not key or key.lower() in NON_SENSOR_CONFIG_STEMS:
|
||||
continue
|
||||
if key.lower() in seen_stems:
|
||||
continue
|
||||
entries.append(key)
|
||||
seen_stems.add(key.lower())
|
||||
|
||||
for stem in orisys.configs.list_config():
|
||||
key = str(stem).strip()
|
||||
if not key or key.lower() in NON_SENSOR_CONFIG_STEMS:
|
||||
continue
|
||||
if key.lower() in seen_stems:
|
||||
continue
|
||||
if key.lower() in PRODUCT_BUNDLES:
|
||||
continue
|
||||
entries.append(key)
|
||||
seen_stems.add(key.lower())
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
for path in list_local_sensor_config_files():
|
||||
stem = path.stem.lower()
|
||||
if stem in seen_stems:
|
||||
continue
|
||||
mapped = LEGACY_STEM_TO_BUNDLE.get(stem)
|
||||
if mapped and mapped.lower() in seen_stems:
|
||||
continue
|
||||
entries.append(str(path.resolve()))
|
||||
seen_stems.add(stem)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
@@ -133,6 +284,11 @@ def config_display_name(value: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
path = Path(text)
|
||||
parts = [p for p in path.as_posix().split("/") if p]
|
||||
if "sensors" in parts:
|
||||
idx = parts.index("sensors")
|
||||
if idx + 1 < len(parts):
|
||||
return parts[idx + 1]
|
||||
if path.suffix.lower() == ".json" or "/" in text or "\\" in text:
|
||||
return path.stem
|
||||
return text
|
||||
@@ -143,6 +299,8 @@ def default_config_name(sensor_type: str = "orisys") -> str:
|
||||
kind = str(sensor_type or "orisys").strip().lower()
|
||||
if kind in ("dd06", "dd06-ov", "roi", "polygon_roi"):
|
||||
return "dd06-ov"
|
||||
if list_local_sensor_bundle_names() or _orisys_bundle_has_force_calibration("box"):
|
||||
return "box"
|
||||
return "ddjx01"
|
||||
|
||||
|
||||
@@ -157,7 +315,7 @@ def _pick_default_config(preferred_stem: str) -> str:
|
||||
|
||||
|
||||
def default_ddjx01_config() -> str:
|
||||
"""Prefer local editable config path, else package stem."""
|
||||
"""Prefer product bundle (box), else local editable config path."""
|
||||
return _pick_default_config(default_config_name("orisys"))
|
||||
|
||||
|
||||
|
||||
+129
-2
@@ -44,6 +44,45 @@ def test_translator_chinese_default() -> None:
|
||||
assert tr._lookup("en_US", "workflow.title") is None
|
||||
|
||||
|
||||
def test_read_forces_from_sensor() -> None:
|
||||
from sensor_bench.acquisition import read_forces_from_sensor, sensor_force_calibrated
|
||||
|
||||
class RawSensor:
|
||||
info = SimpleNamespace(
|
||||
FNORMAL=1,
|
||||
FSHEARX=2,
|
||||
FSHEARY=3,
|
||||
FNORMAL_RAW=4,
|
||||
FSHEARX_RAW=5,
|
||||
FSHEARY_RAW=6,
|
||||
VRAW=7,
|
||||
)
|
||||
_force_cal_sensor_name = "box"
|
||||
|
||||
def read_info(self, *args):
|
||||
return 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, None
|
||||
|
||||
forces = read_forces_from_sensor(RawSensor())
|
||||
assert sensor_force_calibrated(RawSensor())
|
||||
assert forces.force_calibrated is True
|
||||
assert forces.force_unit == "N"
|
||||
assert forces.normal_force == 1.1
|
||||
assert forces.normal_force_raw == 4.4
|
||||
|
||||
class LegacySensor:
|
||||
info = SimpleNamespace(FNORMAL=1, FSHEARX=2, FSHEARY=3, VRAW=4)
|
||||
|
||||
def read_info(self, *args):
|
||||
if len(args) == 4:
|
||||
return 10.0, 20.0, 30.0, None
|
||||
return 10.0, 20.0, 30.0
|
||||
|
||||
legacy = read_forces_from_sensor(LegacySensor())
|
||||
assert legacy.force_calibrated is False
|
||||
assert legacy.force_unit == "raw"
|
||||
assert legacy.normal_force_raw == 10.0
|
||||
|
||||
|
||||
def test_sdk_worker_packet_mapping() -> None:
|
||||
from sensor_bench.acquisition import SdkAcquisitionWorker
|
||||
|
||||
@@ -104,6 +143,9 @@ def test_sdk_worker_packet_mapping() -> None:
|
||||
assert packet.normal_force == 1.0
|
||||
assert packet.shear_x == 2.0
|
||||
assert packet.shear_y == 3.0
|
||||
assert packet.normal_force_raw == 1.0
|
||||
assert packet.force_unit == "raw"
|
||||
assert packet.force_calibrated is False
|
||||
assert packet.raw_frame.shape[:2] == (1080, 1920)
|
||||
assert packet.processed_frame.shape[:2] == (365, 365)
|
||||
|
||||
@@ -137,6 +179,7 @@ def test_force_calibration_and_plot_min_span() -> None:
|
||||
def test_list_orisys_configs_excludes_options() -> None:
|
||||
from sensor_bench.paths import (
|
||||
config_display_name,
|
||||
list_local_sensor_bundle_names,
|
||||
list_local_sensor_config_files,
|
||||
list_orisys_configs,
|
||||
local_sensor_configs_dir,
|
||||
@@ -147,11 +190,76 @@ def test_list_orisys_configs_excludes_options() -> None:
|
||||
assert "options" not in labels
|
||||
assert all(Path(e).stem.lower() != "options" for e in entries if Path(e).suffix.lower() == ".json")
|
||||
|
||||
bundles = list_local_sensor_bundle_names()
|
||||
if bundles:
|
||||
assert labels[0] in bundles
|
||||
|
||||
local_ddjx = local_sensor_configs_dir() / "ddjx01.json"
|
||||
if local_ddjx.exists():
|
||||
local_files = list_local_sensor_config_files()
|
||||
assert any(p.name == "ddjx01.json" for p in local_files)
|
||||
if "box" not in bundles:
|
||||
assert any(config_display_name(e).lower() == "ddjx01" for e in entries)
|
||||
else:
|
||||
assert not any(config_display_name(e).lower() == "ddjx01" for e in entries)
|
||||
|
||||
|
||||
def test_default_config_prefers_box_bundle() -> None:
|
||||
from sensor_bench.paths import (
|
||||
config_display_name,
|
||||
default_config_name,
|
||||
default_ddjx01_config,
|
||||
list_local_sensor_bundle_names,
|
||||
)
|
||||
|
||||
bundles = list_local_sensor_bundle_names()
|
||||
if not bundles:
|
||||
return
|
||||
|
||||
assert default_config_name("orisys") == "box"
|
||||
default = default_ddjx01_config()
|
||||
assert config_display_name(default).lower() == "box"
|
||||
|
||||
|
||||
def test_legacy_config_force_calibration_fallback() -> None:
|
||||
from sensor_bench.acquisition import sensor_force_calibrated
|
||||
from sensor_bench.paths import (
|
||||
bundle_has_force_calibration,
|
||||
local_sensor_configs_dir,
|
||||
resolve_force_bundle_from_config,
|
||||
)
|
||||
|
||||
if not bundle_has_force_calibration("box"):
|
||||
return
|
||||
|
||||
class LegacySensor:
|
||||
pass
|
||||
|
||||
legacy_path = local_sensor_configs_dir() / "ddjx01.json"
|
||||
if legacy_path.exists():
|
||||
assert resolve_force_bundle_from_config(legacy_path) == "box"
|
||||
assert sensor_force_calibrated(LegacySensor(), config_ref=legacy_path) is True
|
||||
|
||||
assert resolve_force_bundle_from_config("ddjx01") == "box"
|
||||
assert resolve_force_bundle_from_config("prototype") == "box"
|
||||
assert sensor_force_calibrated(LegacySensor(), config_ref="ddjx01") is True
|
||||
|
||||
|
||||
def test_normalize_sensor_config_ref_prefers_bundle() -> None:
|
||||
from sensor_bench.paths import (
|
||||
bundle_has_force_calibration,
|
||||
local_sensor_bundle_sensor_json,
|
||||
normalize_sensor_config_ref,
|
||||
)
|
||||
|
||||
if not bundle_has_force_calibration("box"):
|
||||
return
|
||||
|
||||
local_json = local_sensor_bundle_sensor_json("box")
|
||||
assert local_json is not None
|
||||
normalized = normalize_sensor_config_ref("ddjx01")
|
||||
assert Path(normalized).resolve() == local_json.resolve()
|
||||
assert normalize_sensor_config_ref("box") == str(local_json.resolve())
|
||||
|
||||
|
||||
def test_roi_geometry_helpers() -> None:
|
||||
@@ -209,10 +317,13 @@ def test_exhibition_window_offscreen() -> None:
|
||||
assert win.sensor_combo.count() == 2
|
||||
assert win.config_combo is not None
|
||||
assert win.config_combo.count() >= 1
|
||||
from sensor_bench.paths import list_orisys_configs
|
||||
from sensor_bench.paths import config_display_name, default_ddjx01_config, list_orisys_configs
|
||||
|
||||
installed = list_orisys_configs()
|
||||
if "ddjx01" in installed:
|
||||
default = default_ddjx01_config()
|
||||
if config_display_name(default).lower() == "box":
|
||||
assert config_display_name(win._selected_config()).lower() == "box"
|
||||
elif "ddjx01" in installed:
|
||||
assert win.config_combo.currentData() == "ddjx01" or win._selected_config() == "ddjx01"
|
||||
|
||||
win.language_combo.setCurrentIndex(1)
|
||||
@@ -222,6 +333,18 @@ def test_exhibition_window_offscreen() -> None:
|
||||
assert win.btn_reset.text() == "Reset"
|
||||
assert win.show_log_check.text() == "Show log panel"
|
||||
assert "About" in win.about_action.text()
|
||||
title0 = win.plots[0].titleLabel.text
|
||||
assert "FN" in title0
|
||||
assert "raw" in title0
|
||||
|
||||
from sensor_bench.force_calibration import CALIBRATED_FORCE_Y_MAX, CALIBRATED_FORCE_Y_MIN
|
||||
|
||||
win._force_calibrated = True
|
||||
win.force_history.push(time.time(), 1.0, -2.0, 3.0)
|
||||
win._update_plots()
|
||||
ymin, ymax = win.plots[0].viewRange()[1]
|
||||
assert abs(ymin - CALIBRATED_FORCE_Y_MIN) < 1e-6
|
||||
assert abs(ymax - CALIBRATED_FORCE_Y_MAX) < 1e-6
|
||||
|
||||
win.view_combo.setCurrentIndex(1)
|
||||
assert win._view_mode == "captured"
|
||||
@@ -235,9 +358,13 @@ def test_exhibition_window_offscreen() -> None:
|
||||
if __name__ == "__main__":
|
||||
test_load_fixed_mode()
|
||||
test_translator_chinese_default()
|
||||
test_read_forces_from_sensor()
|
||||
test_sdk_worker_packet_mapping()
|
||||
test_force_calibration_and_plot_min_span()
|
||||
test_list_orisys_configs_excludes_options()
|
||||
test_default_config_prefers_box_bundle()
|
||||
test_legacy_config_force_calibration_fallback()
|
||||
test_normalize_sensor_config_ref_prefers_bundle()
|
||||
test_roi_geometry_helpers()
|
||||
test_exhibition_window_offscreen()
|
||||
print("ok")
|
||||
|
||||
Reference in New Issue
Block a user