feat(k1): add live cameras and reliable spatial following

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 00:26:03 +03:00
parent a281faf923
commit 2bda1986bd
33 changed files with 2927 additions and 330 deletions
+5 -2
View File
@@ -88,7 +88,7 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
item for item in plugins if item["metadata"]["id"] == "nodedc.device.xgrids-lixelkity-k1"
)
assert plugin["apiVersion"] == "missioncore.nodedc/v1alpha2"
assert plugin["metadata"]["version"] == "0.2.0"
assert plugin["metadata"]["version"] == "0.3.0"
assert plugin["spec"]["hostApiRange"] == "v1alpha2"
assert plugin["spec"]["compatibilityProfiles"] == [
{
@@ -113,11 +113,13 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
"stream.start-live",
"stream.start-replay",
"stream.stop",
"camera.preview.select",
"camera.preview.stop",
"viewer.settings.update",
} <= action_ids
assert next(item for item in models if item["id"] == "xgrids.lixelkity-k1") == {
"pluginId": "nodedc.device.xgrids-lixelkity-k1",
"pluginVersion": "0.2.0",
"pluginVersion": "0.3.0",
"id": "xgrids.lixelkity-k1",
"vendor": "XGRIDS",
"displayName": "XGRIDS LixelKity K1",
@@ -135,6 +137,7 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
},
{"id": "spatial.point-cloud.live", "label": "Облако точек"},
{"id": "spatial.pose.live", "label": "Траектория"},
{"id": "camera.preview.live", "label": "Видеокамеры"},
{"id": "evidence.raw-capture", "label": "Исходная запись"},
{"id": "evidence.replay", "label": "Повтор записи"},
],
+143 -35
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
import json
import socket
import struct
import threading
import time
@@ -14,7 +13,12 @@ from k1link.data_plane import DecodedDataPlaneView, NormalizationError
from k1link.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.protocol.normalizer import normalize_k1_message
from k1link.viewer.messages import StreamMessage
from k1link.viewer.rerun_bridge import RerunBridge, RerunSceneSettings, _point_colors
from k1link.viewer.rerun_bridge import (
RerunBridge,
RerunSceneSettings,
_live_time_panel,
_point_colors,
)
from k1link.viewer.runtime import VisualizationRuntime
@@ -24,6 +28,7 @@ class FakeRecording:
self.times: list[tuple[str, dict[str, object]]] = []
self.blueprints: list[object] = []
self.disconnected = False
self.flush_count = 0
def serve_grpc(self, **_: object) -> str:
return "rerun+http://127.0.0.1:9876/proxy"
@@ -41,23 +46,52 @@ class FakeRecording:
self.disconnected = True
def flush(self, **_: object) -> None:
return
self.flush_count += 1
def _message(topic: str, payload: bytes, *, sequence: int = 7) -> StreamMessage:
class BlueprintFailureRecording(FakeRecording):
def send_blueprint(self, blueprint: object, **kwargs: object) -> None:
super().send_blueprint(blueprint, **kwargs)
raise RuntimeError("synthetic blueprint failure")
class DisconnectFailureRecording(FakeRecording):
def disconnect(self) -> None:
super().disconnect()
raise RuntimeError("synthetic disconnect failure")
def _message(
topic: str,
payload: bytes,
*,
sequence: int = 7,
received_at_epoch_ns: int = 1_784_124_315_186_225_000,
) -> StreamMessage:
return StreamMessage(
sequence=sequence,
topic=topic,
payload=payload,
received_at_epoch_ns=1_784_124_315_186_225_000,
received_at_epoch_ns=received_at_epoch_ns,
received_monotonic_ns=None,
source="test",
)
def _envelope(topic: str, payload: bytes, *, sequence: int = 7) -> DecodedDataPlaneView:
def _envelope(
topic: str,
payload: bytes,
*,
sequence: int = 7,
received_at_epoch_ns: int = 1_784_124_315_186_225_000,
) -> DecodedDataPlaneView:
envelope = normalize_k1_message(
_message(topic, payload, sequence=sequence),
_message(
topic,
payload,
sequence=sequence,
received_at_epoch_ns=received_at_epoch_ns,
),
processing_started_monotonic_ns=time.monotonic_ns(),
)
assert envelope is not None
@@ -89,9 +123,58 @@ def test_legacy_points_and_pose_are_logged_to_rerun() -> None:
"message_sequence",
"stream_time",
}
assert recording.flush_count == 1
bridge.close()
assert recording.disconnected is True
assert recording.flush_count == 2
def test_live_blueprint_follows_stream_time_without_frontend_cursor_writes() -> None:
panel = _live_time_panel()
assert panel.timeline == "stream_time"
assert panel.play_state == "following"
assert panel.state == "hidden"
def test_constructor_disconnects_recording_after_partial_setup_failure() -> None:
recording = BlueprintFailureRecording()
with pytest.raises(RuntimeError, match="synthetic blueprint failure"):
RerunBridge(recording_factory=lambda _: recording) # type: ignore[arg-type]
assert recording.disconnected is True
def test_fast_replay_trajectory_sampling_uses_source_time() -> None:
recording = FakeRecording()
bridge = RerunBridge(recording_factory=lambda _: recording) # type: ignore[arg-type]
base_time_ns = 1_784_124_315_000_000_000
for index in range(4):
pose_payload = struct.pack(
"<ffffffff",
index * 0.1,
2.0,
3.0,
99.0,
0.9,
0.1,
0.2,
0.3,
)
bridge.process(
_envelope(
"RealtimePath",
pose_payload,
sequence=index + 1,
received_at_epoch_ns=base_time_ns + index * 600_000_000,
)
)
assert bridge.metrics.snapshot()["trajectory_poses"] == 4
bridge.close()
def test_bad_frame_is_rejected_before_rerun_without_publishing() -> None:
@@ -134,34 +217,7 @@ def test_palettes_are_deterministic_and_custom_color_is_exact() -> None:
assert custom.tolist() == [[16, 32, 48], [16, 32, 48]]
def test_real_grpc_server_releases_its_port() -> None:
probe = socket.socket()
probe.bind(("127.0.0.1", 0))
port = int(probe.getsockname()[1])
probe.close()
bridge = RerunBridge(
grpc_port=port,
cors_allow_origin=("http://127.0.0.1:8000",),
)
assert bridge.grpc_url == f"rerun+http://127.0.0.1:{port}/proxy"
bridge.close()
deadline = time.monotonic() + 2.0
while True:
available = socket.socket()
try:
available.bind(("127.0.0.1", port))
break
except OSError:
if time.monotonic() >= deadline:
raise
time.sleep(0.02)
finally:
available.close()
def test_runtime_exposes_rerun_url_and_stops_cleanly(tmp_path: Path) -> None:
def test_runtime_reuses_one_bridge_across_sequential_sessions(tmp_path: Path) -> None:
capture = tmp_path / "mqtt.raw.k1mqtt"
point_topic = "RealtimePointcloud"
pose_topic = "RealtimePath"
@@ -237,11 +293,63 @@ def test_runtime_exposes_rerun_url_and_stops_cleanly(tmp_path: Path) -> None:
assert len(created) == 1
assert runtime.snapshot()["metrics"]["pcl_frames"] == 1
assert runtime.snapshot()["rerun_grpc_url"] == "rerun+http://127.0.0.1:9876/proxy"
assert recording.disconnected is False
runtime.close()
assert runtime.snapshot()["rerun_grpc_url"] is None
assert recording.disconnected is True
def test_runtime_reports_bridge_close_failure_instead_of_false_idle(tmp_path: Path) -> None:
capture = tmp_path / "mqtt.raw.k1mqtt"
topic = b"RealtimePointcloud"
payload = struct.pack("<III", 16, 0, 0) + struct.pack(
"<fffBBBB", 1.0, -2.0, 3.0, 10, 20, 30, 40
)
capture.write_bytes(RAW_MAGIC + FRAME_HEADER.pack(len(topic), len(payload)) + topic + payload)
(tmp_path / "mqtt.metadata.jsonl").write_text(
json.dumps(
{
"record_type": "message",
"sequence": 1,
"received_at_epoch_ns": 1_000_000_000,
"received_monotonic_ns": 1_000_000_000,
}
)
+ "\n",
encoding="utf-8",
)
recording = DisconnectFailureRecording()
runtime = VisualizationRuntime(
bridge_factory=lambda **kwargs: RerunBridge(
recording_factory=lambda _: recording, # type: ignore[arg-type]
**kwargs, # type: ignore[arg-type]
),
normalizer=normalize_k1_message,
)
runtime.start_replay(capture, speed=0.0)
deadline = time.monotonic() + 5.0
snapshot = runtime.snapshot()
while snapshot["phase"] not in {"idle", "error"} and time.monotonic() < deadline:
time.sleep(0.01)
snapshot = runtime.snapshot()
assert snapshot["phase"] == "idle"
assert snapshot["rerun_grpc_url"] == "rerun+http://127.0.0.1:9876/proxy"
assert recording.disconnected is False
with pytest.raises(RuntimeError, match="synthetic disconnect failure"):
runtime.close()
snapshot = runtime.snapshot()
assert snapshot["phase"] == "error"
assert "synthetic disconnect failure" in snapshot["message"]
assert snapshot["rerun_grpc_url"] is None
assert recording.disconnected is True
def test_close_during_blocked_factory_closes_the_late_bridge(tmp_path: Path) -> None:
capture = tmp_path / "mqtt.raw.k1mqtt"
topic = b"RealtimePointcloud"
+21 -11
View File
@@ -664,18 +664,22 @@ def test_provisioning_cannot_switch_device_during_active_acquisition(
assert called is False
def test_sensor_catalog_exposes_observed_camera_only_after_profile_attestation(
def test_sensor_catalog_exposes_two_browser_adapter_cameras_after_profile_attestation(
tmp_path: Path,
) -> None:
service, _ = service_with_fake_runtime(tmp_path)
initial = service.state()
initial_camera = next(
initial_cameras = [
stream
for stream in initial["sensor_catalog"]["streams"]
if stream["stream_id"] == "camera.preview.live"
)
assert initial_camera["availability"] == "unverified"
if stream.get("semantic_channel_id") == "camera.preview.live"
]
assert {stream["source_id"] for stream in initial_cameras} == {
"sensor.camera.left",
"sensor.camera.right",
}
assert all(stream["availability"] == "unverified" for stream in initial_cameras)
state = service.prepare_acquisition(
PrepareAcquisitionRequest(
@@ -683,15 +687,21 @@ def test_sensor_catalog_exposes_observed_camera_only_after_profile_attestation(
compatibility_attestation=ATTESTATION,
)
)
camera = next(
cameras = [
stream
for stream in state["sensor_catalog"]["streams"]
if stream["stream_id"] == "camera.preview.live"
)
if stream.get("semantic_channel_id") == "camera.preview.live"
]
assert camera["availability"] == "observed"
assert camera["modality"] == "encoded-video"
assert camera["decode_status"] == "transport-observed-runtime-adapter-pending"
assert len(cameras) == 2
assert all(camera["availability"] == "available" for camera in cameras)
assert all(camera["modality"] == "encoded-video" for camera in cameras)
assert all(
camera["decode_status"] == "rtsp-h264-observed-browser-remux"
for camera in cameras
)
assert all(camera["activation"]["max_active"] == 1 for camera in cameras)
assert all(camera["delivery"] is None for camera in cameras)
assert state["connection_verification"]["network_reachability"] == "unknown"
assert state["device_calibration"]["status"] == "unavailable"
assert state["device_calibration"]["vehicle_extrinsics"] == ("host-domain-not-owned-by-plugin")
+205
View File
@@ -0,0 +1,205 @@
from __future__ import annotations
import json
import sys
from io import BytesIO
from pathlib import Path
import pytest
from k1link.web.xgrids_k1_camera import (
CAMERA_MEDIA_TYPE,
XgridsK1CameraGateway,
_build_ffmpeg_argv,
_read_mp4_box,
)
from k1link.web.xgrids_k1_facade import (
XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
XGRIDS_K1_PLUGIN_ID,
CameraPreviewSelectRequest,
CameraPreviewStopRequest,
XgridsK1CompatibilityService,
)
def _fake_ffmpeg(tmp_path: Path) -> Path:
executable = tmp_path / "fake-ffmpeg"
executable.write_text(
f"#!{sys.executable}\n"
"import sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"payload = (box(b'ftyp', b'isom') + box(b'moov') + "
"box(b'moof') + box(b'mdat', b'frame'))\n"
"sys.stdout.buffer.write(payload)\n"
"sys.stdout.buffer.flush()\n"
"time.sleep(10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _gateway(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> XgridsK1CameraGateway:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_fake_ffmpeg(tmp_path)))
return XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
def test_camera_selection_is_exclusive_and_hides_device_transport(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
try:
left = gateway.select("sensor.camera.left", "192.168.8.52")
assert left["active_source_id"] == "sensor.camera.left"
assert left["generation"] == 1
assert left["activation"]["max_active"] == 1
assert left["delivery"]["kind"] == "mse-fmp4-websocket"
assert left["delivery"]["media_type"] == CAMERA_MEDIA_TYPE
serialized = json.dumps(left)
assert "192.168.8.52" not in serialized
assert "rtsp://" not in serialized
assert "chn_left_main" not in serialized
right = gateway.select("sensor.camera.right", "192.168.8.52")
assert right["active_source_id"] == "sensor.camera.right"
assert right["generation"] == 2
assert right["delivery"]["url"].endswith("/camera-preview/2")
with pytest.raises(ValueError, match="generation"):
gateway.stop(1)
assert gateway.snapshot()["active_source_id"] == "sensor.camera.right"
stopped = gateway.stop(2)
assert stopped["phase"] == "idle"
assert stopped["delivery"] is None
finally:
gateway.close()
def test_camera_ffmpeg_command_is_allowlisted_copy_remux() -> None:
argv = _build_ffmpeg_argv(
Path("/trusted/ffmpeg"),
"10.0.0.24",
"sensor.camera.left",
)
assert argv[0] == "/trusted/ffmpeg"
assert argv[argv.index("-i") + 1] == (
"rtsp://10.0.0.24:8554/live/chn_left_main"
)
assert argv[argv.index("-c:v") + 1] == "copy"
assert argv[argv.index("-allowed_media_types") + 1] == "video"
assert argv[argv.index("-flush_packets") + 1] == "1"
assert "-c:v" in argv
assert ";" not in " ".join(argv)
with pytest.raises(ValueError, match="private IPv4"):
_build_ffmpeg_argv(
Path("/trusted/ffmpeg"),
"example.com",
"sensor.camera.left",
)
def test_iso_bmff_reader_preserves_complete_boxes() -> None:
def box(kind: bytes, payload: bytes = b"") -> bytes:
return (8 + len(payload)).to_bytes(4, "big") + kind + payload
stream = BytesIO(box(b"ftyp", b"isom") + box(b"moov") + box(b"moof"))
assert _read_mp4_box(stream) == (b"ftyp", box(b"ftyp", b"isom"))
assert _read_mp4_box(stream) == (b"moov", box(b"moov"))
assert _read_mp4_box(stream) == (b"moof", box(b"moof"))
oversized = (9 * 1024 * 1024).to_bytes(4, "big") + b"mdat"
with pytest.raises(ValueError, match="unbounded"):
_read_mp4_box(BytesIO(oversized))
def test_gateway_emits_init_and_complete_media_segments_without_transcoding(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
try:
state = gateway.select("sensor.camera.right", "192.168.1.20")
lease = gateway.open_delivery(state["generation"])
init_segment = lease.segments.get(timeout=3)
media_segment = lease.segments.get(timeout=3)
assert init_segment is not None and init_segment[0] == "init"
assert b"ftyp" in init_segment[1] and b"moov" in init_segment[1]
assert media_segment is not None and media_segment[0] == "media"
assert b"moof" in media_segment[1] and b"mdat" in media_segment[1]
gateway.mark_streaming(lease)
assert gateway.snapshot()["phase"] == "streaming"
gateway.release_delivery(lease, client_closed=True)
assert gateway.snapshot()["phase"] == "selected"
finally:
gateway.close()
def test_service_publishes_two_dynamic_camera_rows_and_stale_stop_is_safe(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_fake_ffmpeg(tmp_path)))
service = XgridsK1CompatibilityService(tmp_path)
try:
with service._lock:
service._k1_ip = "192.168.1.20"
service._device_id = "device-k1-test"
service._device_session_id = "device-session-test"
service._device_session_opened_at = "2026-07-16T20:00:00Z"
service._compatibility_attestation = {
"firmware_version": "3.0.2",
"topology": "direct-lan",
"basis": "operator-attested",
"observed_at": "2026-07-16T20:00:00Z",
}
state = service.select_camera_preview(
CameraPreviewSelectRequest(
source_id="sensor.camera.left",
device_session_id="device-session-test",
)
)
cameras = [
stream
for stream in state["sensor_catalog"]["streams"]
if stream.get("semantic_channel_id") == "camera.preview.live"
]
assert len(cameras) == 2
assert sum(bool(stream["activation"]["selected"]) for stream in cameras) == 1
assert sum(stream["delivery"] is not None for stream in cameras) == 1
assert state["compatibility"]["profile_id"] == XGRIDS_K1_COMPATIBILITY_PROFILE_ID
camera_contract = json.dumps(
{"camera_preview": state["camera_preview"], "streams": cameras}
)
assert "rtsp://" not in camera_contract
assert "192.168.1.20" not in camera_contract
generation = state["camera_preview"]["generation"]
stopped = service.stop_camera_preview(
CameraPreviewStopRequest(
device_session_id="device-session-test",
generation=generation,
)
)
assert stopped["camera_preview"]["phase"] == "idle"
with pytest.raises(ValueError, match="device-сессия"):
service.select_camera_preview(
CameraPreviewSelectRequest(
source_id="sensor.camera.right",
device_session_id="stale-session",
)
)
finally:
service.close()