把 Studio 主机协议改成单向端口,并让「从视频重算」走分析入口

控制器经 AcquireGate / ImageSink / ShellNotes 与信号接线,采集 tick 用 acquire_presenter 上屏;三维测试证明分析结果已喂 3D 后,重算并入 on_analysis_requested。
验证:apps/studio 下相关 pytest 与 tools/ui_check.py(634 checks)通过。
This commit is contained in:
ykcheungab
2026-10-05 14:00:24 +08:00
parent 25ccffd814
commit 28e955a817
27 changed files with 1764 additions and 745 deletions
+104 -9
View File
@@ -4,11 +4,15 @@ The catalog replaced two rules that were wrong in the same direction — a hard-
list, and "any ``*.json`` beside a ``sensor.json`` is another configuration". These tests build
a tree containing exactly the shapes those rules mishandled: a descriptor bundle, a legacy
``sensor.json`` bundle, a folder holding only ``force_calibration.json``, a malformed
``manifest.json``, a bundle whose declared asset is missing, and two bundles sharing one
``profile_id``.
``manifest.json``, a bundle whose declared asset is missing, a bundle with a calibration
selected, and two bundles sharing one ``profile_id``.
The tree is built here, in the test's own temporary directory: a committed fixture would be a
second copy of the bundle format that could drift from the real one without anyone noticing.
The labels are asserted here too, because they are the catalog's whole operator-facing
contract: what a configuration is, where it came from and what it will give — never a
``profile_id``, an ``ok``/``degraded``/``failed`` verdict, or the word ``legacy``.
"""
from __future__ import annotations
@@ -33,6 +37,19 @@ from orisys_app_shared.sensors import (
CAMERA_OK = {"resolution_raw": [640, 480], "fps": 30, "resolution_stitch": [100, 100]}
TRACKER_OK = {"algorithm": "dis"}
#: A calibration that loads cleanly and declares the family it belongs to, so a selected
#: calibration produces no warning at all: the shape an operator should meet with no remark.
CALIBRATION = {
"schema_version": 2,
"unit": "N",
"channels": {
"normal": {"points": [[0, 0], [100, 1.0]]},
"shear_x": {"points": [[0, 0], [100, 1.0]]},
"shear_y": {"points": [[0, 0], [100, 1.0]]},
},
"meta": {"sensor_type": "mini", "sn": "FIXTURE-1"},
}
def _config(
*, declared_type: str | None = None, stitcher: bool = False
@@ -53,7 +70,7 @@ def _descriptor(profile_id: str, **overrides: Any) -> dict[str, Any]:
data: dict[str, Any] = {
"schema_version": 1,
"profile_id": profile_id,
"display_name": profile_id,
"display_name": "Mini Lab",
"product_family": "mini",
"processing_backend": "single_camera",
"sensor_json": "sensor.json",
@@ -93,11 +110,16 @@ def trees(tmp_path):
user / "missing-assets" / "manifest.json",
_descriptor(
"orisys.finger.missing-r1",
display_name="Finger R01 (map missing)",
product_family="finger",
processing_backend="uv_unroll",
computational_assets={"unroll_map": "assets/not-here.npz"},
),
)
# A descriptor bundle with a calibration selected: the valid shape.
_write(user / "mini-cal" / "sensor.json", _config())
_write(user / "mini-cal" / "manifest.json", _descriptor("orisys.mini.cal-r1"))
_write(user / "mini-cal" / "force_calibration.json", CALIBRATION)
# Two bundles declaring one profile_id.
for name in ("dup-a", "dup-b"):
_write(user / name / "sensor.json", _config())
@@ -113,6 +135,7 @@ def test_every_bundle_shape_is_discovered_with_its_origin(trees):
"shipped/box",
"shipped/calib-only",
"user/mini-lab",
"user/mini-cal",
"user/broken-manifest",
"user/missing-assets",
"user/dup-a",
@@ -233,13 +256,85 @@ def test_an_entry_the_sdk_rejects_is_never_reported_as_startable(trees):
assert (entry.start_blocked_reason is None) == expected, entry.ref
def test_the_label_states_family_profile_origin_and_validity(trees):
def test_the_label_states_what_it_is_where_it_came_from_and_what_it_gives(trees):
entry = trees.by_ref("user/mini-lab")
assert entry is not None
assert entry.label() == "Mini Lab · User · No calibration selected — raw force"
def test_a_startable_label_carries_no_identity_and_no_severity_word(trees):
"""What an operator chooses between is products and outcomes, not internal names."""
for entry in trees.entries:
if not entry.valid:
continue
label = entry.label()
assert entry.profile_id not in label, label
for word in ("ok", "degraded", "failed", "legacy"):
assert word not in label, f"{word!r} reached the row: {label}"
def test_no_calibration_selected_reads_as_raw_force_not_as_a_defect(trees):
entry = trees.by_ref("user/mini-lab")
assert entry is not None
# The SDK reports the absent calibration as a warning, which is the state the row used to
# read as "degraded" — a severity word that never said "raw force" or "not selected".
assert entry.state == "degraded"
assert "No calibration selected — raw force" in entry.label()
assert entry.selected_calibration == ""
def test_a_selected_calibration_reads_as_a_selection(trees):
entry = trees.by_ref("user/mini-cal")
assert entry is not None and entry.valid, entry.reason if entry else "missing"
assert entry.state == "ok", entry.reason
assert entry.selected_calibration == "force_calibration.json"
# Named, and said as something selected — never as a property of the bundle ...
assert entry.label() == "Mini Lab · User · Calibration: force_calibration.json"
# ... and the "ok" the SDK calls that is not a word an operator needs to read.
assert "ok" not in entry.label()
def test_an_unstartable_label_says_cannot_start_with_the_sdks_own_reason(trees):
entry = trees.by_ref("user/missing-assets")
label = entry.label()
assert "finger" in label
assert entry.profile_id in label
assert ORIGIN_USER in label
assert entry.state in label
assert entry is not None and not entry.valid
assert entry.label() == f"Finger R01 (map missing) · Cannot start: {entry.reason}"
assert "unroll_map" in entry.label()
def test_a_duplicate_label_names_the_twin(trees):
first = trees.by_ref("user/dup-a")
second = trees.by_ref("user/dup-b")
assert first is not None and second is not None
assert first.label() == "Mini Lab · User · Also provided by user/dup-b"
assert second.label() == "Mini Lab · User · Also provided by user/dup-a"
def test_the_tooltip_keeps_the_identity_the_shape_the_state_and_the_findings(trees):
"""The facts stop being the first thing read; they do not stop being available."""
entry = trees.by_ref("user/mini-lab")
assert entry is not None
tooltip = entry.tooltip()
assert entry.profile_id in tooltip
assert "descriptor bundle" in tooltip
assert f"SDK validation: {entry.state}" in tooltip
assert "processing can start" in tooltip, "the verdict word is left unexplained"
assert "calibration: none selected — raw force" in tooltip
# A finding is a sentence: the code is dropped, the capability it names is not.
assert "calibration.absent" not in tooltip
assert "calibrated force is unavailable; raw force is available" in tooltip
def test_the_tooltip_of_a_legacy_bundle_says_its_shape(trees):
entry = trees.by_ref("shipped/box")
assert entry is not None
tooltip = entry.tooltip()
assert "compatibility bundle, no descriptor" in tooltip
assert entry.profile_id in tooltip
# The code the row lost is not lost here: it is rendered as what it means, and the file it
# is about is kept.
assert "descriptor.legacy" not in tooltip
assert "no manifest.json" in tooltip
assert "built-in profile table" in tooltip
def test_the_root_decision_names_the_root_in_use():