feat(device-plugins): add profiled K1 lifecycle and canonical data plane

This commit is contained in:
DCCONSTRUCTIONS
2026-07-16 19:44:06 +03:00
parent 19ab973110
commit e6f7648b84
45 changed files with 6401 additions and 440 deletions
+26
View File
@@ -0,0 +1,26 @@
"""Decoded in-process projections consumed by Mission Core visualizers.
These views are not wire contracts. Portable plugin/host envelopes live in the
versioned Plugin SDK; an extraction boundary can hydrate those envelopes into
these allocation-conscious representations for local consumers.
"""
from k1link.data_plane.views import (
ConsumerFrameContext,
DecodedDataPlaneView,
DecodedDeviceStatusView,
DecodedPointCloudView,
DecodedPoseView,
NormalizationError,
StatusAttribute,
)
__all__ = [
"ConsumerFrameContext",
"DecodedDataPlaneView",
"DecodedDeviceStatusView",
"DecodedPointCloudView",
"DecodedPoseView",
"NormalizationError",
"StatusAttribute",
]
+128
View File
@@ -0,0 +1,128 @@
from __future__ import annotations
import math
from dataclasses import dataclass
class NormalizationError(ValueError):
"""A transport message matched a known channel but could not be normalized."""
@dataclass(frozen=True, slots=True)
class ConsumerFrameContext:
"""Transport-neutral provenance shared by decoded consumer views.
Raw channel names and payloads deliberately do not cross this boundary. The
byte count is retained for operational metrics, while opaque device/session
aliases are optional because older streams do not carry them. They are
deliberately named as source aliases: a vendor header must never become a
Mission Core device identity merely by crossing the decode boundary.
"""
sequence: int
captured_at_epoch_ns: int
received_monotonic_ns: int | None
processing_started_monotonic_ns: int
encoded_size_bytes: int
live: bool
source_device_alias: str | None = None
source_session_alias: str | None = None
def __post_init__(self) -> None:
if self.sequence < 1:
raise ValueError("frame sequence must be positive")
if self.captured_at_epoch_ns < 0:
raise ValueError("capture time must be non-negative")
if self.received_monotonic_ns is not None and self.received_monotonic_ns < 0:
raise ValueError("receive monotonic time must be non-negative")
if self.processing_started_monotonic_ns < 0:
raise ValueError("processing start time must be non-negative")
if self.encoded_size_bytes < 0:
raise ValueError("encoded size must be non-negative")
@dataclass(frozen=True, slots=True)
class DecodedPointCloudView:
"""In-process point-cloud projection in a named Cartesian frame.
This is a consumer view, not the portable SDK ``PointCloudFrame`` contract.
"""
context: ConsumerFrameContext
frame_id: str
positions_xyz: tuple[tuple[float, float, float], ...]
intensities: bytes | None = None
colors_rgb: bytes | None = None
def __post_init__(self) -> None:
if not self.frame_id:
raise ValueError("point-cloud frame_id must not be empty")
point_count = len(self.positions_xyz)
if self.intensities is not None and len(self.intensities) != point_count:
raise ValueError("intensity count must equal point count")
if self.colors_rgb is not None and len(self.colors_rgb) != point_count * 3:
raise ValueError("RGB byte count must equal point count * 3")
if not all(
len(position) == 3 and all(math.isfinite(value) for value in position)
for position in self.positions_xyz
):
raise ValueError("point positions must contain finite xyz triples")
@property
def point_count(self) -> int:
return len(self.positions_xyz)
@dataclass(frozen=True, slots=True)
class DecodedPoseView:
"""In-process pose projection in a named Cartesian coordinate frame."""
context: ConsumerFrameContext
frame_id: str
child_frame_id: str
position_xyz: tuple[float, float, float]
orientation_xyzw: tuple[float, float, float, float]
def __post_init__(self) -> None:
if not self.frame_id or not self.child_frame_id:
raise ValueError("pose frame identifiers must not be empty")
values = (*self.position_xyz, *self.orientation_xyzw)
if not all(math.isfinite(value) for value in values):
raise ValueError("pose values must be finite")
@dataclass(frozen=True, slots=True)
class StatusAttribute:
"""One stable, normalized status attribute."""
name: str
value: bool | int | float | str
def __post_init__(self) -> None:
if not self.name:
raise ValueError("status attribute name must not be empty")
if isinstance(self.value, float) and not math.isfinite(self.value):
raise ValueError("status attribute float must be finite")
@dataclass(frozen=True, slots=True)
class DecodedDeviceStatusView:
"""An in-process device-status projection.
The current verified K1 profile does not yet decode its status topic. The
view exists so future verified status codecs do not alter consumers.
"""
context: ConsumerFrameContext
state: str
attributes: tuple[StatusAttribute, ...] = ()
def __post_init__(self) -> None:
if not self.state:
raise ValueError("device status state must not be empty")
names = [attribute.name for attribute in self.attributes]
if len(names) != len(set(names)):
raise ValueError("device status attribute names must be unique")
DecodedDataPlaneView = DecodedPointCloudView | DecodedPoseView | DecodedDeviceStatusView