# K1 live console and embedded Rerun bridge

Status: the Rerun live and saved-session milestones are implemented for the
verified firmware-3 point/pose streams. Live and adapter file-replay use the
process-wide gRPC source; saved observation sessions use private digest-bound
RRD over same-origin HTTP. Left/right RTSP preview is delivered separately from
the spatial Rerun stream. The application does not generate placeholder cloud,
trajectory, camera frame or latency values.

## Active device-to-scene path

```text
K1 lio_pcl / lio_pose / ModelingReport
          |
          v
read-only MQTT subscription on TCP 1883
          |
          +--> raw .k1mqtt + JSONL + durable clock origin (before camera/preview)
          +--> transport/session envelope + SHA-256 summary (shutdown/seal)
          |
          +--> injected K1 ModelingReport observer
          |      `--> device scan time / distance / speed product metrics
          |
          v
bounded latest-wins visual preview queue (4 messages)
          |
          v
reviewed raw-LZ4/protobuf point/pose normalizer
          |
          v
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 Mission Core Control Station

React console <-- REST + WebSocket state --> FastAPI on 127.0.0.1:8000
```

The Paho MQTT callback does not decode or render the point cloud. It first
appends the raw frame, flushes it to the operating-system page cache and stages
its aligned metadata, then enqueues a preview message. Raw bytes and metadata
become a durable pair at the bounded group-commit boundary (at most 0.5 s,
4 MiB or 32 messages), not at every callback. If visualization cannot keep up,
the oldest queued preview is discarded while capture continues. Rerun work
stays on the dedicated publisher thread and cannot block that raw-first path.
The four-message queue size is independent of the 32-message evidence group
commit. A plugin-injected observer consumes bounded K1 `ModelingReport` messages
after raw persistence but before visual admission, so status telemetry cannot
evict a point-cloud or pose preview.

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.19+ or
22.12+. No global Python package or system component is installed.

```bash
uv sync --group dev
cd apps/control-station
npm ci
npm run typecheck
npm run build
cd ../..
uv run k1link serve
```

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 a reviewed capture

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. 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:

   ```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 **Парк → Локальное устройство**.
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. Enter the required project name, confirm operator presence, closed LixelGO,
   storage/power and steady green, then choose **Запустить сканирование и
   локальный приём** once.
6. Mission Core emits operations 1–6, waits for their correlated device
   responses, then performs operation 7 and operations 8–10 behind the same
   operator intent. No captured human pause is treated as a timer and no device
   command is automatically retried.
7. Mission Core prepares evidence reception before emitting the single
   canonical START. The project name is carried only by that START; there is no
   separate K1 project-name write. Mission Core opens
   **Наблюдение → Пространственная сцена** and shows calibration while the same
   control socket waits for `SCANNING + project + init_ready`.
8. Real point frames and pose/trajectory updates then appear in the embedded
   viewport. Do not use the physical button or open LixelGO during this cycle.
9. When K1 emits `ModelingReport`, the same plugin block shows its current scan
   time, route distance and speed. These are device reports, not values inferred
   from a browser timer or accumulated across a device counter reset.
10. Choose **Остановить устройство и запись** in the scene. The
    plugin-commanded path emits one canonical STOP on the original socket,
    waits for live unbound READY, then finalizes capture, camera archive and
    summaries automatically. The UI does not ask for a redundant green-indicator
    confirmation because READY plus cleared project binding is already the
    scanner's protocol evidence.

Each new live run creates a direct child below `MISSIONCORE_EVIDENCE_DIR`, or
`.runtime/mission-core/evidence/sessions/` by default, with raw MQTT frames,
per-message metadata and a hash summary. Repository-level
`sessions/*_viewer_live` is legacy import-only evidence and is never selected by
the current writer. The data connector remains subscribe-only. Application
requests are isolated in the separate canonical control-session owner and can
be emitted only after the explicit launch or STOP intent and the corresponding
live response gate.

The recovered command substrate byte-matches the ten
observed pre-START requests plus START/STOP, correlates DeviceInfo and modeling
responses, and classifies live device states before any command transition. One
private application-level OpenAPI authority is kept
separate from the transient vendor ID/serial returned by the BLE-selected K1.
The fixed Keychain loader and dormant shadow coordinator remain available for
inspection. Plugin v0.5.0 additionally installs a continuous interactive MQTT
owner: one UI launch intent advances operations 1–6, 7, 8–10 and START through
their live response barriers; STOP remains a separate explicit action. Physical START/STOP and
durable native-project completion still require operator acceptance.

## Automatic Rerun source and lifecycle

On the first live or adapter file-replay session, `RerunBridge`:

- creates an explicit `RecordingStream("nodedc_mission_core_spatial")`;
- installs the default spatial blueprint;
- starts the gRPC/proxy server on TCP 9876 with a 32 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.

