拆分收尾:标定归属规则移入 CalibrationService(app_window 2340→2304 行)
- What: `apps/studio/src/orisys_studio/calibration_service.py` 新增三条**无状态**规则, 它们原来长在窗口里:`identity_of(path)`(这份系数文件声称属于哪台设备——用 SDK 自己的 解析器读,与设备将要加载的是同一份文档;读不出来就什么都不声称)、`same_path(a, b)` (同一个配置路径的不同写法不算不一致;两个空值也不算)、 `target_mismatch(path, *, sensor_type, config_ref)`(在确认框之前就说清楚"这份标定 看起来不属于这台设备"的理由)。服务本来就是这个文件工作的所有者,规则回到了它这里, 并且不需要窗口就能测。 - What: 窗口保留 `calibration_identity` / `calibration_target_mismatch` 两个转发 (tests 与 tools/ui_check.py 通过窗口驱动它们),删除 `_same_path`。 `_on_calibration_apply` 仍留在窗口:它要弹确认框、要把路径交给采集 worker,那是交互 与会话,不是关于文件的规则;`_calibration_summary`、`_drain_calibration_events`、 `_drain_reset_events`、`_on_calibration_point/cancelled/exported` 同理(面板与状态栏 的胶水)。 - What: 新增 apps/studio/tests/test_calibration_service.py:4 个用例——身份读出 sensor_type/sn/config_ref、读不出来的文件既无身份也无"不匹配"指控(不能把"不知道" 当成"不一致")、另一台设备/另一份配置会被逐条报出而同一台不会、同一路径的不同写法 不算不一致而非路径值算。 - Why: 这是 app_window 拆分计划里最后一块「有独立规则可搬」的部分。搬完之后 "这份标定是谁的"只有一处实现,窗口只剩对话与转发。 - Verification: pytest shared/tests apps/studio/tests(offscreen)636 passed / 0 failed / 15 skipped(原 632 + 新增 4);apps/studio/tools/ui_check.py 587 项检查 0 失败; .repro/locale_check.py 0 缺失。行为等价(同一份实现 + 一层转发)。 - Not in this change: 预览(_start_preview/_stop_preview/_on_preview_tick)与 2D 渲染 (_render_run_image/_show_image_frame/_depth_range)仍留在窗口——它们是面板与相机的 胶水,不属于任何一个已拆出的变更驱动;已知缺陷(本次未修,另行处理):worker 的 on_reset_applied 从未接到 recording.note_reset,真机上按"清零"不会记录确认事件。
This commit is contained in:
@@ -46,6 +46,7 @@ from orisys_app_shared.record.sample import to_record_sample
|
||||
from orisys_studio.recording import RecordingCoordinator, record_config_from_prefs
|
||||
from orisys_studio.replay import ReplayEngine
|
||||
from orisys_studio.replay_controller import ReplayController
|
||||
from orisys_studio.calibration_service import CalibrationService
|
||||
from orisys_studio.display_prefs import DisplayPrefsController
|
||||
from orisys_studio.view_layout import ViewLayoutController
|
||||
from orisys_studio import session_metadata
|
||||
@@ -1509,57 +1510,20 @@ class StudioMainWindow(QMainWindow):
|
||||
self.recorder.log_event("calibration_exported", str(path))
|
||||
self.statusBar().showMessage(t("cal.export_only", path=Path(path).name))
|
||||
|
||||
# ------------------------------------------------- calibration (forwards)
|
||||
# "What does this coefficient file claim, and does it match the bench?" lives in
|
||||
# ``CalibrationService`` (with the rest of the calibration file work, and without
|
||||
# Qt). The dialog and the worker hand-over stay here: they are interaction, not a
|
||||
# rule about files.
|
||||
def calibration_identity(self, path: str) -> dict:
|
||||
"""What device a coefficient file says it belongs to.
|
||||
|
||||
Read with the SDK's own parser so the check sees the same document the
|
||||
device would load. A file that cannot be read yields no identity, and the
|
||||
caller then applies it without a mismatch claim.
|
||||
"""
|
||||
try:
|
||||
import orisys.calibration as sdk_calibration
|
||||
|
||||
document = sdk_calibration.load(path, strict=True)
|
||||
except Exception: # noqa: BLE001 - reported by the apply path itself
|
||||
logger.exception("Could not read calibration identity from %s", path)
|
||||
return {}
|
||||
identity = dict(getattr(document, "identity", {}) or {})
|
||||
pipeline = dict(getattr(document, "pipeline", {}) or {})
|
||||
return {
|
||||
"sensor_type": str(identity.get("sensor_type") or ""),
|
||||
"sn": str(identity.get("sn") or ""),
|
||||
"config_ref": str(pipeline.get("config_ref") or ""),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _same_path(first: str, second: str) -> bool:
|
||||
if not first or not second:
|
||||
return True # nothing to compare: not a mismatch
|
||||
if first == second:
|
||||
return True
|
||||
try:
|
||||
return Path(first).resolve() == Path(second).resolve()
|
||||
except Exception: # noqa: BLE001 - a non-path value simply differs
|
||||
return False
|
||||
return CalibrationService.identity_of(path)
|
||||
|
||||
def calibration_target_mismatch(
|
||||
self, path: str, *, sensor_type: str, config_ref: str
|
||||
) -> list[str]:
|
||||
"""Reasons this calibration does not obviously belong to this device.
|
||||
|
||||
Applying coefficients that were measured on a different sensor silently
|
||||
changes what every recorded newton value means, so the mismatch is stated
|
||||
before the confirmation, not discovered later.
|
||||
"""
|
||||
recorded = self.calibration_identity(path)
|
||||
reasons: list[str] = []
|
||||
recorded_type = recorded.get("sensor_type") or ""
|
||||
if recorded_type and sensor_type and recorded_type != sensor_type:
|
||||
reasons.append(f"sensor_type: {recorded_type} != {sensor_type}")
|
||||
recorded_config = recorded.get("config_ref") or ""
|
||||
if recorded_config and not self._same_path(recorded_config, config_ref):
|
||||
reasons.append(f"config_ref: {recorded_config} != {config_ref}")
|
||||
return reasons
|
||||
return CalibrationService.target_mismatch(
|
||||
path, sensor_type=sensor_type, config_ref=config_ref
|
||||
)
|
||||
|
||||
def _on_calibration_apply(self, path: str) -> None:
|
||||
"""Push freshly exported coefficients into the device — on confirmation.
|
||||
|
||||
@@ -128,3 +128,67 @@ class CalibrationService:
|
||||
sensor_type=str(sensor_type or "box"),
|
||||
local_bundle_dir=local_bundle_dir,
|
||||
)
|
||||
|
||||
# ------------------------------------------------- who a file belongs to
|
||||
# The rules below take no state: they answer "what does this file claim, and does it
|
||||
# match what is on the bench?" — which is the question the *apply* path has to settle
|
||||
# before coefficients measured on another sensor silently change every newton value.
|
||||
@staticmethod
|
||||
def identity_of(path: str | Path) -> dict:
|
||||
"""What device a coefficient file says it belongs to.
|
||||
|
||||
Read with the SDK's own parser so the check sees the same document the device
|
||||
would load. A file that cannot be read yields no identity, and the caller then
|
||||
applies it without a mismatch claim.
|
||||
"""
|
||||
try:
|
||||
import orisys.calibration as sdk_calibration
|
||||
|
||||
document = sdk_calibration.load(str(path), strict=True)
|
||||
except Exception: # noqa: BLE001 - reported by the apply path itself
|
||||
logger.exception("Could not read calibration identity from %s", path)
|
||||
return {}
|
||||
identity = dict(getattr(document, "identity", {}) or {})
|
||||
pipeline = dict(getattr(document, "pipeline", {}) or {})
|
||||
return {
|
||||
"sensor_type": str(identity.get("sensor_type") or ""),
|
||||
"sn": str(identity.get("sn") or ""),
|
||||
"config_ref": str(pipeline.get("config_ref") or ""),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def same_path(first: str, second: str) -> bool:
|
||||
"""Whether two configuration references name the same file.
|
||||
|
||||
Two empty values are "the same" on purpose: nothing to compare is not a
|
||||
mismatch, and refusing to apply an unlabelled calibration would be inventing a
|
||||
disagreement.
|
||||
"""
|
||||
if not first or not second:
|
||||
return True
|
||||
if first == second:
|
||||
return True
|
||||
try:
|
||||
return Path(first).resolve() == Path(second).resolve()
|
||||
except Exception: # noqa: BLE001 - a non-path value simply differs
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def target_mismatch(
|
||||
cls, path: str | Path, *, sensor_type: str, config_ref: str
|
||||
) -> list[str]:
|
||||
"""Reasons this calibration does not obviously belong to this device.
|
||||
|
||||
Applying coefficients that were measured on a different sensor silently changes
|
||||
what every recorded newton value means, so the mismatch is stated before the
|
||||
confirmation, not discovered later.
|
||||
"""
|
||||
recorded = cls.identity_of(path)
|
||||
reasons: list[str] = []
|
||||
recorded_type = recorded.get("sensor_type") or ""
|
||||
if recorded_type and sensor_type and recorded_type != sensor_type:
|
||||
reasons.append(f"sensor_type: {recorded_type} != {sensor_type}")
|
||||
recorded_config = recorded.get("config_ref") or ""
|
||||
if recorded_config and not cls.same_path(recorded_config, config_ref):
|
||||
reasons.append(f"config_ref: {recorded_config} != {config_ref}")
|
||||
return reasons
|
||||
|
||||
Reference in New Issue
Block a user