feat(viewer): add recorded point colors and trajectory follow
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
|
||||
from k1link.device_plugins.xgrids_k1.recorded_point_colors import (
|
||||
RecordedPointColorOverlayStore,
|
||||
)
|
||||
from k1link.sessions import ReplayArtifact, ReplayCommand
|
||||
|
||||
|
||||
def _point_payload(x: float, intensity: int) -> bytes:
|
||||
return struct.pack("<III", 16, 0, 0) + struct.pack(
|
||||
"<fffBBBB",
|
||||
x,
|
||||
-2.0,
|
||||
3.0,
|
||||
10,
|
||||
20,
|
||||
30,
|
||||
intensity,
|
||||
)
|
||||
|
||||
|
||||
def _command(tmp_path: Path) -> ReplayCommand:
|
||||
source = tmp_path / "mqtt.raw.k1mqtt"
|
||||
metadata_path = tmp_path / "mqtt.metadata.jsonl"
|
||||
raw = bytearray(RAW_MAGIC)
|
||||
metadata: list[dict[str, object]] = []
|
||||
epoch_origin_ns = 1_784_124_315_000_000_000
|
||||
monotonic_origin_ns = 9_000_000_000
|
||||
topic = "RealtimePointcloud"
|
||||
for sequence in range(1, 7):
|
||||
payload = _point_payload(float(sequence), sequence * 20)
|
||||
topic_bytes = topic.encode("utf-8")
|
||||
frame_offset = len(raw)
|
||||
raw.extend(FRAME_HEADER.pack(len(topic_bytes), len(payload)))
|
||||
raw.extend(topic_bytes)
|
||||
payload_offset = len(raw)
|
||||
raw.extend(payload)
|
||||
metadata.append(
|
||||
{
|
||||
"record_type": "message",
|
||||
"sequence": sequence,
|
||||
"received_at_epoch_ns": epoch_origin_ns + sequence * 100_000_000,
|
||||
"received_monotonic_ns": monotonic_origin_ns + sequence * 100_000_000,
|
||||
"topic": topic,
|
||||
"payload_bytes": len(payload),
|
||||
"payload_sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"raw_frame_offset": frame_offset,
|
||||
"raw_payload_offset": payload_offset,
|
||||
"raw_frame_bytes": len(raw) - frame_offset,
|
||||
}
|
||||
)
|
||||
source.write_bytes(raw)
|
||||
metadata_path.write_text(
|
||||
"".join(json.dumps(item, separators=(",", ":")) + "\n" for item in metadata),
|
||||
encoding="utf-8",
|
||||
)
|
||||
artifacts = (
|
||||
ReplayArtifact(
|
||||
artifact_id="raw-transport-primary",
|
||||
path=source,
|
||||
media_type="application/x-nodedc-k1mqtt",
|
||||
file_byte_length=source.stat().st_size,
|
||||
replay_byte_length=source.stat().st_size,
|
||||
expected_sha256=hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
),
|
||||
ReplayArtifact(
|
||||
artifact_id="raw-transport-index",
|
||||
path=metadata_path,
|
||||
media_type="application/x-ndjson",
|
||||
file_byte_length=metadata_path.stat().st_size,
|
||||
replay_byte_length=metadata_path.stat().st_size,
|
||||
expected_sha256=hashlib.sha256(metadata_path.read_bytes()).hexdigest(),
|
||||
),
|
||||
)
|
||||
return ReplayCommand(
|
||||
session_id="recorded-color-test",
|
||||
plugin_id="nodedc.device.xgrids-lixelkity-k1",
|
||||
allowed_root=tmp_path,
|
||||
session_root=tmp_path,
|
||||
primary_artifact_id="raw-transport-primary",
|
||||
artifacts=artifacts,
|
||||
timeline_origin_epoch_ns=epoch_origin_ns,
|
||||
timeline_origin_monotonic_ns=monotonic_origin_ns,
|
||||
speed=1.0,
|
||||
loop=False,
|
||||
)
|
||||
|
||||
|
||||
def test_recorded_color_overlay_logs_only_colors_at_operator_frame_cadence(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = RecordedPointColorOverlayStore()
|
||||
payload = store.render(
|
||||
_command(tmp_path),
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id="recording-001",
|
||||
color_mode="height",
|
||||
palette="viridis",
|
||||
custom_color="#35d7c1",
|
||||
)
|
||||
output = tmp_path / "colors.rrd"
|
||||
output.write_bytes(payload)
|
||||
printed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rerun_cli",
|
||||
"rrd",
|
||||
"print",
|
||||
"-vvv",
|
||||
"--entity",
|
||||
"/world/points",
|
||||
str(output),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout
|
||||
|
||||
assert payload.startswith(b"RRF2")
|
||||
assert printed.count("Points3D:colors") == 2
|
||||
assert "Points3D:positions" not in printed
|
||||
assert "session_time" in printed
|
||||
|
||||
|
||||
def test_recorded_color_overlay_reuses_index_and_identical_payload(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = RecordedPointColorOverlayStore()
|
||||
command = _command(tmp_path)
|
||||
first = store.render(
|
||||
command,
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id="recording-001",
|
||||
color_mode="distance",
|
||||
palette="plasma",
|
||||
custom_color="#35d7c1",
|
||||
)
|
||||
second = store.render(
|
||||
command,
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id="recording-001",
|
||||
color_mode="distance",
|
||||
palette="plasma",
|
||||
custom_color="#35d7c1",
|
||||
)
|
||||
|
||||
assert second is first
|
||||
@@ -247,12 +247,23 @@ def test_palettes_are_deterministic_and_custom_color_is_exact() -> None:
|
||||
None,
|
||||
RerunSceneSettings(color_mode="class", custom_color="#102030"),
|
||||
)
|
||||
custom_over_rgb = _point_colors(
|
||||
positions,
|
||||
intensities,
|
||||
np.asarray([[255, 0, 0], [0, 255, 0]], dtype=np.uint8),
|
||||
RerunSceneSettings(
|
||||
color_mode="rgb",
|
||||
palette="custom",
|
||||
custom_color="#102030",
|
||||
),
|
||||
)
|
||||
|
||||
assert height.shape == (2, 3)
|
||||
assert height.dtype == np.uint8
|
||||
assert height[0].tolist() == [68, 1, 84]
|
||||
assert height[1].tolist() == [253, 231, 37]
|
||||
assert custom.tolist() == [[16, 32, 48], [16, 32, 48]]
|
||||
assert custom_over_rgb.tolist() == [[16, 32, 48], [16, 32, 48]]
|
||||
|
||||
|
||||
def test_runtime_reuses_one_bridge_across_sequential_sessions(tmp_path: Path) -> None:
|
||||
|
||||
@@ -37,6 +37,9 @@ from k1link.device_plugins.xgrids_k1.rrd_export import (
|
||||
export_k1mqtt_to_rrd,
|
||||
recorded_blueprint_rrd,
|
||||
)
|
||||
from k1link.viewer.recorded import (
|
||||
RECORDED_SPATIAL_FOLLOW_VIEW_ID,
|
||||
)
|
||||
from k1link.viewer.recorded import (
|
||||
recorded_blueprint as viewer_recorded_blueprint,
|
||||
)
|
||||
@@ -450,6 +453,40 @@ def test_viewer_blueprint_reset_is_bounded_and_recreates_render_views() -> None:
|
||||
assert perception_behavior.visible.as_arrow_array().to_pylist() == [False]
|
||||
|
||||
|
||||
def test_recorded_follow_mode_tracks_sensor_pose_with_an_independent_orbital_eye() -> None:
|
||||
normal = viewer_recorded_blueprint(
|
||||
RerunSceneSettings(),
|
||||
include_initial_playback_state=False,
|
||||
)
|
||||
followed = viewer_recorded_blueprint(
|
||||
RerunSceneSettings(),
|
||||
include_initial_playback_state=False,
|
||||
follow_trajectory=True,
|
||||
)
|
||||
followed_again = viewer_recorded_blueprint(
|
||||
RerunSceneSettings(show_grid=False),
|
||||
include_initial_playback_state=False,
|
||||
follow_trajectory=True,
|
||||
)
|
||||
|
||||
normal_view = normal.root_container.contents[0]
|
||||
followed_view = followed.root_container.contents[0]
|
||||
followed_eye = followed_view.properties["EyeControls3D"]
|
||||
followed_components = {
|
||||
str(batch.component_descriptor()): batch.as_arrow_array().to_pylist()
|
||||
for batch in followed_eye.as_component_batches()
|
||||
}
|
||||
|
||||
assert "EyeControls3D" not in normal_view.properties
|
||||
assert followed_view.id == RECORDED_SPATIAL_FOLLOW_VIEW_ID
|
||||
assert followed_again.root_container.contents[0].id == RECORDED_SPATIAL_FOLLOW_VIEW_ID
|
||||
assert followed_view.id != normal_view.id
|
||||
assert followed_components == {
|
||||
"EyeControls3D:kind": [2],
|
||||
"EyeControls3D:tracking_entity": ["/world/sensor_pose"],
|
||||
}
|
||||
|
||||
|
||||
def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
@@ -37,6 +37,7 @@ from k1link.web.session_api import (
|
||||
LayoutPutRequest,
|
||||
RecordedBlueprintRequest,
|
||||
RecordedPerceptionRequest,
|
||||
RecordedPointColorsRequest,
|
||||
ReplayRequest,
|
||||
build_session_router,
|
||||
)
|
||||
@@ -1083,6 +1084,7 @@ def test_recorded_blueprint_endpoint_is_small_strict_and_session_scoped(
|
||||
show_trajectory=True,
|
||||
show_grid=False,
|
||||
point_size=6.25,
|
||||
color_mode="height",
|
||||
palette="custom",
|
||||
custom_color="#112233",
|
||||
active_view="perception",
|
||||
@@ -1091,6 +1093,7 @@ def test_recorded_blueprint_endpoint_is_small_strict_and_session_scoped(
|
||||
show_detections_2d=True,
|
||||
show_segmentation=True,
|
||||
show_cuboids_3d=True,
|
||||
follow_trajectory=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -1104,6 +1107,7 @@ def test_recorded_blueprint_endpoint_is_small_strict_and_session_scoped(
|
||||
assert len(observed_settings) == 1
|
||||
assert observed_settings[0].show_points is False
|
||||
assert observed_settings[0].show_trajectory is True
|
||||
assert observed_settings[0].color_mode == "height"
|
||||
assert observed_kwargs[0]["blueprint_session_id"] == "a" * 32
|
||||
assert observed_kwargs[0]["active_view"] == "perception"
|
||||
assert observed_kwargs[0]["view_reset_generation"] == 1
|
||||
@@ -1111,6 +1115,7 @@ def test_recorded_blueprint_endpoint_is_small_strict_and_session_scoped(
|
||||
assert observed_kwargs[0]["show_detections_2d"] is True
|
||||
assert observed_kwargs[0]["show_segmentation"] is True
|
||||
assert observed_kwargs[0]["show_cuboids_3d"] is True
|
||||
assert observed_kwargs[0]["follow_trajectory"] is True
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
RecordedBlueprintRequest.model_validate(
|
||||
@@ -1201,6 +1206,96 @@ def test_recorded_perception_endpoint_returns_one_complete_optional_overlay(
|
||||
assert empty.status_code == 204
|
||||
|
||||
|
||||
def test_recorded_point_color_endpoint_is_strict_and_forwards_confined_command(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
calls: list[tuple[str, str, str, str, str, str]] = []
|
||||
|
||||
class Provider:
|
||||
def __call__(
|
||||
self,
|
||||
command: ReplayCommand,
|
||||
*,
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
color_mode: str,
|
||||
palette: str,
|
||||
custom_color: str,
|
||||
) -> bytes:
|
||||
calls.append(
|
||||
(
|
||||
command.session_id,
|
||||
application_id,
|
||||
recording_id,
|
||||
color_mode,
|
||||
palette,
|
||||
custom_color,
|
||||
)
|
||||
)
|
||||
return b"RRF2colors"
|
||||
|
||||
router = build_session_router(
|
||||
store,
|
||||
point_color_renderers={"nodedc.device.xgrids-lixelkity-k1": Provider()},
|
||||
)
|
||||
color_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/point-colors.rrd",
|
||||
"POST",
|
||||
)
|
||||
response = asyncio.run(
|
||||
color_route(
|
||||
session_id=session.name,
|
||||
request=RecordedPointColorsRequest(
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id="recording-001",
|
||||
color_mode="distance",
|
||||
palette="viridis",
|
||||
custom_color="#112233",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert response.body == b"RRF2colors"
|
||||
assert response.media_type == "application/vnd.rerun.rrd"
|
||||
assert response.headers["content-length"] == str(len(response.body))
|
||||
assert len(calls) == 1
|
||||
assert calls[0] == (
|
||||
session.name,
|
||||
"nodedc_mission_core_recorded",
|
||||
"recording-001",
|
||||
"distance",
|
||||
"viridis",
|
||||
"#112233",
|
||||
)
|
||||
|
||||
unavailable_router = build_session_router(store)
|
||||
unavailable_route = endpoint(
|
||||
unavailable_router,
|
||||
"/api/v1/observation-sessions/{session_id}/point-colors.rrd",
|
||||
"POST",
|
||||
)
|
||||
with pytest.raises(HTTPException) as unavailable:
|
||||
asyncio.run(
|
||||
unavailable_route(
|
||||
session_id=session.name,
|
||||
request=RecordedPointColorsRequest(
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id="recording-001",
|
||||
color_mode="intensity",
|
||||
palette="turbo",
|
||||
custom_color="#35d7c1",
|
||||
),
|
||||
)
|
||||
)
|
||||
assert unavailable.value.status_code == 409
|
||||
|
||||
|
||||
def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user