让直播标定文件从传感器对话框进入打开链路。
对话框可选配置默认、原始力或浏览文件,并按配置条目写入 prefs;点运行时把 cal_path(Finger 含 device_id)交给 worker,采集中则热更新。对话框与打开链路单测通过。
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
"""Live calibration choice in the sensor dialog: pick, remember, open, hot-apply."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.sensors import scan_sensor_catalog
|
||||
from orisys_studio.ui import sensor_picker as picker_module
|
||||
from orisys_studio.ui.sensor_dialog import ROLE_DEFAULT, ROLE_FILE, ROLE_NONE, SensorDialog
|
||||
from orisys_studio.ui.sensor_picker import SensorPickerWidget
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
CONFIG = {
|
||||
"name": "fixture",
|
||||
"camera": {
|
||||
"resolution_raw": [640, 480],
|
||||
"fps": 30,
|
||||
"resolution_stitch": [100, 100],
|
||||
},
|
||||
"tracker": {"algorithm": "dis"},
|
||||
"stitcher": {"n": [1, 1]},
|
||||
}
|
||||
|
||||
|
||||
def _write(path: Path, payload: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _descriptor(profile_id: str, **overrides: object) -> dict[str, object]:
|
||||
data: dict[str, object] = {
|
||||
"schema_version": 1,
|
||||
"profile_id": profile_id,
|
||||
"display_name": "Mini lab",
|
||||
"product_family": "mini",
|
||||
"processing_backend": "single_camera",
|
||||
"sensor_json": "sensor.json",
|
||||
}
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
|
||||
class _Host(QWidget):
|
||||
"""Window stand-in with the live-calibration prefs API the dialog expects."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.prefs: dict = {}
|
||||
self.applied = 0
|
||||
self._choices: dict[str, tuple[str, Path | None, str]] = {}
|
||||
self.picker: SensorPickerWidget | None = None
|
||||
self._dialog: SensorDialog | None = None
|
||||
|
||||
def _on_reveal_config(self) -> None:
|
||||
return
|
||||
|
||||
def _prefs_key(self) -> str | None:
|
||||
if self.picker is None:
|
||||
return None
|
||||
entry = self.picker.selected_entry()
|
||||
return None if entry is None else entry.ref
|
||||
|
||||
def live_calibration_choice(self) -> tuple[str, Path | None, str]:
|
||||
key = self._prefs_key()
|
||||
if not key:
|
||||
return ROLE_NONE, None, ""
|
||||
return self._choices.get(key, (ROLE_NONE, None, ""))
|
||||
|
||||
def set_live_calibration_choice(
|
||||
self,
|
||||
*,
|
||||
role: str,
|
||||
path: Path | str | None = None,
|
||||
device_id: str | None = None,
|
||||
) -> None:
|
||||
key = self._prefs_key()
|
||||
if not key:
|
||||
return
|
||||
file_path = Path(path) if role == ROLE_FILE and path else None
|
||||
self._choices[key] = (role, file_path, str(device_id or ""))
|
||||
store = self.prefs.setdefault("live_calibration", {})
|
||||
payload: dict[str, str] = {"role": role}
|
||||
if file_path is not None:
|
||||
payload["path"] = str(file_path)
|
||||
if device_id:
|
||||
payload["device_id"] = str(device_id)
|
||||
store[key] = payload
|
||||
|
||||
def apply_live_calibration_choice(self) -> None:
|
||||
if self._dialog is not None and self._dialog.choice_problem():
|
||||
return
|
||||
self.applied += 1
|
||||
|
||||
|
||||
def _make_picker(qt_app, tmp_path, monkeypatch, build):
|
||||
root = tmp_path / "user"
|
||||
build(root)
|
||||
catalog = scan_sensor_catalog(
|
||||
shipped_root=tmp_path / "no-shipped-root", user_root=root
|
||||
)
|
||||
monkeypatch.setattr(picker_module, "scan_sensor_catalog", lambda: catalog)
|
||||
return SensorPickerWidget()
|
||||
|
||||
|
||||
def _dialog_for(picker) -> tuple[_Host, SensorDialog]:
|
||||
host = _Host()
|
||||
host.picker = picker
|
||||
dialog = SensorDialog(window=host, picker=picker)
|
||||
host._dialog = dialog
|
||||
return host, dialog
|
||||
|
||||
|
||||
def test_without_a_profile_default_the_combo_starts_on_raw_force(
|
||||
qt_app, tmp_path, monkeypatch
|
||||
):
|
||||
def build(root: Path) -> None:
|
||||
_write(root / "mini-lab" / "sensor.json", CONFIG)
|
||||
_write(root / "mini-lab" / "manifest.json", _descriptor("orisys.mini.lab-r1"))
|
||||
|
||||
picker = _make_picker(qt_app, tmp_path, monkeypatch, build)
|
||||
picker.set_sensor_type("mini")
|
||||
_host, dialog = _dialog_for(picker)
|
||||
try:
|
||||
assert dialog.calibration_combo.findData(ROLE_DEFAULT) < 0
|
||||
assert dialog.current_role() == ROLE_NONE
|
||||
assert dialog.current_calibration_path() is None
|
||||
assert t("sensor.catalog_calibration_none") in dialog.profile_label.text()
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
picker.deleteLater()
|
||||
|
||||
|
||||
def test_a_box_profile_default_appears_as_a_combo_row(qt_app):
|
||||
"""The shipped box bundle declares ``force_calibration.json`` as its default."""
|
||||
picker = SensorPickerWidget()
|
||||
picker.set_sensor_type("box")
|
||||
entry = picker.selected_entry()
|
||||
assert entry is not None
|
||||
assert entry.profile is not None
|
||||
assert entry.profile.default_calibration is not None, (
|
||||
"premise: shipped box must expose a profile default calibration"
|
||||
)
|
||||
_host, dialog = _dialog_for(picker)
|
||||
try:
|
||||
assert dialog.calibration_combo.findData(ROLE_DEFAULT) >= 0
|
||||
assert dialog.calibration_combo.findData(ROLE_NONE) >= 0
|
||||
assert dialog.current_role() == ROLE_NONE
|
||||
dialog.calibration_combo.setCurrentIndex(
|
||||
dialog.calibration_combo.findData(ROLE_DEFAULT)
|
||||
)
|
||||
path = dialog.current_calibration_path()
|
||||
assert path is not None and path.name == "force_calibration.json"
|
||||
assert t(
|
||||
"sensor.catalog_calibration_selected", name="force_calibration.json"
|
||||
) in dialog.profile_label.text()
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
picker.deleteLater()
|
||||
|
||||
|
||||
def test_browsing_a_file_shows_its_name_and_persists_per_bundle(
|
||||
qt_app, tmp_path, monkeypatch
|
||||
):
|
||||
def build(root: Path) -> None:
|
||||
for name, family, profile in (
|
||||
("mini-a", "mini", "orisys.mini.a-r1"),
|
||||
("finger-a", "finger", "orisys.finger.a-r1"),
|
||||
):
|
||||
folder = root / name
|
||||
_write(folder / "sensor.json", CONFIG)
|
||||
extras = {
|
||||
"display_name": name,
|
||||
"product_family": family,
|
||||
"processing_backend": (
|
||||
"uv_unroll" if family == "finger" else "single_camera"
|
||||
),
|
||||
}
|
||||
if family == "finger":
|
||||
extras["computational_assets"] = {}
|
||||
_write(folder / "manifest.json", _descriptor(profile, **extras))
|
||||
|
||||
picker = _make_picker(qt_app, tmp_path, monkeypatch, build)
|
||||
host, dialog = _dialog_for(picker)
|
||||
cal = tmp_path / "custom.json"
|
||||
cal.write_text(
|
||||
json.dumps({"meta": {"sensor_type": "mini", "sn": "X"}}), encoding="utf-8"
|
||||
)
|
||||
try:
|
||||
picker.set_sensor_type("mini")
|
||||
dialog.select_calibration_file(cal)
|
||||
assert dialog.current_role() == ROLE_FILE
|
||||
assert dialog.calibration_combo.currentText() == t(
|
||||
"video_analysis.calibration_file", name="custom.json"
|
||||
)
|
||||
mini_key = picker.selected_entry().ref
|
||||
assert host.prefs["live_calibration"][mini_key]["path"] == str(cal)
|
||||
|
||||
picker.set_sensor_type("finger")
|
||||
dialog.refresh()
|
||||
assert dialog.current_role() == ROLE_NONE
|
||||
finger_key = picker.selected_entry().ref
|
||||
assert finger_key != mini_key
|
||||
assert ROLE_FILE != host.prefs.get("live_calibration", {}).get(
|
||||
finger_key, {}
|
||||
).get("role")
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
picker.deleteLater()
|
||||
|
||||
|
||||
def test_a_finger_file_with_a_mismatched_sn_is_not_a_valid_choice(
|
||||
qt_app, tmp_path, monkeypatch
|
||||
):
|
||||
def build(root: Path) -> None:
|
||||
folder = root / "finger-lab"
|
||||
_write(folder / "sensor.json", CONFIG)
|
||||
_write(
|
||||
folder / "manifest.json",
|
||||
_descriptor(
|
||||
"orisys.finger.lab-r1",
|
||||
display_name="Finger lab",
|
||||
product_family="finger",
|
||||
processing_backend="uv_unroll",
|
||||
computational_assets={},
|
||||
),
|
||||
)
|
||||
|
||||
picker = _make_picker(qt_app, tmp_path, monkeypatch, build)
|
||||
picker.set_sensor_type("finger")
|
||||
host, dialog = _dialog_for(picker)
|
||||
cal = tmp_path / "DD02-14.json"
|
||||
cal.write_text(
|
||||
json.dumps({"meta": {"sensor_type": "finger", "sn": "DD02-14"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
try:
|
||||
dialog.select_calibration_file(cal)
|
||||
# Offscreen dialogs are not shown; ``isHidden`` is the visibility flag.
|
||||
assert not dialog.device_edit.isHidden()
|
||||
assert dialog.device_edit.text() == "DD02-14"
|
||||
assert dialog.choice_problem() == ""
|
||||
dialog.device_edit.setText("OTHER")
|
||||
assert dialog.choice_problem() == t(
|
||||
"video_analysis.device_mismatch", sn="DD02-14"
|
||||
)
|
||||
dialog.btn_apply.click()
|
||||
assert host.applied == 0
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
picker.deleteLater()
|
||||
|
||||
|
||||
def test_window_resolves_finger_file_choice_for_open(make_window, tmp_path):
|
||||
window = make_window()
|
||||
cal = tmp_path / "DD02-14.json"
|
||||
cal.write_text(
|
||||
json.dumps({"meta": {"sensor_type": "finger", "sn": "DD02-14"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
finger = next(
|
||||
(
|
||||
entry
|
||||
for entry in scan_sensor_catalog().entries
|
||||
if str(entry.product_family) == "finger"
|
||||
and entry.start_blocked_reason is None
|
||||
),
|
||||
None,
|
||||
)
|
||||
if finger is None:
|
||||
pytest.skip("no startable Finger profile in the catalog")
|
||||
window.sensor_picker.set_sensor_type("finger")
|
||||
window.sensor_picker.set_config_name(finger.key)
|
||||
window.set_live_calibration_choice(
|
||||
role=ROLE_FILE, path=cal, device_id="DD02-14"
|
||||
)
|
||||
cal_path, device_id, problem = window._resolved_live_calibration()
|
||||
assert problem == ""
|
||||
assert cal_path == cal
|
||||
assert device_id == "DD02-14"
|
||||
|
||||
window.set_live_calibration_choice(
|
||||
role=ROLE_FILE, path=cal, device_id="OTHER"
|
||||
)
|
||||
_path, _device, problem = window._resolved_live_calibration()
|
||||
assert problem == t("video_analysis.device_mismatch", sn="DD02-14")
|
||||
|
||||
|
||||
def test_apply_while_running_queues_set_pending_calibration(make_window, tmp_path):
|
||||
window = make_window()
|
||||
cal = tmp_path / "force_calibration.json"
|
||||
cal.write_text('{"schema_version": 2}', encoding="utf-8")
|
||||
window.set_live_calibration_choice(role=ROLE_FILE, path=cal)
|
||||
pending: list[str] = []
|
||||
|
||||
class _Worker:
|
||||
def add_sink(self, _sink) -> None:
|
||||
return
|
||||
|
||||
def set_pending_calibration(self, path) -> None:
|
||||
pending.append(str(path))
|
||||
|
||||
worker = _Worker()
|
||||
# Both slots filled so ``ensure_workers`` does not replace the shim.
|
||||
window.session.set_worker("box", worker)
|
||||
window.session.set_worker("finger", worker)
|
||||
window.session.select_worker("box")
|
||||
window.session.set_running(True)
|
||||
assert window._running
|
||||
assert window._active_worker is worker
|
||||
|
||||
window.apply_live_calibration_choice()
|
||||
assert pending == [str(cal)]
|
||||
|
||||
pending.clear()
|
||||
window.set_live_calibration_choice(role=ROLE_NONE)
|
||||
window.apply_live_calibration_choice()
|
||||
assert pending == []
|
||||
assert window.statusBar().currentMessage() == t("sensor.live_calibration_remembered")
|
||||
Reference in New Issue
Block a user