feat(k1): add live cameras and reliable spatial following
This commit is contained in:
+143
-35
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user