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"
|
$WorkDir = Join-Path $RepoRoot "build\pyinstaller_work"
|
||||||
$SpecPath = Join-Path $PSScriptRoot "sensor_bench.spec"
|
$SpecPath = Join-Path $PSScriptRoot "sensor_bench.spec"
|
||||||
$RepoConfig = Join-Path $RepoRoot "config"
|
$RepoConfig = Join-Path $RepoRoot "config"
|
||||||
|
$RepoSensors = Join-Path $RepoRoot "sensors"
|
||||||
|
|
||||||
if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
||||||
throw "conda was not found on PATH. Open an Anaconda Prompt or initialize conda for PowerShell."
|
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"
|
conda run -n $EnvName python -m PyInstaller --noconfirm --clean --distpath "$DistDir" --workpath "$WorkDir" "$SpecPath"
|
||||||
if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed." }
|
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")
|
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"
|
$InternalLeak = Join-Path $DistDir "SensorBench\internal"
|
||||||
if (Test-Path $InternalLeak) {
|
if (Test-Path $InternalLeak) {
|
||||||
throw "Public dist must not contain internal/: $InternalLeak"
|
throw "Public dist must not contain internal/: $InternalLeak"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ work_dir = repo_root / "build" / "pyinstaller_work"
|
|||||||
|
|
||||||
datas = [
|
datas = [
|
||||||
(str(repo_root / "config"), "config"),
|
(str(repo_root / "config"), "config"),
|
||||||
|
(str(repo_root / "sensors"), "sensors"),
|
||||||
]
|
]
|
||||||
binaries = []
|
binaries = []
|
||||||
hiddenimports = [
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -17,6 +17,16 @@ def wheel_config_dir() -> Path:
|
|||||||
return inner_pkg.parent / "configs"
|
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:
|
def sync_configs(repo_root: Path) -> int:
|
||||||
src = wheel_config_dir()
|
src = wheel_config_dir()
|
||||||
if not src.is_dir():
|
if not src.is_dir():
|
||||||
@@ -37,9 +47,33 @@ def sync_configs(repo_root: Path) -> int:
|
|||||||
return 0
|
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:
|
def main() -> int:
|
||||||
repo_root = Path(__file__).resolve().parents[1]
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ def main() -> int:
|
|||||||
require(DIST / "config" / "locales" / "zh_CN.json")
|
require(DIST / "config" / "locales" / "zh_CN.json")
|
||||||
require(DIST / "config" / "locales" / "en_US.json")
|
require(DIST / "config" / "locales" / "en_US.json")
|
||||||
require(DIST / "config" / "ddjx01.json")
|
require(DIST / "config" / "ddjx01.json")
|
||||||
|
require(DIST / "sensors" / "box" / "sensor.json")
|
||||||
|
require(DIST / "sensors" / "box" / "force_calibration.json")
|
||||||
require(DIST / "_internal")
|
require(DIST / "_internal")
|
||||||
|
|
||||||
for rel in FORBIDDEN_RELATIVE_PATHS:
|
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_fn": "Normal Force FN",
|
||||||
"plot_fx": "Shear Force FX",
|
"plot_fx": "Shear Force FX",
|
||||||
"plot_fy": "Shear Force FY",
|
"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_done": "Tracker reset.",
|
||||||
"reset_need_start": "Start the sensor before resetting the tracker.",
|
"reset_need_start": "Start the sensor before resetting the tracker.",
|
||||||
"reset_failed": "Reset failed: {message}",
|
"reset_failed": "Reset failed: {message}",
|
||||||
|
|||||||
@@ -67,6 +67,9 @@
|
|||||||
"plot_fn": "法向力 FN",
|
"plot_fn": "法向力 FN",
|
||||||
"plot_fx": "切向力 FX",
|
"plot_fx": "切向力 FX",
|
||||||
"plot_fy": "切向力 FY",
|
"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_done": "追踪器已重置。",
|
||||||
"reset_need_start": "请先启动传感器后再重置追踪器。",
|
"reset_need_start": "请先启动传感器后再重置追踪器。",
|
||||||
"reset_failed": "重置失败:{message}",
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+133
-23
@@ -18,6 +18,11 @@ import cv2
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from .force_calibration import FORCE_SAMPLE_RATE_HZ, LOWPASS_CUTOFF_HZ
|
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)
|
@dataclass(frozen=True)
|
||||||
@@ -35,6 +40,21 @@ class FixedCameraMode:
|
|||||||
return f"{self.width}x{self.height} @{self.fps:g} (DDJX01 / MJPG)"
|
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
|
@dataclass
|
||||||
class SdkPacket:
|
class SdkPacket:
|
||||||
"""One acquisition tick from the Orisys Sensor."""
|
"""One acquisition tick from the Orisys Sensor."""
|
||||||
@@ -51,11 +71,109 @@ class SdkPacket:
|
|||||||
normal_force: float = 0.0
|
normal_force: float = 0.0
|
||||||
shear_x: float = 0.0
|
shear_x: float = 0.0
|
||||||
shear_y: 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
|
session_id: int = 0
|
||||||
mode: Optional[FixedCameraMode] = None
|
mode: Optional[FixedCameraMode] = None
|
||||||
flow: Optional[np.ndarray] = 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]:
|
def orisys_importable() -> tuple[bool, str]:
|
||||||
try:
|
try:
|
||||||
import orisys # noqa: F401
|
import orisys # noqa: F401
|
||||||
@@ -255,7 +373,7 @@ class SdkAcquisitionWorker:
|
|||||||
try:
|
try:
|
||||||
self._status(f"Opening Orisys Sensor ({mode.label()})…")
|
self._status(f"Opening Orisys Sensor ({mode.label()})…")
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"config_name": mode.config_ref,
|
"config_name": normalize_sensor_config_ref(mode.config_ref),
|
||||||
"verbose": False,
|
"verbose": False,
|
||||||
"backend": self._backend,
|
"backend": self._backend,
|
||||||
}
|
}
|
||||||
@@ -297,24 +415,11 @@ class SdkAcquisitionWorker:
|
|||||||
frame_times = [t for t in frame_times if t >= cutoff]
|
frame_times = [t for t in frame_times if t >= cutoff]
|
||||||
fps = float(len(frame_times)) if frame_times else 0.0
|
fps = float(len(frame_times)) if frame_times else 0.0
|
||||||
|
|
||||||
fn = fx = fy = 0.0
|
forces = read_forces_from_sensor(
|
||||||
flow = None
|
self._sensor,
|
||||||
try:
|
include_flow=True,
|
||||||
fn, fx, fy, flow = self._sensor.read_info(
|
config_ref=mode.config_ref,
|
||||||
self._sensor.info.FNORMAL,
|
)
|
||||||
self._sensor.info.FSHEARX,
|
|
||||||
self._sensor.info.FSHEARY,
|
|
||||||
self._sensor.info.VRAW,
|
|
||||||
)
|
|
||||||
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)
|
raw_bgr = _ensure_bgr(raw)
|
||||||
proc_bgr = _ensure_bgr(processed)
|
proc_bgr = _ensure_bgr(processed)
|
||||||
@@ -328,12 +433,17 @@ class SdkAcquisitionWorker:
|
|||||||
height=int(raw_bgr.shape[0]),
|
height=int(raw_bgr.shape[0]),
|
||||||
processed_width=int(proc_bgr.shape[1]),
|
processed_width=int(proc_bgr.shape[1]),
|
||||||
processed_height=int(proc_bgr.shape[0]),
|
processed_height=int(proc_bgr.shape[0]),
|
||||||
normal_force=float(fn),
|
normal_force=forces.normal_force,
|
||||||
shear_x=float(fx),
|
shear_x=forces.shear_x,
|
||||||
shear_y=float(fy),
|
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,
|
session_id=session,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
flow=flow,
|
flow=forces.flow,
|
||||||
)
|
)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._latest = packet
|
self._latest = packet
|
||||||
|
|||||||
@@ -7,8 +7,16 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
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 .force_calibration import FORCE_SAMPLE_RATE_HZ, LOWPASS_CUTOFF_HZ
|
||||||
|
from .paths import normalize_sensor_config_ref
|
||||||
from .roi_geometry import (
|
from .roi_geometry import (
|
||||||
build_roi_warp,
|
build_roi_warp,
|
||||||
load_json_config,
|
load_json_config,
|
||||||
@@ -138,6 +146,7 @@ class Dd06AcquisitionWorker:
|
|||||||
config = load_json_config(config_ref)
|
config = load_json_config(config_ref)
|
||||||
roi_points, roi_image_size = load_roi_polygon(config)
|
roi_points, roi_image_size = load_roi_polygon(config)
|
||||||
out_size = rectified_output_size(config, roi_points)
|
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 = orisys.Sensor("external", config_name=config_ref, verbose=False)
|
||||||
self._sensor.configure(
|
self._sensor.configure(
|
||||||
lowpass_cutoff_hz=LOWPASS_CUTOFF_HZ,
|
lowpass_cutoff_hz=LOWPASS_CUTOFF_HZ,
|
||||||
@@ -190,24 +199,11 @@ class Dd06AcquisitionWorker:
|
|||||||
frame_times = [t for t in frame_times if t >= cutoff]
|
frame_times = [t for t in frame_times if t >= cutoff]
|
||||||
fps = float(len(frame_times)) if frame_times else 0.0
|
fps = float(len(frame_times)) if frame_times else 0.0
|
||||||
|
|
||||||
fn = fx = fy = 0.0
|
forces = read_forces_from_sensor(
|
||||||
flow = None
|
self._sensor,
|
||||||
try:
|
include_flow=True,
|
||||||
fn, fx, fy, flow = self._sensor.read_info(
|
config_ref=self._config_ref,
|
||||||
self._sensor.info.FNORMAL,
|
)
|
||||||
self._sensor.info.FSHEARX,
|
|
||||||
self._sensor.info.FSHEARY,
|
|
||||||
self._sensor.info.VRAW,
|
|
||||||
)
|
|
||||||
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)
|
raw_bgr = _ensure_bgr(frame)
|
||||||
# Prefer sensor.img / get_img result for processed visualization.
|
# Prefer sensor.img / get_img result for processed visualization.
|
||||||
@@ -225,12 +221,17 @@ class Dd06AcquisitionWorker:
|
|||||||
height=int(raw_bgr.shape[0]),
|
height=int(raw_bgr.shape[0]),
|
||||||
processed_width=int(proc_bgr.shape[1]),
|
processed_width=int(proc_bgr.shape[1]),
|
||||||
processed_height=int(proc_bgr.shape[0]),
|
processed_height=int(proc_bgr.shape[0]),
|
||||||
normal_force=float(fn),
|
normal_force=forces.normal_force,
|
||||||
shear_x=float(fx),
|
shear_x=forces.shear_x,
|
||||||
shear_y=float(fy),
|
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,
|
session_id=session,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
flow=flow,
|
flow=forces.flow,
|
||||||
)
|
)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._latest = packet
|
self._latest = packet
|
||||||
|
|||||||
@@ -47,12 +47,13 @@ from .acquisition import (
|
|||||||
from .camera_discovery import list_cameras, pick_default_camera
|
from .camera_discovery import list_cameras, pick_default_camera
|
||||||
from .dd06_acquisition import Dd06AcquisitionWorker
|
from .dd06_acquisition import Dd06AcquisitionWorker
|
||||||
from .force_calibration import (
|
from .force_calibration import (
|
||||||
|
CALIBRATED_FORCE_Y_MAX,
|
||||||
|
CALIBRATED_FORCE_Y_MIN,
|
||||||
FORCE_SAMPLE_RATE_HZ,
|
FORCE_SAMPLE_RATE_HZ,
|
||||||
PLOT_BUFFER_POINTS,
|
PLOT_BUFFER_POINTS,
|
||||||
PLOT_WINDOW_SEC,
|
PLOT_WINDOW_SEC,
|
||||||
Y_AXIS_MIN_SPAN,
|
Y_AXIS_MIN_SPAN,
|
||||||
Y_RANGE_ALPHA,
|
Y_RANGE_ALPHA,
|
||||||
calibrate_normal_force,
|
|
||||||
)
|
)
|
||||||
from .i18n import DEFAULT_LOCALE, Translator
|
from .i18n import DEFAULT_LOCALE, Translator
|
||||||
from .paths import (
|
from .paths import (
|
||||||
@@ -226,6 +227,8 @@ class ExhibitionWindow(QMainWindow):
|
|||||||
self._view_mode = "pure_color"
|
self._view_mode = "pure_color"
|
||||||
self._sensor_type = "orisys"
|
self._sensor_type = "orisys"
|
||||||
self._log_panel_visible = False
|
self._log_panel_visible = False
|
||||||
|
self._force_unit = "raw"
|
||||||
|
self._force_calibrated = False
|
||||||
self._fixed_mode: Optional[FixedCameraMode] = None
|
self._fixed_mode: Optional[FixedCameraMode] = None
|
||||||
self._orisys_config = str(default_ddjx01_config())
|
self._orisys_config = str(default_ddjx01_config())
|
||||||
self._dd06_config = str(default_dd06_config())
|
self._dd06_config = str(default_dd06_config())
|
||||||
@@ -448,12 +451,23 @@ class ExhibitionWindow(QMainWindow):
|
|||||||
self.view_combo.blockSignals(False)
|
self.view_combo.blockSignals(False)
|
||||||
|
|
||||||
for plot, title_key in zip(self.plots, self.plot_titles):
|
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:
|
if self._source_pixmap is None:
|
||||||
self.image_label.setText(self.t("labels.no_preview"))
|
self.image_label.setText(self.t("labels.no_preview"))
|
||||||
self.status_label.setText(self.t("status.ready"))
|
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:
|
def _selected_config(self) -> str:
|
||||||
data = self.config_combo.currentData()
|
data = self.config_combo.currentData()
|
||||||
if data:
|
if data:
|
||||||
@@ -646,6 +660,9 @@ class ExhibitionWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.force_history.reset()
|
self.force_history.reset()
|
||||||
|
self._force_unit = "raw"
|
||||||
|
self._force_calibrated = False
|
||||||
|
self._update_force_plot_titles()
|
||||||
self._awaiting_first_frame = True
|
self._awaiting_first_frame = True
|
||||||
self._source_pixmap = None
|
self._source_pixmap = None
|
||||||
self.image_label.clear()
|
self.image_label.clear()
|
||||||
@@ -794,12 +811,29 @@ class ExhibitionWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
self.status_label.setText(info)
|
self.status_label.setText(info)
|
||||||
self._log(info)
|
self._log(info)
|
||||||
self._log(
|
self._force_unit = packet.force_unit
|
||||||
self.t(
|
self._force_calibrated = packet.force_calibrated
|
||||||
"exhibition.force_fn_n",
|
self._update_force_plot_titles()
|
||||||
value=calibrate_normal_force(packet.normal_force),
|
if packet.force_calibrated:
|
||||||
|
self._log(
|
||||||
|
self.t(
|
||||||
|
"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,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
self._last_packet = packet
|
self._last_packet = packet
|
||||||
self._last_packet_id = packet_id
|
self._last_packet_id = packet_id
|
||||||
@@ -870,8 +904,11 @@ class ExhibitionWindow(QMainWindow):
|
|||||||
x = state.times
|
x = state.times
|
||||||
y = state.values[:, i]
|
y = state.values[:, i]
|
||||||
curve.setData(x, y)
|
curve.setData(x, y)
|
||||||
ymin, ymax = state.y_ranges[i]
|
if self._force_calibrated:
|
||||||
self.plots[i].setYRange(ymin, ymax)
|
ymin, ymax = CALIBRATED_FORCE_Y_MIN, CALIBRATED_FORCE_Y_MAX
|
||||||
|
else:
|
||||||
|
ymin, ymax = state.y_ranges[i]
|
||||||
|
self.plots[i].setYRange(ymin, ymax, padding=0)
|
||||||
self.dot_cores[i].setData(x[-1:], y[-1:])
|
self.dot_cores[i].setData(x[-1:], y[-1:])
|
||||||
x0, x1 = state.x_range
|
x0, x1 = state.x_range
|
||||||
for plot in self.plots:
|
for plot in self.plots:
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ FORCE_SAMPLE_RATE_HZ = 30.0
|
|||||||
PLOT_BUFFER_POINTS = 120
|
PLOT_BUFFER_POINTS = 120
|
||||||
PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ
|
PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ
|
||||||
Y_AXIS_MIN_SPAN = 20000.0
|
Y_AXIS_MIN_SPAN = 20000.0
|
||||||
|
CALIBRATED_FORCE_Y_MIN = -20.0
|
||||||
|
CALIBRATED_FORCE_Y_MAX = 20.0
|
||||||
Y_RANGE_ALPHA = 0.5
|
Y_RANGE_ALPHA = 0.5
|
||||||
LOWPASS_CUTOFF_HZ = 10.0
|
LOWPASS_CUTOFF_HZ = 10.0
|
||||||
|
|
||||||
|
|||||||
+174
-16
@@ -8,6 +8,31 @@ from pathlib import Path
|
|||||||
|
|
||||||
NON_SENSOR_CONFIG_STEMS = {"options", "bench_theme"}
|
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:
|
def is_frozen() -> bool:
|
||||||
return bool(getattr(sys, "frozen", False))
|
return bool(getattr(sys, "frozen", False))
|
||||||
@@ -58,6 +83,11 @@ def local_sensor_configs_dir() -> Path:
|
|||||||
return app_root() / "config"
|
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:
|
def _is_sensor_config_json(path: Path) -> bool:
|
||||||
if path.suffix.lower() != ".json":
|
if path.suffix.lower() != ".json":
|
||||||
return False
|
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))
|
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:
|
def orisys_configs_dir() -> Path | None:
|
||||||
"""Prefer local sensor configs dir when present; else installed package dir."""
|
"""Prefer local sensor configs dir when present; else installed package dir."""
|
||||||
local = local_sensor_configs_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
|
return local
|
||||||
try:
|
try:
|
||||||
import orisys
|
import orisys
|
||||||
@@ -93,37 +227,54 @@ def orisys_configs_dir() -> Path | None:
|
|||||||
def list_orisys_configs() -> list[str]:
|
def list_orisys_configs() -> list[str]:
|
||||||
"""Return selectable sensor configs.
|
"""Return selectable sensor configs.
|
||||||
|
|
||||||
Local ``config/*.json`` files (excluding non-sensor stems such as
|
Product bundles under ``sensors/<name>/sensor.json`` and Orisys bundle
|
||||||
``bench_theme``) are listed first as
|
names are listed before legacy flat ``config/*.json`` files so force
|
||||||
absolute paths so Start uses the editable copy beside the EXE. Package stems
|
calibration resolves correctly.
|
||||||
from the installed Orisys wheel are appended when not already covered.
|
|
||||||
"""
|
"""
|
||||||
entries: list[str] = []
|
entries: list[str] = []
|
||||||
seen_stems: set[str] = set()
|
seen_stems: set[str] = set()
|
||||||
|
|
||||||
for path in list_local_sensor_config_files():
|
for name in list_local_sensor_bundle_names():
|
||||||
stem = path.stem.lower()
|
path = local_sensor_bundle_sensor_json(name)
|
||||||
if stem in seen_stems:
|
assert path is not None
|
||||||
continue
|
|
||||||
entries.append(str(path.resolve()))
|
entries.append(str(path.resolve()))
|
||||||
seen_stems.add(stem)
|
seen_stems.add(name.lower())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import orisys
|
import orisys
|
||||||
|
|
||||||
for stem in orisys.configs.list_config():
|
for name in orisys.configs.list_sensor_bundles():
|
||||||
key = str(stem).strip()
|
key = str(name).strip()
|
||||||
if not key:
|
if not key or key.lower() in NON_SENSOR_CONFIG_STEMS:
|
||||||
continue
|
|
||||||
if key.lower() in NON_SENSOR_CONFIG_STEMS:
|
|
||||||
continue
|
continue
|
||||||
if key.lower() in seen_stems:
|
if key.lower() in seen_stems:
|
||||||
continue
|
continue
|
||||||
entries.append(key)
|
entries.append(key)
|
||||||
seen_stems.add(key.lower())
|
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
|
except Exception: # noqa: BLE001
|
||||||
pass
|
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
|
return entries
|
||||||
|
|
||||||
|
|
||||||
@@ -133,6 +284,11 @@ def config_display_name(value: str) -> str:
|
|||||||
if not text:
|
if not text:
|
||||||
return ""
|
return ""
|
||||||
path = Path(text)
|
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:
|
if path.suffix.lower() == ".json" or "/" in text or "\\" in text:
|
||||||
return path.stem
|
return path.stem
|
||||||
return text
|
return text
|
||||||
@@ -143,6 +299,8 @@ def default_config_name(sensor_type: str = "orisys") -> str:
|
|||||||
kind = str(sensor_type or "orisys").strip().lower()
|
kind = str(sensor_type or "orisys").strip().lower()
|
||||||
if kind in ("dd06", "dd06-ov", "roi", "polygon_roi"):
|
if kind in ("dd06", "dd06-ov", "roi", "polygon_roi"):
|
||||||
return "dd06-ov"
|
return "dd06-ov"
|
||||||
|
if list_local_sensor_bundle_names() or _orisys_bundle_has_force_calibration("box"):
|
||||||
|
return "box"
|
||||||
return "ddjx01"
|
return "ddjx01"
|
||||||
|
|
||||||
|
|
||||||
@@ -157,7 +315,7 @@ def _pick_default_config(preferred_stem: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def default_ddjx01_config() -> 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"))
|
return _pick_default_config(default_config_name("orisys"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+130
-3
@@ -44,6 +44,45 @@ def test_translator_chinese_default() -> None:
|
|||||||
assert tr._lookup("en_US", "workflow.title") is 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:
|
def test_sdk_worker_packet_mapping() -> None:
|
||||||
from sensor_bench.acquisition import SdkAcquisitionWorker
|
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.normal_force == 1.0
|
||||||
assert packet.shear_x == 2.0
|
assert packet.shear_x == 2.0
|
||||||
assert packet.shear_y == 3.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.raw_frame.shape[:2] == (1080, 1920)
|
||||||
assert packet.processed_frame.shape[:2] == (365, 365)
|
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:
|
def test_list_orisys_configs_excludes_options() -> None:
|
||||||
from sensor_bench.paths import (
|
from sensor_bench.paths import (
|
||||||
config_display_name,
|
config_display_name,
|
||||||
|
list_local_sensor_bundle_names,
|
||||||
list_local_sensor_config_files,
|
list_local_sensor_config_files,
|
||||||
list_orisys_configs,
|
list_orisys_configs,
|
||||||
local_sensor_configs_dir,
|
local_sensor_configs_dir,
|
||||||
@@ -147,11 +190,76 @@ def test_list_orisys_configs_excludes_options() -> None:
|
|||||||
assert "options" not in labels
|
assert "options" not in labels
|
||||||
assert all(Path(e).stem.lower() != "options" for e in entries if Path(e).suffix.lower() == ".json")
|
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"
|
local_ddjx = local_sensor_configs_dir() / "ddjx01.json"
|
||||||
if local_ddjx.exists():
|
if local_ddjx.exists():
|
||||||
local_files = list_local_sensor_config_files()
|
local_files = list_local_sensor_config_files()
|
||||||
assert any(p.name == "ddjx01.json" for p in local_files)
|
assert any(p.name == "ddjx01.json" for p in local_files)
|
||||||
assert any(config_display_name(e).lower() == "ddjx01" for e in entries)
|
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:
|
def test_roi_geometry_helpers() -> None:
|
||||||
@@ -209,10 +317,13 @@ def test_exhibition_window_offscreen() -> None:
|
|||||||
assert win.sensor_combo.count() == 2
|
assert win.sensor_combo.count() == 2
|
||||||
assert win.config_combo is not None
|
assert win.config_combo is not None
|
||||||
assert win.config_combo.count() >= 1
|
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()
|
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"
|
assert win.config_combo.currentData() == "ddjx01" or win._selected_config() == "ddjx01"
|
||||||
|
|
||||||
win.language_combo.setCurrentIndex(1)
|
win.language_combo.setCurrentIndex(1)
|
||||||
@@ -222,6 +333,18 @@ def test_exhibition_window_offscreen() -> None:
|
|||||||
assert win.btn_reset.text() == "Reset"
|
assert win.btn_reset.text() == "Reset"
|
||||||
assert win.show_log_check.text() == "Show log panel"
|
assert win.show_log_check.text() == "Show log panel"
|
||||||
assert "About" in win.about_action.text()
|
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)
|
win.view_combo.setCurrentIndex(1)
|
||||||
assert win._view_mode == "captured"
|
assert win._view_mode == "captured"
|
||||||
@@ -235,9 +358,13 @@ def test_exhibition_window_offscreen() -> None:
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_load_fixed_mode()
|
test_load_fixed_mode()
|
||||||
test_translator_chinese_default()
|
test_translator_chinese_default()
|
||||||
|
test_read_forces_from_sensor()
|
||||||
test_sdk_worker_packet_mapping()
|
test_sdk_worker_packet_mapping()
|
||||||
test_force_calibration_and_plot_min_span()
|
test_force_calibration_and_plot_min_span()
|
||||||
test_list_orisys_configs_excludes_options()
|
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_roi_geometry_helpers()
|
||||||
test_exhibition_window_offscreen()
|
test_exhibition_window_offscreen()
|
||||||
print("ok")
|
print("ok")
|
||||||
|
|||||||
Reference in New Issue
Block a user