`activateAutomaticSpatialSource` is a generic host action, not a scanner-control
command. A plugin invokes it only after its live or adapter-replay start returns
success, then opens the spatial scene; a failed start preserves the old scene
and source. The successful action releases any host-selected archive/manual URL
so the new backend source becomes authoritative. After an acquisition becomes
nonterminal, a fail-closed host guard blocks saved-session switching, persisted
replay reattach and manual RRD/Rerun source input/application/reset until the
acquisition ends. This successful-start transition intentionally does not
consult the operator-switch guard, and the guard never stops equipment.

## Rerun entities

| Entity | Rerun archetype | Meaning |
| --- | --- | --- |
| `/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 2,000 decoded poses |

Point count, frame rate, queue drops and measured pipeline time remain product
metrics in the outer Control Station. Live K1 scan time, route distance and
speed also remain product metrics in the plugin-owned scene block. Saved-session
export separately logs those three device-reported values below
`/metrics/device` as Rerun scalar time series; this does not add vendor parsing
to the generic live bridge.

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.

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.

## 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, semantic object/mask layers and camera frustums are not
wired yet. The host-owned `observation.spatial` layout profile does persist the
implemented scene controls, tool-window state, dynamic source visibility and
normalized floating-window geometry. It is separate from Rerun's internal
blueprint and from observation evidence.

## Saved observation sessions

Every completed native live run is indexed by the local host session store. A
valid crash prefix without its final summary is indexed as `interrupted`. The
**Сохранённые сессии** control shows the three newest runs and launches an
opaque same-origin RRD rather than starting a second live bridge.

Sealing, recovery or legacy import makes the session eligible for the bounded
backend preparation worker. The worker reads every native message once and
atomically publishes a private cache-v9 RRD using the zero-based `session_time`
duration timeline. New sessions bind that zero/end to the durable capture-clock
envelope; older valid evidence retains the first/last-message compatibility
fallback. A SHA sidecar binds the generation to native evidence.
Replay-open never performs conversion: it returns HTTP 202 and a preparation
handle or reuses the verified artifact. The embedded viewer opens the exact
generation through Rerun's native incremental HTTP receiver. The bottom Control
Station timeline then controls play/pause and seek directly; it does not
approximate time with a React timer.

The native `.k1mqtt`, aligned metadata and capture-clock envelope remain the
spatial evidence master. RRD generation does not use the bounded live-preview
queue, so it retains every point/pose frame accepted by the reviewed normalizer
and logs every valid `ModelingReport` as distance, speed and elapsed-time scalar
rows. See
[`09_OBSERVATION_SESSIONS.md`](09_OBSERVATION_SESSIONS.md) for storage,
recovery and HTTP Range details.

## Time and latency semantics

The Rerun `capture_time` timeline uses Mac host-clock epoch values. Data rows use
`StreamMessage.received_at_epoch_ns`, while new archived sessions also contain a
real origin row at the capture-envelope start and a real end row at the sealed
session completion. Their zero-based `session_time` is derived from the same
monotonic envelope; it can therefore cover a camera fragment before the first
MQTT packet or after the last spatial packet without inventing an RRD range.
Legacy evidence without an envelope falls back to first/last message bounds.
The K1 header timestamp epoch is not proven, so `capture_time` is not a sensor
acquisition timestamp. The live 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.

Retained read-only physical captures support the profile-scoped telemetry units:
in five of six runs, `ScanTime / 2` tracks host monotonic duration within about
0.17 seconds across 76–2,294-second spans; the remaining older run contains a
counter/distance reset and is treated as a new scan generation. In two moving
runs, `ModelingReport.MoveDistance` matches the independent
`lio_pose.distance` values at 41.521 m and 26.631 m, while reported peak speeds
of 1.382 m/s and 1.291 m/s are physically plausible. This evidence does not turn
the counters into a cross-firmware contract.

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 32 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 absent from the observed MQTT report topics.
  The Rerun spatial recording contains point cloud and pose/trajectory;
  left/right compressed RTSP preview and acquisition-owned fMP4 archive use a
  separate generic media path. Historical sessions predating that archive have
  no recoverable video.
- Physical double-click remains the independently verified device-local fallback.
  Mission Core also contains the installed response-gated operator-present
  acceptance transport for the exact K1/A4/FW 3.0.2 profile; its full
  START-to-STOP durable-save cycle was physically accepted on 2026-07-18/19.
- 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-16

A powered K1 capture was passed through the active MQTT → decoder → Rerun gRPC →
embedded Web Viewer path without a viewer-side substitute:

| Check | Result |
| --- | ---: |
| 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 |

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 archived playback
of one selected camera together with the point cloud or sensor-to-screen
latency.

## Legacy Foxglove regression module

`src/k1link/device_plugins/xgrids_k1/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.
