feat(device-plugins): add profiled K1 lifecycle and canonical data plane
This commit is contained in:
@@ -1,17 +1,24 @@
|
||||
{
|
||||
"apiVersion": "missioncore.nodedc/v1alpha1",
|
||||
"apiVersion": "missioncore.nodedc/v1alpha2",
|
||||
"kind": "DevicePlugin",
|
||||
"metadata": {
|
||||
"id": "nodedc.device.xgrids-lixelkity-k1",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"displayName": "XGRIDS K1 Integration"
|
||||
},
|
||||
"spec": {
|
||||
"hostApiRange": "v1alpha1",
|
||||
"hostApiRange": "v1alpha2",
|
||||
"runtime": {
|
||||
"backendEntrypoint": "k1link.web.xgrids_k1_facade:build_xgrids_k1_plugin",
|
||||
"isolation": "transitional-in-process"
|
||||
},
|
||||
"compatibilityProfiles": [
|
||||
{
|
||||
"profileId": "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
|
||||
"path": "profiles/fw-3.0.2/direct-lan.v1.json",
|
||||
"modelId": "xgrids.lixelkity-k1"
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"device.discovery.ble",
|
||||
"device.provisioning.wifi-over-ble",
|
||||
@@ -21,7 +28,16 @@
|
||||
"actions": [
|
||||
{ "id": "state.read", "mutating": false, "secretFields": [] },
|
||||
{ "id": "discovery.scan", "mutating": false, "secretFields": [] },
|
||||
{ "id": "device.inspect", "mutating": false, "secretFields": [] },
|
||||
{ "id": "sensor.catalog.read", "mutating": false, "secretFields": [] },
|
||||
{ "id": "calibration.device-snapshot.read", "mutating": false, "secretFields": [] },
|
||||
{ "id": "network.provision", "mutating": true, "secretFields": ["password"] },
|
||||
{ "id": "connection.verify", "mutating": false, "secretFields": [] },
|
||||
{ "id": "acquisition.prepare", "mutating": true, "secretFields": [] },
|
||||
{ "id": "acquisition.start", "mutating": true, "secretFields": [] },
|
||||
{ "id": "acquisition.stop", "mutating": true, "secretFields": [] },
|
||||
{ "id": "acquisition.abort", "mutating": true, "secretFields": [] },
|
||||
{ "id": "acquisition.state.read", "mutating": false, "secretFields": [] },
|
||||
{ "id": "stream.start-live", "mutating": true, "secretFields": [] },
|
||||
{ "id": "stream.start-replay", "mutating": true, "secretFields": [] },
|
||||
{ "id": "stream.stop", "mutating": true, "secretFields": [] },
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
"""Strict loader for the local XGRIDS K1 compatibility profile.
|
||||
|
||||
The profile is descriptive and fail-closed. Loading it never performs device
|
||||
I/O and never grants write authority to a transport implementation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
PROFILE_SCHEMA_VERSION = 1
|
||||
DEFAULT_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
|
||||
DEFAULT_PROFILE_PATH = Path(__file__).parent / "profiles" / "fw-3.0.2" / "direct-lan.v1.json"
|
||||
EVIDENCE_FLAGS = (
|
||||
"observed",
|
||||
"decoded",
|
||||
"replay_verified",
|
||||
"physical_verified",
|
||||
"write_enabled",
|
||||
)
|
||||
|
||||
|
||||
class CompatibilityProfileError(ValueError):
|
||||
"""The compatibility profile is malformed or exceeds its reviewed scope."""
|
||||
|
||||
|
||||
def _object(value: Any, path: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise CompatibilityProfileError(f"{path} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _array(value: Any, path: str) -> list[Any]:
|
||||
if not isinstance(value, list):
|
||||
raise CompatibilityProfileError(f"{path} must be an array")
|
||||
return value
|
||||
|
||||
|
||||
def _string(value: Any, path: str) -> str:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise CompatibilityProfileError(f"{path} must be a non-empty string")
|
||||
return value
|
||||
|
||||
|
||||
def _unique_index(items: list[Any], path: str) -> dict[str, dict[str, Any]]:
|
||||
index: dict[str, dict[str, Any]] = {}
|
||||
for position, value in enumerate(items):
|
||||
item_path = f"{path}[{position}]"
|
||||
item = _object(value, item_path)
|
||||
item_id = _string(item.get("id"), f"{item_path}.id")
|
||||
if item_id in index:
|
||||
raise CompatibilityProfileError(f"{path} contains duplicate id {item_id!r}")
|
||||
index[item_id] = item
|
||||
return index
|
||||
|
||||
|
||||
def _validate_evidence(value: Any, path: str) -> dict[str, bool]:
|
||||
evidence = _object(value, path)
|
||||
if set(evidence) != set(EVIDENCE_FLAGS):
|
||||
expected = ", ".join(EVIDENCE_FLAGS)
|
||||
raise CompatibilityProfileError(f"{path} must contain exactly: {expected}")
|
||||
for flag in EVIDENCE_FLAGS:
|
||||
if not isinstance(evidence[flag], bool):
|
||||
raise CompatibilityProfileError(f"{path}.{flag} must be a boolean")
|
||||
if evidence["write_enabled"]:
|
||||
raise CompatibilityProfileError(f"{path}.write_enabled must remain false in v1")
|
||||
return evidence # type: ignore[return-value]
|
||||
|
||||
|
||||
def _walk_and_validate_evidence(value: Any, path: str = "$") -> None:
|
||||
if isinstance(value, dict):
|
||||
if "evidence" in value:
|
||||
_validate_evidence(value["evidence"], f"{path}.evidence")
|
||||
for key, child in value.items():
|
||||
child_path = f"{path}.{key}"
|
||||
if key == "write_enabled" and path != "$.evidence_vocabulary":
|
||||
if not isinstance(child, bool):
|
||||
raise CompatibilityProfileError(f"{child_path} must be a boolean")
|
||||
if child:
|
||||
raise CompatibilityProfileError(f"{child_path} must remain false in v1")
|
||||
_walk_and_validate_evidence(child, child_path)
|
||||
elif isinstance(value, list):
|
||||
for position, child in enumerate(value):
|
||||
_walk_and_validate_evidence(child, f"{path}[{position}]")
|
||||
|
||||
|
||||
def _expect_evidence(
|
||||
item: dict[str, Any],
|
||||
path: str,
|
||||
*,
|
||||
observed: bool,
|
||||
decoded: bool,
|
||||
replay_verified: bool,
|
||||
physical_verified: bool,
|
||||
) -> None:
|
||||
actual = _validate_evidence(item.get("evidence"), f"{path}.evidence")
|
||||
expected = {
|
||||
"observed": observed,
|
||||
"decoded": decoded,
|
||||
"replay_verified": replay_verified,
|
||||
"physical_verified": physical_verified,
|
||||
"write_enabled": False,
|
||||
}
|
||||
if actual != expected:
|
||||
raise CompatibilityProfileError(
|
||||
f"{path}.evidence exceeds or contradicts the reviewed v1 evidence"
|
||||
)
|
||||
|
||||
|
||||
def _validate_sources(profile: dict[str, Any]) -> set[str]:
|
||||
sources = _unique_index(
|
||||
_array(profile.get("evidence_sources"), "$.evidence_sources"),
|
||||
"$.evidence_sources",
|
||||
)
|
||||
for source_id, source in sources.items():
|
||||
_string(source.get("kind"), f"$.evidence_sources[{source_id!r}].kind")
|
||||
path = _string(source.get("path"), f"$.evidence_sources[{source_id!r}].path")
|
||||
if path.startswith("/") or ".." in Path(path).parts:
|
||||
raise CompatibilityProfileError(
|
||||
f"$.evidence_sources[{source_id!r}].path must be repository-relative"
|
||||
)
|
||||
_string(source.get("scope"), f"$.evidence_sources[{source_id!r}].scope")
|
||||
return set(sources)
|
||||
|
||||
|
||||
def _validate_source_references(value: Any, source_ids: set[str], path: str = "$") -> None:
|
||||
if isinstance(value, dict):
|
||||
if "source_ids" in value:
|
||||
references = _array(value["source_ids"], f"{path}.source_ids")
|
||||
if len(references) != len(set(references)):
|
||||
raise CompatibilityProfileError(f"{path}.source_ids contains duplicates")
|
||||
for position, source_id in enumerate(references):
|
||||
source_id = _string(source_id, f"{path}.source_ids[{position}]")
|
||||
if source_id not in source_ids:
|
||||
raise CompatibilityProfileError(
|
||||
f"{path}.source_ids[{position}] references unknown evidence source"
|
||||
)
|
||||
for key, child in value.items():
|
||||
_validate_source_references(child, source_ids, f"{path}.{key}")
|
||||
elif isinstance(value, list):
|
||||
for position, child in enumerate(value):
|
||||
_validate_source_references(child, source_ids, f"{path}[{position}]")
|
||||
|
||||
|
||||
def _validate_transports(profile: dict[str, Any]) -> None:
|
||||
transports = _unique_index(
|
||||
_array(profile.get("transports"), "$.transports"),
|
||||
"$.transports",
|
||||
)
|
||||
if set(transports) != {
|
||||
"ble.wifi-bootstrap.fw3.v1",
|
||||
"mqtt.direct-lan.fw3.v1",
|
||||
"rtsp.camera-preview.fw3.v1",
|
||||
}:
|
||||
raise CompatibilityProfileError("$.transports must contain only the reviewed v1 transports")
|
||||
|
||||
ble = transports["ble.wifi-bootstrap.fw3.v1"]
|
||||
if ble.get("service_uuid") != "00007f00-0000-1000-8000-00805f9b34fb":
|
||||
raise CompatibilityProfileError("BLE service UUID differs from reviewed evidence")
|
||||
characteristics = _object(ble.get("characteristics"), "$.transports[ble].characteristics")
|
||||
if characteristics != {
|
||||
"wifi_request": "00007f01-0000-1000-8000-00805f9b34fb",
|
||||
"wifi_status": "00007f02-0000-1000-8000-00805f9b34fb",
|
||||
}:
|
||||
raise CompatibilityProfileError("BLE characteristic UUIDs differ from reviewed evidence")
|
||||
if ble.get("request_frame_bytes") != 99:
|
||||
raise CompatibilityProfileError("BLE provisioning frame must remain exactly 99 bytes")
|
||||
_expect_evidence(
|
||||
ble,
|
||||
"$.transports[ble.wifi-bootstrap.fw3.v1]",
|
||||
observed=True,
|
||||
decoded=True,
|
||||
replay_verified=False,
|
||||
physical_verified=True,
|
||||
)
|
||||
|
||||
mqtt = transports["mqtt.direct-lan.fw3.v1"]
|
||||
if mqtt.get("protocol") != "MQTT 3.1.1":
|
||||
raise CompatibilityProfileError("direct-LAN application protocol must remain MQTT 3.1.1")
|
||||
network = _object(mqtt.get("network"), "$.transports[mqtt].network")
|
||||
if network.get("transport") != "TCP" or network.get("port") != 1883:
|
||||
raise CompatibilityProfileError("direct-LAN MQTT endpoint must remain TCP 1883")
|
||||
if network.get("tls") is not False or network.get("authentication") != "none-observed":
|
||||
raise CompatibilityProfileError("direct-LAN MQTT security claim differs from observation")
|
||||
allowlist = _array(
|
||||
mqtt.get("subscription_allowlist"),
|
||||
"$.transports[mqtt].subscription_allowlist",
|
||||
)
|
||||
if "lixel/application/report/#" not in allowlist:
|
||||
raise CompatibilityProfileError("MQTT report-topic allowlist is missing")
|
||||
for topic in allowlist:
|
||||
topic = _string(topic, "$.transports[mqtt].subscription_allowlist[]")
|
||||
if "/request/" in topic:
|
||||
raise CompatibilityProfileError(
|
||||
"request topics cannot enter the subscribe-only profile"
|
||||
)
|
||||
_expect_evidence(
|
||||
mqtt,
|
||||
"$.transports[mqtt.direct-lan.fw3.v1]",
|
||||
observed=True,
|
||||
decoded=False,
|
||||
replay_verified=False,
|
||||
physical_verified=True,
|
||||
)
|
||||
|
||||
rtsp = transports["rtsp.camera-preview.fw3.v1"]
|
||||
if rtsp.get("protocol") != "RTSP 1.0 with interleaved RTP over TCP":
|
||||
raise CompatibilityProfileError("camera-preview protocol differs from observation")
|
||||
rtsp_network = _object(rtsp.get("network"), "$.transports[rtsp].network")
|
||||
if rtsp_network.get("transport") != "TCP" or rtsp_network.get("port") != 8554:
|
||||
raise CompatibilityProfileError("camera-preview endpoint must remain TCP 8554")
|
||||
if (
|
||||
rtsp_network.get("tls") is not False
|
||||
or rtsp_network.get("authentication") != "none-observed"
|
||||
):
|
||||
raise CompatibilityProfileError("camera-preview security differs from observation")
|
||||
media = _object(rtsp.get("media"), "$.transports[rtsp].media")
|
||||
if media != {
|
||||
"codec": "H.264",
|
||||
"rtp_payload_type": 96,
|
||||
"clock_hz": 90000,
|
||||
"framing": "RTP/AVP/TCP interleaved channels 0-1",
|
||||
}:
|
||||
raise CompatibilityProfileError("camera-preview media contract differs from observation")
|
||||
_expect_evidence(
|
||||
rtsp,
|
||||
"$.transports[rtsp.camera-preview.fw3.v1]",
|
||||
observed=True,
|
||||
decoded=False,
|
||||
replay_verified=False,
|
||||
physical_verified=True,
|
||||
)
|
||||
|
||||
|
||||
def _validate_channels(profile: dict[str, Any]) -> None:
|
||||
channels = _unique_index(_array(profile.get("channels"), "$.channels"), "$.channels")
|
||||
expected_ids = {
|
||||
"spatial.point-cloud.live",
|
||||
"spatial.pose.live",
|
||||
"device.status.live",
|
||||
"device.heartbeat.live",
|
||||
"camera.preview.live",
|
||||
}
|
||||
if set(channels) != expected_ids:
|
||||
raise CompatibilityProfileError("$.channels differs from the reviewed v1 channel set")
|
||||
|
||||
point = channels["spatial.point-cloud.live"]
|
||||
if point.get("topic") != "lixel/application/report/lio_pcl":
|
||||
raise CompatibilityProfileError("point-cloud topic differs from reviewed evidence")
|
||||
_expect_evidence(
|
||||
point,
|
||||
"$.channels[spatial.point-cloud.live]",
|
||||
observed=True,
|
||||
decoded=True,
|
||||
replay_verified=True,
|
||||
physical_verified=True,
|
||||
)
|
||||
|
||||
pose = channels["spatial.pose.live"]
|
||||
if pose.get("topic") != "lixel/application/report/lio_pose":
|
||||
raise CompatibilityProfileError("pose topic differs from reviewed evidence")
|
||||
_expect_evidence(
|
||||
pose,
|
||||
"$.channels[spatial.pose.live]",
|
||||
observed=True,
|
||||
decoded=True,
|
||||
replay_verified=True,
|
||||
physical_verified=True,
|
||||
)
|
||||
|
||||
for channel_id, topic in (
|
||||
("device.status.live", "lixel/application/report/device_status"),
|
||||
("device.heartbeat.live", "lixel/application/report/heartbeat"),
|
||||
):
|
||||
channel = channels[channel_id]
|
||||
if channel.get("topic") != topic or channel.get("semantic_payload") is not None:
|
||||
raise CompatibilityProfileError(f"{channel_id} must remain raw-only in v1")
|
||||
_expect_evidence(
|
||||
channel,
|
||||
f"$.channels[{channel_id}]",
|
||||
observed=True,
|
||||
decoded=False,
|
||||
replay_verified=False,
|
||||
physical_verified=True,
|
||||
)
|
||||
|
||||
camera = channels["camera.preview.live"]
|
||||
if camera.get("discovery_status") != "observed":
|
||||
raise CompatibilityProfileError("camera discovery must match the owner-controlled run")
|
||||
if camera.get("topic") is not None:
|
||||
raise CompatibilityProfileError("camera preview is not an MQTT topic")
|
||||
expected_endpoints = {
|
||||
"rtsp://{confirmed-device-private-ipv4}:8554/live/chn_left_main",
|
||||
"rtsp://{confirmed-device-private-ipv4}:8554/live/chn_right_main",
|
||||
}
|
||||
endpoints = _array(camera.get("endpoint_templates"), "$.channels[camera].endpoint_templates")
|
||||
if set(endpoints) != expected_endpoints or len(endpoints) != len(expected_endpoints):
|
||||
raise CompatibilityProfileError("camera endpoint templates differ from observation")
|
||||
if camera.get("wire_format") != "RTSP 1.0, interleaved RTP/TCP, H.264 PT96 at 90000 Hz":
|
||||
raise CompatibilityProfileError("camera wire format differs from observation")
|
||||
_string(camera.get("semantic_payload"), "$.channels[camera].semantic_payload")
|
||||
_expect_evidence(
|
||||
camera,
|
||||
"$.channels[camera.preview.live]",
|
||||
observed=True,
|
||||
decoded=False,
|
||||
replay_verified=False,
|
||||
physical_verified=True,
|
||||
)
|
||||
|
||||
|
||||
def _validate_acquisition_control(profile: dict[str, Any]) -> None:
|
||||
control = _object(profile.get("acquisition_control"), "$.acquisition_control")
|
||||
if control.get("mode") != "operator-manual" or control.get("write_enabled") is not False:
|
||||
raise CompatibilityProfileError("acquisition control must remain operator-manual")
|
||||
|
||||
device_control = _object(
|
||||
control.get("verified_device_control"),
|
||||
"$.acquisition_control.verified_device_control",
|
||||
)
|
||||
if device_control.get("gesture") != "physical-double-click":
|
||||
raise CompatibilityProfileError("physical acquisition gesture differs from lab evidence")
|
||||
_expect_evidence(
|
||||
device_control,
|
||||
"$.acquisition_control.verified_device_control",
|
||||
observed=True,
|
||||
decoded=False,
|
||||
replay_verified=False,
|
||||
physical_verified=True,
|
||||
)
|
||||
|
||||
actions = _unique_index(
|
||||
_array(control.get("semantic_actions"), "$.acquisition_control.semantic_actions"),
|
||||
"$.acquisition_control.semantic_actions",
|
||||
)
|
||||
if set(actions) != {
|
||||
"acquisition.start",
|
||||
"acquisition.stop",
|
||||
"calibration.device.start",
|
||||
}:
|
||||
raise CompatibilityProfileError("semantic action set differs from reviewed v1")
|
||||
|
||||
for action_id, action_value in (("acquisition.start", 1), ("acquisition.stop", 2)):
|
||||
action = actions[action_id]
|
||||
if action.get("execution") != "operator-manual":
|
||||
raise CompatibilityProfileError(f"{action_id} must remain operator-manual")
|
||||
mapping = _object(
|
||||
action.get("vendor_request_mapping"),
|
||||
f"$.acquisition_control.semantic_actions[{action_id}].vendor_request_mapping",
|
||||
)
|
||||
if mapping.get("evidence_kind") != "owner-controlled-wire-observation":
|
||||
raise CompatibilityProfileError(f"{action_id} vendor mapping differs from evidence")
|
||||
if mapping.get("topic") != "lixel/application/request/modeling":
|
||||
raise CompatibilityProfileError(
|
||||
f"{action_id} vendor topic differs from static evidence"
|
||||
)
|
||||
if mapping.get("qos") != 2 or mapping.get("action_field_value") != action_value:
|
||||
raise CompatibilityProfileError(
|
||||
f"{action_id} vendor mapping differs from static evidence"
|
||||
)
|
||||
if not _array(
|
||||
mapping.get("required_unresolved_context"),
|
||||
f"$.acquisition_control.semantic_actions[{action_id}].required_unresolved_context",
|
||||
):
|
||||
raise CompatibilityProfileError(f"{action_id} must declare unresolved request context")
|
||||
_expect_evidence(
|
||||
mapping,
|
||||
f"$.acquisition_control.semantic_actions[{action_id}].vendor_request_mapping",
|
||||
observed=True,
|
||||
decoded=True,
|
||||
replay_verified=False,
|
||||
physical_verified=True,
|
||||
)
|
||||
_expect_evidence(
|
||||
action,
|
||||
f"$.acquisition_control.semantic_actions[{action_id}]",
|
||||
observed=True,
|
||||
decoded=True,
|
||||
replay_verified=False,
|
||||
physical_verified=True,
|
||||
)
|
||||
|
||||
calibration = actions["calibration.device.start"]
|
||||
if calibration.get("execution") != "unavailable":
|
||||
raise CompatibilityProfileError("calibration must remain unavailable in v1")
|
||||
if calibration.get("vendor_request_mapping") is not None:
|
||||
raise CompatibilityProfileError("calibration vendor request is not evidenced")
|
||||
_expect_evidence(
|
||||
calibration,
|
||||
"$.acquisition_control.semantic_actions[calibration.device.start]",
|
||||
observed=False,
|
||||
decoded=False,
|
||||
replay_verified=False,
|
||||
physical_verified=False,
|
||||
)
|
||||
|
||||
|
||||
def validate_compatibility_profile(profile: Any) -> dict[str, Any]:
|
||||
"""Validate and return one read-only firmware-3/direct-LAN profile object."""
|
||||
root = _object(profile, "$")
|
||||
if root.get("schema_version") != PROFILE_SCHEMA_VERSION:
|
||||
raise CompatibilityProfileError("unsupported compatibility profile schema_version")
|
||||
if root.get("profile_id") != DEFAULT_PROFILE_ID:
|
||||
raise CompatibilityProfileError("unexpected compatibility profile_id")
|
||||
|
||||
scope = _object(root.get("scope"), "$.scope")
|
||||
firmware = _object(scope.get("firmware"), "$.scope.firmware")
|
||||
if firmware != {"match": "exact", "version": "3.0.2"}:
|
||||
raise CompatibilityProfileError("profile must match firmware 3.0.2 exactly")
|
||||
if scope.get("topology") != "direct-lan":
|
||||
raise CompatibilityProfileError("profile topology must remain direct-lan")
|
||||
|
||||
vocabulary = _object(root.get("evidence_vocabulary"), "$.evidence_vocabulary")
|
||||
if set(vocabulary) != set(EVIDENCE_FLAGS):
|
||||
raise CompatibilityProfileError("evidence vocabulary differs from schema v1")
|
||||
for flag in EVIDENCE_FLAGS:
|
||||
_string(vocabulary[flag], f"$.evidence_vocabulary.{flag}")
|
||||
|
||||
safety = _object(root.get("safety"), "$.safety")
|
||||
if safety.get("default_mode") != "read-only":
|
||||
raise CompatibilityProfileError("profile default mode must remain read-only")
|
||||
if safety.get("vendor_writes_enabled") is not False:
|
||||
raise CompatibilityProfileError("profile must not enable vendor writes")
|
||||
if safety.get("unknown_firmware_policy") != "reject-profile":
|
||||
raise CompatibilityProfileError("unknown firmware must fail closed")
|
||||
if safety.get("request_topic_subscription_enabled") is not False:
|
||||
raise CompatibilityProfileError("request-topic subscription must remain disabled")
|
||||
|
||||
source_ids = _validate_sources(root)
|
||||
_validate_source_references(root, source_ids)
|
||||
_walk_and_validate_evidence(root)
|
||||
_validate_transports(root)
|
||||
_validate_channels(root)
|
||||
_validate_acquisition_control(root)
|
||||
return root
|
||||
|
||||
|
||||
def load_compatibility_profile(path: Path | str = DEFAULT_PROFILE_PATH) -> dict[str, Any]:
|
||||
"""Load a JSON profile, rejecting duplicate keys and unreviewed claims."""
|
||||
resolved = Path(path).expanduser().resolve()
|
||||
|
||||
def reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
for key, value in pairs:
|
||||
if key in result:
|
||||
raise CompatibilityProfileError(f"duplicate JSON key {key!r}")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
try:
|
||||
raw = resolved.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
raise CompatibilityProfileError(f"cannot read compatibility profile: {resolved}") from exc
|
||||
try:
|
||||
profile = json.loads(raw, object_pairs_hook=reject_duplicate_keys)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise CompatibilityProfileError(f"invalid compatibility profile JSON: {exc}") from exc
|
||||
return validate_compatibility_profile(profile)
|
||||
|
||||
|
||||
def matches_target(
|
||||
profile: dict[str, Any],
|
||||
*,
|
||||
firmware: str,
|
||||
topology: str,
|
||||
) -> bool:
|
||||
"""Return whether an already validated exact-match profile covers the target."""
|
||||
validated = validate_compatibility_profile(profile)
|
||||
scope = validated["scope"]
|
||||
return scope["firmware"]["version"] == firmware and scope["topology"] == topology
|
||||
|
||||
|
||||
def _main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Validate an XGRIDS K1 compatibility profile")
|
||||
parser.add_argument("path", nargs="?", type=Path, default=DEFAULT_PROFILE_PATH)
|
||||
args = parser.parse_args()
|
||||
profile = load_compatibility_profile(args.path)
|
||||
print(profile["profile_id"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(_main())
|
||||
@@ -0,0 +1,70 @@
|
||||
# XGRIDS K1 compatibility profiles
|
||||
|
||||
This directory is the plugin-local, versioned compatibility vocabulary for
|
||||
verified K1 wire behavior. It is deliberately **not** a platform ontology and
|
||||
does not add runtime dependencies on NODE.DC Ontology Core.
|
||||
|
||||
The first profile is
|
||||
[`fw-3.0.2/direct-lan.v1.json`](fw-3.0.2/direct-lan.v1.json). It matches exactly:
|
||||
|
||||
- model: XGRIDS LixelKity K1;
|
||||
- firmware: `3.0.2`;
|
||||
- topology: K1 and connector on the same owner-controlled LAN;
|
||||
- evidence scope: one physical scanner across controlled direct-LAN and
|
||||
owner-operated LixelGO/iPhone laboratory runs.
|
||||
|
||||
It must not be applied to another firmware or treated as a vendor API claim.
|
||||
Unknown firmware fails closed.
|
||||
|
||||
## Evidence flags
|
||||
|
||||
Every transport, data channel, and semantic action uses five independent
|
||||
boolean flags:
|
||||
|
||||
| Flag | Meaning |
|
||||
| --- | --- |
|
||||
| `observed` | Directly seen on the owner-controlled K1 |
|
||||
| `decoded` | A bounded semantic decoder exists for observed bytes |
|
||||
| `replay_verified` | Captured semantic data passed the replay-to-view path |
|
||||
| `physical_verified` | Correlated with a controlled physical state/action |
|
||||
| `write_enabled` | The profile grants emission of a state-changing request |
|
||||
|
||||
`decoded` does not mean that MQTT framing alone was parsed. Status and heartbeat
|
||||
are therefore observed raw channels, not decoded status models. Camera preview
|
||||
transport is observed, but its media decoder/replay flags remain independent.
|
||||
|
||||
The v1 loader rejects every `write_enabled: true`. Owner-operated LixelGO wire
|
||||
capture verifies the `ModelingRequest` topic and action values, but complete
|
||||
device/session/OpenAPI header construction, settings, save completion, timeout
|
||||
and rollback contracts remain unresolved. Acquisition therefore stays
|
||||
`operator-manual` through the verified physical double-click.
|
||||
|
||||
The existing BLE Wi-Fi provisioning workflow has its own reviewed profile and
|
||||
operator confirmation. Merely loading this compatibility profile neither calls
|
||||
nor authorizes that legacy mutation path.
|
||||
|
||||
## Validation
|
||||
|
||||
The loader uses only the Python standard library and performs no device I/O:
|
||||
|
||||
```bash
|
||||
python plugins/xgrids-k1/profile_loader.py
|
||||
```
|
||||
|
||||
It verifies the exact firmware/topology scope, evidence vocabulary, source
|
||||
references, GATT UUIDs, MQTT subscribe-only boundary, channel evidence, camera
|
||||
RTSP/H.264 endpoints, operator-manual acquisition, and disabled vendor request
|
||||
mappings.
|
||||
|
||||
## Evolution rules
|
||||
|
||||
1. A different firmware or topology gets a new profile file and profile ID.
|
||||
2. New evidence may only promote the flags supported by retained raw evidence,
|
||||
a documented decoder/replay check, or a physical lab report.
|
||||
3. Unknown fields remain explicit; they are never filled from naming or payload
|
||||
shape alone.
|
||||
4. Breaking profile semantics require a new `schema_version` and validator.
|
||||
5. Sensitive packet captures, device identities, addresses and credentials stay
|
||||
outside Git; redacted reports and hashes are the committed evidence links.
|
||||
6. A profile describes compatibility. Host authorization and transport writes
|
||||
remain separate policy and execution concerns.
|
||||
@@ -0,0 +1,406 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"profile_id": "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
|
||||
"profile_status": "experimental-evidence-backed",
|
||||
"scope": {
|
||||
"vendor": "XGRIDS",
|
||||
"model": "LixelKity K1",
|
||||
"firmware": {
|
||||
"match": "exact",
|
||||
"version": "3.0.2"
|
||||
},
|
||||
"topology": "direct-lan",
|
||||
"claim_limit": "One owner-controlled K1 on exact firmware 3.0.2 across controlled direct-LAN and owner-operated LixelGO/iPhone laboratory runs. This is not a vendor API or a cross-firmware compatibility claim."
|
||||
},
|
||||
"evidence_vocabulary": {
|
||||
"observed": "The channel, transport, or physical behavior was directly seen on the owner-controlled K1.",
|
||||
"decoded": "A bounded decoder produces the stated semantic payload from observed bytes; transport framing alone is not a semantic decode.",
|
||||
"replay_verified": "A captured payload of this semantic channel has passed the repository replay-to-view path.",
|
||||
"physical_verified": "The result was correlated with a controlled physical K1 state or operator action.",
|
||||
"write_enabled": "This profile authorizes software to emit the state-changing vendor request. False never grants runtime write authority."
|
||||
},
|
||||
"safety": {
|
||||
"default_mode": "read-only",
|
||||
"vendor_writes_enabled": false,
|
||||
"unknown_firmware_policy": "reject-profile",
|
||||
"request_topic_subscription_enabled": false,
|
||||
"notes": [
|
||||
"Loading this descriptive profile does not authorize a BLE or MQTT write.",
|
||||
"The existing reviewed Wi-Fi provisioning procedure remains separately operator-confirmed and is not activated by this profile.",
|
||||
"Observed LixelGO modeling requests describe the wire contract but remain non-replayable and write-disabled.",
|
||||
"Unknown firmware, transport, topics, fields, and action responses fail closed."
|
||||
]
|
||||
},
|
||||
"evidence_sources": [
|
||||
{
|
||||
"id": "wifi-provisioning-profile",
|
||||
"kind": "reviewed-profile",
|
||||
"path": "docs/04_K1_WIFI_PROVISIONING_PROFILE.md",
|
||||
"scope": "Observed firmware, GATT UUIDs, 99-byte provisioning frame, status read and physical LAN association."
|
||||
},
|
||||
{
|
||||
"id": "mqtt-stream-profile",
|
||||
"kind": "reviewed-profile",
|
||||
"path": "docs/05_K1_MQTT_STREAM_PROFILE.md",
|
||||
"scope": "Direct-LAN MQTT transport, report topics, bounded point/pose codecs and static modeling-request mapping."
|
||||
},
|
||||
{
|
||||
"id": "lab-001",
|
||||
"kind": "redacted-physical-lab-report",
|
||||
"path": "docs/lab/001_K1_LIVE_MQTT_20260715.redacted.md",
|
||||
"scope": "Physical BLE-to-Wi-Fi result, MQTT message counts, point/pose decode totals and negative camera observation."
|
||||
},
|
||||
{
|
||||
"id": "live-viewer-profile",
|
||||
"kind": "implemented-path-description",
|
||||
"path": "docs/06_K1_LIVE_VIEWER.md",
|
||||
"scope": "Raw-first live/replay path for point cloud and pose."
|
||||
},
|
||||
{
|
||||
"id": "lab-002",
|
||||
"kind": "redacted-physical-protocol-report",
|
||||
"path": "docs/lab/002_LIXELGO_IPHONE_LOCAL_PROTOCOL_20260716.redacted.md",
|
||||
"scope": "Owner-operated LixelGO start/stop mapping, local-only bounded traffic result and left/right RTSP/H.264 camera-preview discovery."
|
||||
}
|
||||
],
|
||||
"transports": [
|
||||
{
|
||||
"id": "ble.wifi-bootstrap.fw3.v1",
|
||||
"role": "bootstrap-and-status",
|
||||
"protocol": "BLE GATT",
|
||||
"service_uuid": "00007f00-0000-1000-8000-00805f9b34fb",
|
||||
"characteristics": {
|
||||
"wifi_request": "00007f01-0000-1000-8000-00805f9b34fb",
|
||||
"wifi_status": "00007f02-0000-1000-8000-00805f9b34fb"
|
||||
},
|
||||
"reviewed_profile_id": "xgrids-k1-fw3-wifi-v1",
|
||||
"request_frame_bytes": 99,
|
||||
"status_semantics": {
|
||||
"ap_baseline_ipv4": "192.168.56.1",
|
||||
"lan_acceptance": "A non-AP private IPv4 must be observed and independently confirmed on the intended LAN."
|
||||
},
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": true,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"wifi-provisioning-profile",
|
||||
"lab-001"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mqtt.direct-lan.fw3.v1",
|
||||
"role": "report-data-plane",
|
||||
"protocol": "MQTT 3.1.1",
|
||||
"network": {
|
||||
"transport": "TCP",
|
||||
"port": 1883,
|
||||
"tls": false,
|
||||
"authentication": "none-observed",
|
||||
"addressing": "confirmed-device-private-ipv4-only"
|
||||
},
|
||||
"subscription_allowlist": [
|
||||
"lixel/application/report/#",
|
||||
"RealtimePointcloud",
|
||||
"RealtimePath",
|
||||
"DeviceStatus"
|
||||
],
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": false,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"mqtt-stream-profile",
|
||||
"lab-001",
|
||||
"live-viewer-profile",
|
||||
"lab-002"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rtsp.camera-preview.fw3.v1",
|
||||
"role": "camera-preview-data-plane",
|
||||
"protocol": "RTSP 1.0 with interleaved RTP over TCP",
|
||||
"network": {
|
||||
"transport": "TCP",
|
||||
"port": 8554,
|
||||
"tls": false,
|
||||
"authentication": "none-observed",
|
||||
"addressing": "confirmed-device-private-ipv4-only"
|
||||
},
|
||||
"media": {
|
||||
"codec": "H.264",
|
||||
"rtp_payload_type": 96,
|
||||
"clock_hz": 90000,
|
||||
"framing": "RTP/AVP/TCP interleaved channels 0-1"
|
||||
},
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": false,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"lab-002"
|
||||
]
|
||||
}
|
||||
],
|
||||
"channels": [
|
||||
{
|
||||
"id": "spatial.point-cloud.live",
|
||||
"kind": "point-cloud",
|
||||
"direction": "device-report",
|
||||
"topic": "lixel/application/report/lio_pcl",
|
||||
"wire_format": "protobuf MqttCompressMsg containing raw-LZ4 LioPclReport",
|
||||
"semantic_payload": "metric XYZ, complete uint32 rgbi and verified low-byte intensity",
|
||||
"bounds": {
|
||||
"max_mqtt_payload_bytes": 2097152,
|
||||
"max_compressed_bytes": 1048576,
|
||||
"max_decoded_bytes": 8388608,
|
||||
"max_expansion_ratio": 64,
|
||||
"max_points_per_frame": 250000
|
||||
},
|
||||
"unverified_fields": [
|
||||
"upper 24 bits of rgbi as RGB",
|
||||
"sensor timestamp epoch",
|
||||
"sensor-to-vehicle extrinsics"
|
||||
],
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": true,
|
||||
"replay_verified": true,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"mqtt-stream-profile",
|
||||
"lab-001",
|
||||
"live-viewer-profile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "spatial.pose.live",
|
||||
"kind": "pose",
|
||||
"direction": "device-report",
|
||||
"topic": "lixel/application/report/lio_pose",
|
||||
"wire_format": "protobuf LioPoseReport",
|
||||
"semantic_payload": "position XYZ, quaternion XYZW, distance and pose accuracy",
|
||||
"unverified_fields": [
|
||||
"sensor timestamp epoch",
|
||||
"coordinate-frame convention",
|
||||
"sensor-to-vehicle extrinsics"
|
||||
],
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": true,
|
||||
"replay_verified": true,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"mqtt-stream-profile",
|
||||
"lab-001",
|
||||
"live-viewer-profile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "device.status.live",
|
||||
"kind": "device-status",
|
||||
"direction": "device-report",
|
||||
"topic": "lixel/application/report/device_status",
|
||||
"wire_format": "opaque bytes",
|
||||
"semantic_payload": null,
|
||||
"limitations": [
|
||||
"The report was physically observed, but no bounded semantic device-status decoder is implemented.",
|
||||
"Raw capture is evidence; field names or meanings must not be inferred from payload shape."
|
||||
],
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": false,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"mqtt-stream-profile",
|
||||
"lab-001"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "device.heartbeat.live",
|
||||
"kind": "heartbeat",
|
||||
"direction": "device-report",
|
||||
"topic": "lixel/application/report/heartbeat",
|
||||
"wire_format": "opaque bytes",
|
||||
"semantic_payload": null,
|
||||
"limitations": [
|
||||
"Only channel presence and approximate report cadence are verified.",
|
||||
"Payload semantics are not decoded."
|
||||
],
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": false,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"mqtt-stream-profile",
|
||||
"lab-001"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "camera.preview.live",
|
||||
"kind": "camera-preview",
|
||||
"direction": "device-report",
|
||||
"discovery_status": "observed",
|
||||
"topic": null,
|
||||
"endpoint_templates": [
|
||||
"rtsp://{confirmed-device-private-ipv4}:8554/live/chn_left_main",
|
||||
"rtsp://{confirmed-device-private-ipv4}:8554/live/chn_right_main"
|
||||
],
|
||||
"wire_format": "RTSP 1.0, interleaved RTP/TCP, H.264 PT96 at 90000 Hz",
|
||||
"semantic_payload": "compressed live left/right camera preview selected by endpoint",
|
||||
"limitations": [
|
||||
"No full-resolution raw frame, camera calibration or panorama-stitching contract is verified.",
|
||||
"Left/right optical identity is supported by endpoint labels and operator-selected application views, not an independent image-content fixture.",
|
||||
"A bounded Mission Core camera decoder and replay fixture are not yet implemented."
|
||||
],
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": false,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"lab-002"
|
||||
]
|
||||
}
|
||||
],
|
||||
"acquisition_control": {
|
||||
"mode": "operator-manual",
|
||||
"write_enabled": false,
|
||||
"verified_device_control": {
|
||||
"gesture": "physical-double-click",
|
||||
"state_dependent_result": "start from steady-green standby; stop during active scanning",
|
||||
"single_click_result": "not a verified scan-start action",
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": false,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"lab-001"
|
||||
]
|
||||
},
|
||||
"semantic_actions": [
|
||||
{
|
||||
"id": "acquisition.start",
|
||||
"execution": "operator-manual",
|
||||
"operator_control": "physical-double-click from steady-green standby",
|
||||
"observed_application_control": "owner-operated LixelGO project confirmation",
|
||||
"vendor_request_mapping": {
|
||||
"evidence_kind": "owner-controlled-wire-observation",
|
||||
"transport": "MQTT 3.1.1",
|
||||
"topic": "lixel/application/request/modeling",
|
||||
"qos": 2,
|
||||
"message_type": "ModelingRequest",
|
||||
"action_field_value": 1,
|
||||
"required_unresolved_context": [
|
||||
"complete device/session/OpenAPI header construction",
|
||||
"project/record/scan/mount setting semantics and safe defaults",
|
||||
"timeout, rejection and rollback contract"
|
||||
],
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": true,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"write_enabled": false
|
||||
},
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": true,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"mqtt-stream-profile",
|
||||
"lab-001",
|
||||
"lab-002"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "acquisition.stop",
|
||||
"execution": "operator-manual",
|
||||
"operator_control": "physical-double-click during active scanning",
|
||||
"observed_application_control": "owner-operated LixelGO stop confirmation",
|
||||
"vendor_request_mapping": {
|
||||
"evidence_kind": "owner-controlled-wire-observation",
|
||||
"transport": "MQTT 3.1.1",
|
||||
"topic": "lixel/application/request/modeling",
|
||||
"qos": 2,
|
||||
"message_type": "ModelingRequest",
|
||||
"action_field_value": 2,
|
||||
"required_unresolved_context": [
|
||||
"complete device/session/OpenAPI header construction",
|
||||
"save-completion and final-standby state mapping",
|
||||
"timeout and rollback contract"
|
||||
],
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": true,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"write_enabled": false
|
||||
},
|
||||
"evidence": {
|
||||
"observed": true,
|
||||
"decoded": true,
|
||||
"replay_verified": false,
|
||||
"physical_verified": true,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"mqtt-stream-profile",
|
||||
"lab-001",
|
||||
"lab-002"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "calibration.device.start",
|
||||
"execution": "unavailable",
|
||||
"operator_control": null,
|
||||
"vendor_request_mapping": null,
|
||||
"observed_behavior": "Static initialization follows acquisition.start; no independent calibration action was observed.",
|
||||
"limitations": [
|
||||
"No standalone calibration command topic, request schema, acknowledgment or state transition is verified."
|
||||
],
|
||||
"evidence": {
|
||||
"observed": false,
|
||||
"decoded": false,
|
||||
"replay_verified": false,
|
||||
"physical_verified": false,
|
||||
"write_enabled": false
|
||||
},
|
||||
"source_ids": [
|
||||
"lab-002"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user