把 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:
@@ -1,8 +1,8 @@
|
||||
"""Sensor catalog helpers.
|
||||
|
||||
The catalog answers three questions the picker and the resolver both need: which configuration
|
||||
bundles exist, what each one is (family, profile, origin and the SDK's verdict), and where a
|
||||
user's own bundles go on this machine.
|
||||
bundles exist, what each one is (name, origin, calibration selection and the SDK's verdict), and
|
||||
where a user's own bundles go on this machine.
|
||||
"""
|
||||
|
||||
from orisys_app_shared.sensors.bundle_catalog import (
|
||||
@@ -14,6 +14,9 @@ from orisys_app_shared.sensors.bundle_catalog import (
|
||||
ROOT_KIND_EXPLICIT,
|
||||
ROOT_KIND_FALLBACK,
|
||||
ROOT_KIND_SIBLING,
|
||||
SHAPE_COMPATIBILITY,
|
||||
SHAPE_DESCRIPTOR,
|
||||
SHAPE_UNREADABLE,
|
||||
STATE_UNREADABLE,
|
||||
CatalogEntry,
|
||||
CatalogRoot,
|
||||
@@ -33,6 +36,9 @@ __all__ = [
|
||||
"ROOT_KIND_EXPLICIT",
|
||||
"ROOT_KIND_FALLBACK",
|
||||
"ROOT_KIND_SIBLING",
|
||||
"SHAPE_COMPATIBILITY",
|
||||
"SHAPE_DESCRIPTOR",
|
||||
"SHAPE_UNREADABLE",
|
||||
"STATE_UNREADABLE",
|
||||
"CatalogEntry",
|
||||
"CatalogRoot",
|
||||
|
||||
@@ -24,6 +24,13 @@ hiding it is how the same file ends up offered as a sensor config.
|
||||
|
||||
Two bundles that declare one ``profile_id`` are both listed and each names the other. Nothing
|
||||
is shadowed, and the ambiguity is available to a caller that must refuse to start.
|
||||
|
||||
What the picker *says* about an entry is deliberately not what it is called internally. The
|
||||
label names the product, whether the bundle shipped with the app or came from the operator, and
|
||||
what the calibration selection gives — a named file, or raw force. The ``profile_id``, the
|
||||
bundle's shape and the SDK's own ``ok``/``degraded``/``failed`` verdict live in
|
||||
:meth:`CatalogEntry.tooltip`: they inform the operator who asked, instead of heading the row
|
||||
with a word that only says "something optional is missing".
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -43,7 +50,7 @@ from orisys_app_shared.paths import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - typing only
|
||||
from orisys.sensors import ResolvedSensorProfile
|
||||
from orisys.sensors import Issue, ResolvedSensorProfile
|
||||
|
||||
#: Which root an entry came from.
|
||||
ORIGIN_SHIPPED = "shipped"
|
||||
@@ -70,6 +77,45 @@ KNOWN_FAMILY_ORDER: tuple[str, ...] = ("box", "mini", "finger", "finger-mems")
|
||||
#: the SDK's own :attr:`ValidationReport.state` (``ok`` / ``degraded`` / ``failed``).
|
||||
STATE_UNREADABLE = "invalid"
|
||||
|
||||
#: How each origin reads in the picker label. ``fallback`` is still the operator's own bundles:
|
||||
#: which directory the writable root resolved to is a tooltip fact, not a picker distinction.
|
||||
ORIGIN_LABELS: dict[str, str] = {
|
||||
ORIGIN_SHIPPED: "Shipped",
|
||||
ORIGIN_USER: "User",
|
||||
ORIGIN_FALLBACK: "User",
|
||||
}
|
||||
|
||||
#: How the SDK read a bundle. Stable identifiers, so a surface that has to translate the fact
|
||||
#: (or branch on it) never matches on a sentence.
|
||||
SHAPE_DESCRIPTOR = "descriptor"
|
||||
SHAPE_COMPATIBILITY = "compatibility"
|
||||
SHAPE_UNREADABLE = "unreadable"
|
||||
|
||||
_SHAPE_NOTES: dict[str, str] = {
|
||||
SHAPE_DESCRIPTOR: "descriptor bundle",
|
||||
SHAPE_COMPATIBILITY: "compatibility bundle, no descriptor",
|
||||
SHAPE_UNREADABLE: "no readable bundle",
|
||||
}
|
||||
|
||||
#: What the SDK's verdict *means*, said rather than named. The SDK's own state word is kept
|
||||
#: beside it because the tooltip is where the technical facts have to survive; on its own,
|
||||
#: "degraded" reads as a defect report about a healthy bundle.
|
||||
_STATE_NOTES: dict[str, str] = {
|
||||
"ok": "processing can start",
|
||||
"degraded": "processing can start; something optional is missing",
|
||||
"failed": "processing cannot start",
|
||||
STATE_UNREADABLE: "no bundle could be read here",
|
||||
}
|
||||
|
||||
#: The SDK's own issue codes whose operator-facing meaning is a capability, not the finding.
|
||||
#: A code that is not listed falls back to the SDK's message, so a renamed code cannot silently
|
||||
#: drop a warning.
|
||||
_CAPABILITY_NOTES: dict[str, str] = {
|
||||
"calibration.absent": (
|
||||
"no calibration selected — calibrated force is unavailable; raw force is available"
|
||||
),
|
||||
}
|
||||
|
||||
_WRITE_PROBE_PREFIX = ".orisys-write-probe"
|
||||
|
||||
|
||||
@@ -89,6 +135,18 @@ def _sort_key(path: Path) -> str:
|
||||
return path.name.lower()
|
||||
|
||||
|
||||
def _sentence(issue: Issue) -> str:
|
||||
"""One SDK finding as a sentence an operator can read.
|
||||
|
||||
The code goes: ``calibration.applicability-unknown`` is a name, and the message beside it is
|
||||
already the sentence. A warning whose meaning is a capability is replaced by that capability
|
||||
instead of the finding, so the operator reads what the bundle will and will not give. The
|
||||
file it is about stays — that is a fact, not a code.
|
||||
"""
|
||||
text = _CAPABILITY_NOTES.get(issue.code, issue.message)
|
||||
return f"{text} [{issue.path}]" if issue.path is not None else text
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UserRootDecision:
|
||||
"""Where the writable user bundles live, and why."""
|
||||
@@ -139,8 +197,11 @@ class CatalogEntry:
|
||||
valid: bool
|
||||
state: str
|
||||
reason: str = ""
|
||||
errors: tuple[str, ...] = ()
|
||||
warnings: tuple[str, ...] = ()
|
||||
#: The SDK's own findings, kept as issues rather than as rendered strings: the tooltip owes
|
||||
#: the operator a sentence per warning, and a code whose meaning is a capability has to be
|
||||
#: recognisable to be replaced by it.
|
||||
errors: tuple[Issue, ...] = ()
|
||||
warnings: tuple[Issue, ...] = ()
|
||||
diagnostics: tuple[str, ...] = ()
|
||||
#: ``origin/folder`` of every other bundle declaring this entry's ``profile_id``.
|
||||
duplicates: tuple[str, ...] = ()
|
||||
@@ -156,6 +217,60 @@ class CatalogEntry:
|
||||
"""The family group this entry is listed under; never empty."""
|
||||
return self.product_family or FAMILY_UNIDENTIFIED
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""What the operator calls this configuration: its name, family, or folder."""
|
||||
return self.display_name or self.product_family or self.folder
|
||||
|
||||
@property
|
||||
def origin_label(self) -> str:
|
||||
"""``Shipped`` or ``User``: where this configuration came from."""
|
||||
return ORIGIN_LABELS.get(self.origin, self.origin)
|
||||
|
||||
@property
|
||||
def shape(self) -> str:
|
||||
"""How the SDK read this bundle: :data:`SHAPE_DESCRIPTOR` and friends.
|
||||
|
||||
``backend_source`` is the SDK's own report of where the processing backend came from,
|
||||
and only a parsed ``manifest.json`` descriptor reports ``manifest`` — so this is the
|
||||
SDK's answer, not the catalog reading the folder a second time.
|
||||
"""
|
||||
profile = self.profile
|
||||
if profile is None:
|
||||
return SHAPE_UNREADABLE
|
||||
if profile.backend_source == "manifest":
|
||||
return SHAPE_DESCRIPTOR
|
||||
return SHAPE_COMPATIBILITY
|
||||
|
||||
@property
|
||||
def shape_note(self) -> str:
|
||||
"""The bundle's shape as the sentence the tooltip shows."""
|
||||
return _SHAPE_NOTES.get(self.shape, _SHAPE_NOTES[SHAPE_UNREADABLE])
|
||||
|
||||
@property
|
||||
def selected_calibration(self) -> str:
|
||||
"""The calibration the SDK resolved for this bundle, by file name; ``""`` for none.
|
||||
|
||||
Read from the resolution rather than from the directory: a calibration file merely
|
||||
existing proves nothing, and the SDK never substitutes a different one.
|
||||
"""
|
||||
profile = self.profile
|
||||
path = None if profile is None else profile.calibration.path
|
||||
return path.name if path is not None else ""
|
||||
|
||||
@property
|
||||
def calibration_note(self) -> str:
|
||||
"""What the calibration *selection* gives: a named file, or raw force.
|
||||
|
||||
A calibration is a separate optional artifact chosen for a session, never a property of
|
||||
the bundle, so an absent one is stated as what is not selected — not as a bundle that is
|
||||
uncalibrated, and never as a defect.
|
||||
"""
|
||||
selected = self.selected_calibration
|
||||
if selected:
|
||||
return f"Calibration: {selected}"
|
||||
return "No calibration selected — raw force"
|
||||
|
||||
@property
|
||||
def duplicate(self) -> bool:
|
||||
return bool(self.duplicates)
|
||||
@@ -178,18 +293,39 @@ class CatalogEntry:
|
||||
return None
|
||||
|
||||
def label(self) -> str:
|
||||
"""One line for a picker item: family, profile, origin and validity."""
|
||||
return f"{self.family} · {self.profile_id or self.folder} · {self.origin} · {self.state}"
|
||||
"""One line for a picker item: what it is, where it came from, what it will give.
|
||||
|
||||
Deliberately absent: the ``profile_id`` (identity no operator can act on), the SDK's
|
||||
``ok``/``degraded``/``failed`` verdict (a severity with no information in it) and the
|
||||
word ``legacy`` (which means "read through the compatibility adapter", not "old").
|
||||
What is left is the choice itself: the product, its origin, and the force a session
|
||||
started from it will produce.
|
||||
"""
|
||||
name = self.name
|
||||
if not self.valid:
|
||||
return f"{name} · Cannot start: {self.start_blocked_reason}"
|
||||
origin = self.origin_label
|
||||
if self.duplicates:
|
||||
return f"{name} · {origin} · Also provided by {', '.join(self.duplicates)}"
|
||||
return f"{name} · {origin} · {self.calibration_note}"
|
||||
|
||||
def tooltip(self) -> str:
|
||||
"""The facts behind the label, including why the entry is not startable."""
|
||||
"""The facts behind the label, including why the entry is not startable.
|
||||
|
||||
The technical half the picker row has no room for: the identity the SDK assigned, how
|
||||
the bundle was read, its validation, and every finding as a sentence rather than as a
|
||||
code — ``calibration.absent`` says nothing an operator can use, "calibrated force is
|
||||
unavailable; raw force is available" says everything.
|
||||
"""
|
||||
state_note = _STATE_NOTES.get(self.state, "verdict not recognised")
|
||||
lines = [
|
||||
f"{self.display_name} ({self.ref})",
|
||||
f"{self.name} ({self.ref}) — {self.shape_note}",
|
||||
f"profile_id: {self.profile_id or 'unknown'}",
|
||||
f"product family: {self.product_family or 'unknown'}",
|
||||
f"processing backend: {self.processing_backend or 'unknown'}",
|
||||
f"origin: {self.origin} ({self.bundle_path})",
|
||||
f"state: {self.state}",
|
||||
f"SDK validation: {self.state} ({state_note})",
|
||||
f"calibration: {self.selected_calibration or 'none selected — raw force'}",
|
||||
]
|
||||
blocked = self.start_blocked_reason
|
||||
if blocked:
|
||||
@@ -197,9 +333,11 @@ class CatalogEntry:
|
||||
# The reason already quotes the errors it was built from; repeating them here would
|
||||
# bury the one line that explains the entry.
|
||||
lines.extend(
|
||||
f"error: {text}" for text in self.errors if text not in self.reason
|
||||
f"error: {_sentence(issue)}"
|
||||
for issue in self.errors
|
||||
if str(issue) not in self.reason
|
||||
)
|
||||
lines.extend(f"warning: {text}" for text in self.warnings)
|
||||
lines.extend(f"warning: {_sentence(issue)}" for issue in self.warnings)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -436,7 +574,10 @@ def _resolved_entry(
|
||||
) -> CatalogEntry:
|
||||
"""An entry the SDK resolved, valid or rejected, with its verdict kept whole."""
|
||||
validation = profile.validation
|
||||
errors = tuple(str(issue) for issue in validation.errors)
|
||||
# The issues are kept as the SDK objects, not stringified here: the tooltip owes a sentence
|
||||
# per warning, and the one place the code belongs is the SDK's own reason, which is what
|
||||
# "Cannot start" quotes.
|
||||
errors = tuple(validation.errors)
|
||||
return CatalogEntry(
|
||||
key=str(profile.sensor_json_path),
|
||||
folder=folder.name,
|
||||
@@ -449,9 +590,9 @@ def _resolved_entry(
|
||||
processing_backend=profile.processing_backend,
|
||||
valid=validation.usable,
|
||||
state=validation.state,
|
||||
reason="; ".join(errors),
|
||||
reason="; ".join(str(issue) for issue in errors),
|
||||
errors=errors,
|
||||
warnings=tuple(str(issue) for issue in validation.warnings),
|
||||
warnings=tuple(validation.warnings),
|
||||
diagnostics=tuple(profile.diagnostics),
|
||||
profile=profile,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,8 @@ descriptor bundle, a legacy ``sensor.json`` bundle, or an entry that stays liste
|
||||
with the SDK's own reason.
|
||||
|
||||
The two functions below stay the picker's compatibility surface. The picker itself uses the
|
||||
catalog object directly, so it can show each entry's family, profile, origin and validity.
|
||||
catalog object directly, so it can show each entry's name, origin and calibration selection,
|
||||
with the identity and the SDK's verdict in the tooltip.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
Reference in New Issue
Block a user