feat: activate self-hosted lidar spatial scene
This commit is contained in:
+209
-83
@@ -1,9 +1,11 @@
|
||||
# K1 live console and Foxglove bridge
|
||||
# K1 live console and embedded Rerun bridge
|
||||
|
||||
Status: alpha implementation for the verified firmware-3 MQTT streams. It uses
|
||||
real K1 data and does not generate a placeholder cloud, pose or latency value.
|
||||
Status: the Rerun alpha milestone is implemented for the verified firmware-3
|
||||
MQTT streams. Live capture and replay use real K1 point-cloud and pose messages;
|
||||
the application does not generate a placeholder cloud, trajectory, camera frame
|
||||
or latency value.
|
||||
|
||||
## Architecture
|
||||
## Active device-to-scene path
|
||||
|
||||
```text
|
||||
K1 lio_pcl / lio_pose
|
||||
@@ -11,139 +13,263 @@ K1 lio_pcl / lio_pose
|
||||
v
|
||||
read-only MQTT subscription on TCP 1883
|
||||
|
|
||||
+--> raw .k1mqtt + JSONL + SHA-256 summary (first)
|
||||
+--> raw .k1mqtt + JSONL + SHA-256 summary (written first)
|
||||
|
|
||||
v
|
||||
bounded latest-wins preview queue (32 messages)
|
||||
|
|
||||
v
|
||||
raw-LZ4/protobuf decoder --> foxglove.PointCloud / Pose / SceneUpdate
|
||||
reviewed raw-LZ4/protobuf decoders
|
||||
|
|
||||
v
|
||||
ws://127.0.0.1:8765 --> Foxglove 3D
|
||||
Rerun Points3D + Transform3D + LineStrips3D
|
||||
|
|
||||
v
|
||||
Rerun gRPC/proxy on TCP 9876
|
||||
|
|
||||
+--> rerun_grpc_url in REST/WebSocket state
|
||||
|
|
||||
v
|
||||
self-hosted @rerun-io/web-viewer inside NODE.DC Control Station
|
||||
|
||||
React console <-- REST + WebSocket state --> FastAPI on 127.0.0.1:8000
|
||||
```
|
||||
|
||||
The Paho MQTT callback never decodes the point cloud. It writes and flushes the
|
||||
raw frame and metadata, then enqueues a preview reference. If visualization cannot
|
||||
keep up, the oldest queued preview is discarded while the raw capture continues.
|
||||
The Paho MQTT callback does not decode or render the point cloud. It first
|
||||
writes and flushes the raw frame and metadata, then enqueues a preview message.
|
||||
If visualization cannot keep up, the oldest queued preview is discarded while
|
||||
the raw capture continues. Rerun work stays on the dedicated publisher thread
|
||||
and cannot block raw-first evidence capture.
|
||||
|
||||
The current live/replay runtime instantiates only `RerunBridge`. The former
|
||||
Foxglove implementation is not a parallel runtime and does not listen on TCP
|
||||
8765; it is retained only as a legacy regression module and test oracle.
|
||||
|
||||
## Start the local application
|
||||
|
||||
Prerequisites are the repository-local Python environment and Node.js 20 or
|
||||
newer. No global Python package or system component is installed.
|
||||
Prerequisites are the repository-local Python environment and Node.js 20.19+ or
|
||||
22.12+. No global Python package or system component is installed.
|
||||
|
||||
```bash
|
||||
uv sync --group dev
|
||||
cd apps/k1-viewer
|
||||
npm install
|
||||
npm run typecheck
|
||||
npm run build
|
||||
cd ../..
|
||||
uv run k1link serve
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:8000`. `k1link serve` intentionally exposes no LAN bind
|
||||
option because its provisioning endpoint temporarily receives a Wi-Fi password.
|
||||
The password is accepted only in the POST body, is never logged or persisted by
|
||||
the connector, and is cleared from the React form after success.
|
||||
Open `http://127.0.0.1:8000`. The static Control Station, REST API, WebSocket
|
||||
state channel and credential endpoint bind to loopback. The Rerun gRPC server is
|
||||
a separate listener with a different network boundary described under
|
||||
[Network and security boundary](#network-and-security-boundary).
|
||||
|
||||
## Replay the captured proof
|
||||
## Replay a reviewed capture
|
||||
|
||||
1. In **Session**, select **Replay capture**.
|
||||
2. Enter the repository-relative path:
|
||||
1. In **Парк → Локальное устройство**, choose the replay mode.
|
||||
2. Enter a native `mqtt.raw.k1mqtt` capture or a reviewed four-column TSV,
|
||||
for example:
|
||||
|
||||
```text
|
||||
sessions/20260715T122850Z_live_power_cycle/captures/mqtt_scan_full_payloads_03.tsv
|
||||
```
|
||||
|
||||
3. Use speed `1` and enable loop for initial viewer setup.
|
||||
4. Start replay, then select **Open Foxglove 3D**.
|
||||
5. In a Foxglove 3D panel, enable `/k1/points`, `/k1/pose` and
|
||||
`/k1/trajectory`.
|
||||
6. For the cloud, choose `intensity` as the color field and a Turbo/Rainbow
|
||||
colormap or custom two-color gradient. Foxglove can also color by `x`, `y`,
|
||||
`z` and its derived `<distance>`.
|
||||
3. Select replay speed and optional looping, then start the session.
|
||||
4. The runtime starts the process-wide Rerun `RecordingStream` and its
|
||||
gRPC/proxy server on TCP 9876 before the first replay. Later sessions reuse it.
|
||||
5. Open **Наблюдение → Пространственная сцена**. With the manual source field
|
||||
empty, the embedded viewer automatically uses:
|
||||
|
||||
The reviewed TSV reader accepts exactly four columns: receive timestamp, topic,
|
||||
declared payload length and hex payload. It verifies timestamp, UTF-8 topic,
|
||||
declared length, hex encoding and allocation bounds. Native
|
||||
`mqtt.raw.k1mqtt` captures are supported directly and use their sibling metadata
|
||||
timestamps when present.
|
||||
```text
|
||||
rerun+http://127.0.0.1:9876/proxy
|
||||
```
|
||||
|
||||
6. Use **Отображение** and **Слои** to adjust the real cloud and trajectory.
|
||||
7. Stop the session when finished. Acquisition and session metrics stop, while
|
||||
the embedded viewer and `rerun_grpc_url` remain ready. A later session resets
|
||||
scene-local state and reuses the same listener without reloading the page.
|
||||
|
||||
The TSV reader accepts exactly receive timestamp, topic, declared payload length
|
||||
and hexadecimal payload. It verifies timestamp, UTF-8 topic, declared length,
|
||||
hex encoding and allocation bounds. Native `.k1mqtt` captures are supported
|
||||
directly and use their sibling metadata receive timestamps when present.
|
||||
|
||||
## Connect and stream live
|
||||
|
||||
1. Power K1 to its normal steady-green standby state.
|
||||
2. Confirm the manual power checklist in **Connect**.
|
||||
3. Run the real six-second BLE scan and select the K1 candidate.
|
||||
4. Enter the existing router SSID/password and press **Provision Wi-Fi &
|
||||
connect**. That button is the explicit authorization for one reviewed 99-byte
|
||||
provisioning write; the backend never retries automatically.
|
||||
5. When K1 reports a non-AP private address, press **Start live stream**.
|
||||
6. Open Foxglove before scanning if convenient.
|
||||
7. Double-click the physical K1 button to start scanning. Double-click again to
|
||||
stop, wait for the LED to return to steady green, then stop the local session.
|
||||
2. Confirm the manual power checklist in **Парк → Локальное устройство**.
|
||||
3. Run the real six-second BLE scan and select the intended device from the
|
||||
complete visible-device list.
|
||||
4. Enter the existing router SSID/password and explicitly authorize the reviewed
|
||||
provisioning write. The backend does not retry the write automatically.
|
||||
5. When K1 reports a non-AP private address, start live reception.
|
||||
6. Wait until the UI reports that the local Rerun bridge is ready. No manual
|
||||
viewer URL is needed.
|
||||
7. Open **Наблюдение → Пространственная сцена**.
|
||||
8. Double-click the physical K1 button to start scanning. Real point frames and
|
||||
pose/trajectory updates then appear in the embedded viewport.
|
||||
9. Double-click K1 again to stop physical scanning, wait for steady green, then
|
||||
stop the local session so captures and summaries are finalized.
|
||||
|
||||
Each live run creates an ignored `sessions/<UTC>_viewer_live/` directory with a
|
||||
redacted manifest, operator notes, raw MQTT frames, per-message metadata and a
|
||||
hash summary. No application request topic is published.
|
||||
hash summary. The connector subscribes to the fixed report-topic allowlist and
|
||||
does not publish an application request or modeling command.
|
||||
|
||||
## Published topics
|
||||
## Automatic Rerun source and lifecycle
|
||||
|
||||
| Topic | Foxglove schema | Meaning |
|
||||
On the first live or replay session, `RerunBridge`:
|
||||
|
||||
- creates an explicit `RecordingStream("nodedc_device_spatial")`;
|
||||
- installs the default spatial blueprint;
|
||||
- starts the gRPC/proxy server on TCP 9876 with a 512 MiB late-client buffer;
|
||||
- reports `rerun+http://127.0.0.1:9876/proxy` only after the server is ready;
|
||||
- accepts the local development and production browser origins used by this
|
||||
repository;
|
||||
- keeps the recording and URL alive until `k1link serve` shuts down.
|
||||
|
||||
At every session start it resets the trajectory, current point count, metrics,
|
||||
blueprint and session-local visible time, then feeds the new source through the
|
||||
same recording. This process-wide lifecycle is intentional: the Rerun 0.34.1
|
||||
browser receiver can remain connected after canvas teardown, so restarting the
|
||||
native listener on the same port is not a reliable session boundary.
|
||||
|
||||
The frontend prefers an operator-entered source when one is present; otherwise
|
||||
it automatically uses `rerun_grpc_url` from backend state. A manual field can
|
||||
still open another compatible `rerun+http://.../proxy` source or an RRD file
|
||||
served over HTTP(S). No externally hosted viewer UI is involved:
|
||||
`@rerun-io/web-viewer` and its WebAssembly runtime are bundled with the local
|
||||
application. FastAPI carries control state only; the browser reads the point
|
||||
stream directly from the Rerun gRPC/proxy endpoint.
|
||||
|
||||
## Rerun entities
|
||||
|
||||
| Entity | Rerun archetype | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `/k1/points` | `foxglove.PointCloud` | metric XYZ float32 + uint8 intensity, stride 16 |
|
||||
| `/k1/pose` | `foxglove.PoseInFrame` | current decoded K1 pose in the `map` frame |
|
||||
| `/k1/trajectory` | `foxglove.SceneUpdate` | bounded cyan line strip, throttled while growing |
|
||||
| `/k1/metrics` | JSON schema | latency, decode/publish time, FPS, points and drops |
|
||||
| `/world` | `ViewCoordinates` | right-handed Z-up display convention |
|
||||
| `/world/points` | `Points3D` | decoded metric XYZ with computed or available RGB colors |
|
||||
| `/world/sensor_pose` | `Transform3D` + `TransformAxes3D` | current decoded translation, xyzw quaternion and pose axes |
|
||||
| `/world/trajectory` | `LineStrips3D` | bounded path of up to 20,000 decoded poses |
|
||||
|
||||
Point coordinates remain `(x/scaler, y/scaler, z/scaler)` exactly as verified.
|
||||
No axis swap, quaternion normalization or vehicle extrinsic is silently applied.
|
||||
Only the low byte of `rgbi` is labeled intensity; interpreting the upper bytes
|
||||
as RGB remains unverified.
|
||||
Point count, frame rate, queue drops and measured pipeline time remain product
|
||||
metrics in the outer Control Station; they are not logged as Rerun entities and
|
||||
therefore cannot create additional automatic viewer panes.
|
||||
|
||||
## Latency semantics
|
||||
Point coordinates remain `(x/scaler, y/scaler, z/scaler)` exactly as decoded.
|
||||
No axis swap, quaternion normalization, scanner-to-vehicle extrinsic or SLAM
|
||||
post-processing is silently applied. The point entity is in `/world` rather
|
||||
than under the pose transform, so an already map-frame cloud is not transformed
|
||||
twice. The physical K1 axes and a scanner-to-vehicle transform still require
|
||||
calibration before vehicle mounting.
|
||||
|
||||
`pipeline_ms` / `mqtt_to_publish_ms` is measured with the host monotonic clock
|
||||
from MQTT callback receipt through raw disk write, bounded queue wait, decode,
|
||||
packing and Foxglove channel publish. Rolling p50/p95 values use the last 512
|
||||
published decoded messages.
|
||||
Only the low byte of firmware-3 `rgbi` has been verified as intensity. The
|
||||
upper bytes are not labeled RGB. Legacy frames use RGB only when those decoded
|
||||
fields are actually present.
|
||||
|
||||
This is the exact Mac pipeline latency, not yet sensor-photon-to-screen latency.
|
||||
The K1 header timestamp epoch has not been proven, Foxglove rendering time is
|
||||
outside the Python publisher, and display latency is not observable without a
|
||||
clock-correlated device timestamp or high-speed-camera experiment. Foxglove
|
||||
message time therefore uses host receive Unix time while durations use
|
||||
`monotonic_ns`.
|
||||
## Scene controls
|
||||
|
||||
The current product controls call `POST /api/viewer/settings`; backend state is
|
||||
authoritative and the bridge applies the updated settings to subsequent
|
||||
messages.
|
||||
|
||||
| Control | Implemented behavior |
|
||||
| --- | --- |
|
||||
| Point size | Rerun UI-point radius from 0.5 to 12.0; default 2.5 |
|
||||
| Color attribute | intensity, height Z, distance from the point-coordinate origin, available RGB, or solid class/custom color |
|
||||
| Palette | Turbo, Viridis, Plasma, grayscale or operator-selected custom color |
|
||||
| Accumulation | sliding session-local `stream_time` window from 0 to 120 seconds; default 12 seconds |
|
||||
| Point layer | shows or clears `/world/points` |
|
||||
| Trajectory | shows or clears the decoded path; publication is throttled while it grows |
|
||||
| Grid | updates visibility of the Rerun 3D line grid through the active blueprint |
|
||||
|
||||
The default 12-second accumulation is a viewer time range over real point
|
||||
frames; it does not duplicate or alter the raw capture. An accumulation of zero
|
||||
shows only the current frame. RGB mode falls back to the verified scalar
|
||||
coloring path when the active point format has no RGB fields. The current
|
||||
`class`/custom behavior is a selected solid color, not semantic segmentation.
|
||||
|
||||
Projection switching, a custom playback timeline, semantic object/mask layers,
|
||||
camera frustums and persisted RBL/layout profiles are not wired yet and must not
|
||||
be inferred from the implemented controls above.
|
||||
|
||||
## Time and latency semantics
|
||||
|
||||
The Rerun `capture_time` timeline uses
|
||||
`StreamMessage.received_at_epoch_ns`: Unix time when the Mac received the MQTT
|
||||
message. The K1 header timestamp epoch is not proven, so `capture_time` is not
|
||||
a sensor acquisition timestamp. The viewer accumulation window uses
|
||||
`stream_time`, assigned when the current live/replay run publishes a message;
|
||||
therefore replayed historical timestamps do not mix two sequential sessions.
|
||||
|
||||
For live MQTT only, `pipeline_ms` / `mqtt_to_publish_ms` uses the Mac monotonic
|
||||
clock from MQTT callback receipt through raw disk write, bounded queue wait,
|
||||
decode, Rerun packing and the SDK log call. Replay and idle states expose no
|
||||
latency value. Browser fetch, WebAssembly ingestion, GPU rendering and display
|
||||
scanout are outside this measurement.
|
||||
|
||||
The displayed value is therefore Mac pipeline latency, not sensor-photon-to-
|
||||
screen latency. End-to-end display latency requires a proven device clock or a
|
||||
separate clock-correlated/high-speed-camera experiment.
|
||||
|
||||
## Network and security boundary
|
||||
|
||||
There are two different listeners:
|
||||
|
||||
- FastAPI, the WebSocket control plane and Wi-Fi credential endpoint bind only
|
||||
to `127.0.0.1:8000`;
|
||||
- Rerun gRPC/proxy on TCP 9876 currently binds **all network interfaces**, even
|
||||
though the URL returned to the local browser contains `127.0.0.1`.
|
||||
|
||||
The Rerun endpoint has no connector-level authentication and no TLS. It may
|
||||
expose point clouds, trajectory and mapped interiors to any host that can reach
|
||||
the port. Use it only on a trusted laboratory LAN, block TCP 9876 at the router
|
||||
and host firewall as appropriate, and never forward it directly to the public
|
||||
Internet or cellular WAN. Remote operation requires an authenticated TLS reverse
|
||||
proxy or another reviewed secure transport before deployment.
|
||||
|
||||
The 512 MiB Rerun server buffer limits retained late-client data but is not an
|
||||
access-control mechanism. The listener and embedded viewer intentionally remain
|
||||
ready between acquisition sessions. Stop `k1link serve` when the network
|
||||
listener and its process memory must be closed unconditionally.
|
||||
|
||||
## Current boundaries
|
||||
|
||||
- Raw panoramic camera frames were not present on the observed MQTT report
|
||||
topics; this milestone intentionally ships point cloud plus trajectory only.
|
||||
- Physical double-click remains the start/stop control. A future MQTT modeling
|
||||
- Raw panoramic camera frames were absent from the observed MQTT report topics.
|
||||
This milestone contains point cloud and pose/trajectory; operational metrics
|
||||
are rendered by the outer Control Station.
|
||||
- Physical double-click remains the K1 scan start/stop control. Any MQTT command
|
||||
publisher needs a separately reviewed state-changing profile.
|
||||
- The React application opens the official Foxglove viewer over a local
|
||||
WebSocket. It does not embed or fork the proprietary modern viewer; Foxglove
|
||||
account/seat terms apply to that viewer independently of this connector.
|
||||
- Exact path axes and a scanner-to-vehicle transform must be calibrated before
|
||||
mounting on the unmanned platform.
|
||||
- No terrain map, elevation model, obstacle segmentation, localization fusion,
|
||||
mission planner or vehicle control is implemented by this viewer milestone.
|
||||
- Exact coordinate axes and the scanner-to-vehicle transform remain a mounting
|
||||
calibration task.
|
||||
- A single local runtime owns TCP 9876. A port collision causes a visible bridge
|
||||
startup error rather than silently selecting another source. The Rerun native
|
||||
listener lives for the lifetime of `k1link serve` and is reused by later
|
||||
sessions in that process; shutdown closes it explicitly.
|
||||
|
||||
## Verification checkpoint — 2026-07-15
|
||||
## Verification checkpoint — 2026-07-16
|
||||
|
||||
The full captured 180-second scan was passed through the production Foxglove
|
||||
bridge without a viewer-side substitute:
|
||||
A powered K1 capture was passed through the active MQTT → decoder → Rerun gRPC →
|
||||
embedded Web Viewer path without a viewer-side substitute:
|
||||
|
||||
| Check | Result |
|
||||
| --- | ---: |
|
||||
| MQTT messages read | 2,836 |
|
||||
| `lio_pcl` frames published | 1,140 |
|
||||
| `lio_pose` frames published | 1,215 |
|
||||
| points packed and published | 4,165,862 |
|
||||
| real MQTT messages processed | 80 |
|
||||
| `lio_pcl` frames published | 38 |
|
||||
| `lio_pose` frames published | 42 |
|
||||
| points in the last cloud | 2,775 |
|
||||
| decoder errors | 0 |
|
||||
| final trajectory poses | 1,215 |
|
||||
|
||||
The same replay was then started through the FastAPI/React contract at `10x`.
|
||||
The API exposed the loopback Foxglove URL, live point/pose metrics and non-zero
|
||||
preview-drop accounting under deliberate acceleration, and released TCP 8765
|
||||
after the stop request. This validates replay, overload behavior and lifecycle;
|
||||
the next checkpoint is the powered K1 ideal-LAN live run.
|
||||
The 38 point frames and 42 pose frames account for all 80 observed messages.
|
||||
This proves current real-device decoding, bridge publication and embedded-viewer
|
||||
delivery for point cloud plus trajectory. It does not prove a camera channel or
|
||||
sensor-to-screen latency.
|
||||
|
||||
## Legacy Foxglove regression module
|
||||
|
||||
`src/k1link/viewer/foxglove_bridge.py` and its tests remain in the repository
|
||||
to compare decoder/packing behavior and preserve earlier evidence. They are not
|
||||
instantiated by `VisualizationRuntime`, are not the Control Station source, and
|
||||
do not make TCP 8765 part of the current operator path.
|
||||
|
||||
Reference in New Issue
Block a user