feat(k1): complete primary acquisition lifecycle

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 23:03:59 +03:00
parent 9d51080d2e
commit aa3680948f
66 changed files with 6093 additions and 544 deletions
+46 -15
View File
@@ -15,27 +15,55 @@ Each gate produces evidence and an explicit GO, PAUSE or BLOCKED result.
| Stage 4 artifacts/flows | GO — bounded capture, hashes and negative control |
| Stage 5 point cloud | GO — raw-LZ4 protobuf, 1,140 live frames decoded |
| Stage 5 pose | GO — 1,215 live frames decoded and motion-correlated |
| Stage 5 modeling telemetry | GO (read-only) — bounded `ModelingReport` decoder and device-reported scan time/distance/speed are integrated before the preview queue |
| Stage 5 camera | GO (live) — left/right RTSP/H.264 preview observed, read-only runtime adapter and physical UI acceptance completed |
| Stage 6 live viewer | GO — React Control Station, embedded self-hosted Rerun cloud/trajectory and Mac pipeline metrics |
| Stage 7 observation archive | GO (point/pose) — durable catalog, recovery, background RRD preparation, saved-session timeline and atomic playback verified |
| Stage 7 recorded cameras | GO (contract), acceptance pending — acquisition-owned fMP4 archive and player are implemented/tested; no real archived K1 camera session exists yet |
| Plugin isolation | GO (laboratory control plane) — vendor backend/frontend are plugin-owned; manifest/runtime descriptor parity, versioned handshake, lifecycle health and transport correlation fail closed while execution remains in-process |
| Stage 6 live viewer | GO — React Control Station, embedded self-hosted Rerun cloud/trajectory, plugin-owned spatial controls and live device/Mac metrics |
| Stage 7 observation archive | GO (point/pose/telemetry contract) — durable catalog, recovery, capture-clock-bounded RRD preparation, archived metric time series, saved-session timeline and atomic playback are implemented |
| Stage 7 recorded cameras | GO (contract), acceptance pending — acquisition-owned fMP4 archive and player are implemented/tested; one real archived K1 camera plus point-cloud session has not passed playback yet |
| Plugin isolation | GO (laboratory control plane) — vendor backend/frontend and optional scene controls are plugin-owned; manifest/runtime descriptor parity, versioned handshake, lifecycle health and transport correlation fail closed while execution remains in-process |
| Stage 8 product storage | PAUSE — retention, replication, encryption, capacity monitoring and long-run browser/WASM stress remain deployment gates |
USB project copying remains optional ground truth rather than a blocker for the
now-verified network path. Owner-operated LixelGO traffic verifies the MQTT
start/stop mapping and RTSP camera transport. MQTT control publishing remains
deliberately deferred because complete request/save/rollback semantics are not
yet modeled and the physical button remains a known-safe fallback.
start/stop mapping and RTSP camera transport. The exact start/stop protobuf
encoder, response correlator and device-status state machine now exist without a
publisher. Header construction is known: an explicit ASCII device ID and ASCII
OpenAPI key are required, while the session ID is derived exactly as
`${device_id}:ModelingRequest`. MQTT control publishing remains deliberately
disabled because legitimate OpenAPI credential provenance/provisioning and a
durable post-stop save gate are not proven. The physical button remains the
known-safe fallback.
The Stage 6 live path uses a bounded raw-first bridge: loss in the visualization
queue cannot discard MQTT evidence. Stage 7 adds an independent durable
observation-session lifecycle. Completed or recovered native captures are
prepared once by a bounded backend worker into digest-bound RRD/cache-v7 and
camera-manifest generations; a browser replay request never performs conversion.
The saved scene, controller and timeline remain hidden until the complete RRD
range and every declared camera pass admission. Sensor-to-display latency and
camera/LiDAR sensor-clock alignment remain separate correlation tests.
queue cannot discard MQTT evidence. The queue holds four preview messages;
`ModelingReport` is consumed by a plugin-injected observer before that queue and
does not compete with point/pose frames. A project name is mandatory at
preparation, NFKC-normalized/trimmed on both sides, rejected for control
or surrogate characters or more than 96 characters, and retained only as
display metadata.
Stage 7 adds an independent durable observation-session lifecycle. New native
captures durably publish the clock origin before camera production, retain a
transport-scoped provisional envelope and atomically switch the capture-summary
pointer to a content-addressed session envelope after all producers stop.
Completed or recovered captures are prepared once by a bounded backend worker
into digest-bound RRD/cache-v9 and camera-manifest generations;
a browser replay request never performs conversion. The RRD materializes real
origin/end rows plus archived device-reported distance, speed and scan-time
series. The saved scene, controller and timeline remain hidden until the
complete RRD range and every declared camera pass admission. Sensor-to-display
latency and camera/LiDAR sensor-clock alignment remain separate correlation
tests.
The generic host clears a prior manual or archived spatial source only after a
plugin-owned live/file-replay start succeeds; a failed start preserves the
current scene. Once an acquisition is nonterminal, a fail-closed guard blocks
saved-session switching, persisted replay reattach and manual source
input/apply/reset until that acquisition has been finalized. The successful
automatic-source action is an internal start-result boundary and intentionally
does not masquerade as an operator source switch. The K1 scene-level stop
control is plugin-owned; with the active read-only profile it stops and seals
local reception only and never claims to stop the scanner.
## Stage 0 — repository and host baseline
@@ -221,6 +249,9 @@ useful stream is decoded or structurally identified.
- automated scan-button electronics;
- OpenWrt/monitor-mode infrastructure;
- firmware or internal-Linux analysis;
- physical end-to-end replay acceptance for newly archived left/right cameras;
- physical end-to-end shared-timeline playback for a newly archived session
containing point cloud plus one selected K1 camera;
- legitimate K1 OpenAPI credential provisioning, authorized command transport
and stable-artifact proof after the observed stop lifecycle;
- long-running large-session WebViewer/WASM memory telemetry;
- production retention, replication, encryption and cross-platform packaging.
+32 -9
View File
@@ -129,27 +129,46 @@ orientation is exposed as `(x,y,z,w)`. Unknown tail fields are retained.
`PrePathArray` is exactly 16 little-endian float64 values.
## Modeling telemetry
`lixel/application/report/modeling` contains a bounded `ModelingReport`. Field 2
is PGO progress; field 3 is a nested scan status containing float32 move
distance, float32 move speed and an int64 scan-time counter. Retained physical
captures correlate that counter at exactly two ticks per second. Mission Core
consumes this status before the disposable preview queue and exposes the current
device-reported scan generation as elapsed seconds, route metres and metres per
second. When scan time and distance reset together, the old route is not added
to the new generation.
## Application control boundary
Static analysis first identified the application start/stop topic as
`lixel/application/request/modeling`, QoS 2, with a protobuf
`ModelingRequest`. Start action is 1 and stop action is 2. A valid request also
contains a device/session/OpenAPI header and, for start, project/record/scan/mount
settings.
`ModelingRequest`. Start action is 1 and stop action is 2. The retained header
contains explicit device identity and OpenAPI value; its session relation is
literally `{device_id}:ModelingRequest`, not a caller-selected session. The
retained start request carries an operator project name, record mode 2, scan
mode 1 and mount mode 0; the retained stop request carries only header and
action. QoS is 2, retain is false, and correlated success uses numeric result
`302252033` with the same header identity and action.
Lab 002 then observed both mappings in owner-operated LixelGO traffic and
correlated them with device success responses and physical start/stop of the
high-rate streams. This promotes the mapping from a static hint to descriptive
wire evidence; it does not authorize replay or publishing.
Publishing is deliberately not implemented. The physical double-click provides
a verified autonomous start/stop path and avoids inventing session headers or
changing scan settings. A future publisher requires a separate reviewed profile,
explicit confirmation, response handling and rollback.
The repository now contains an inert bounded encoder/response parser and a
fail-closed device-status state machine for this exact profile. They have no
MQTT publish dependency. Publishing remains deliberately disabled: the OpenAPI
credential's provenance and secure provisioning are unresolved, and the
complete `ScanOver`/ready/stable-artifact sequence has not physically proved a
durable save result. The verified physical double-click remains the acquisition
control until a separate reviewed write gate closes.
## Decoder and capture bounds
## Decoder and raw-capture bounds
Default defensive limits are applied before allocation or iteration:
The semantic decoder applies these default defensive limits before allocation
or iteration:
- 2 MiB maximum MQTT payload;
- 1 MiB maximum compressed block;
@@ -158,6 +177,10 @@ Default defensive limits are applied before allocation or iteration:
- 250,000 points per frame;
- bounded protobuf field counts and exact LZ4 decoded length.
The raw-first evidence recorder has an independent 64 MiB default per-message
limit, configurable only up to 256 MiB. That larger transport ceiling does not
raise any semantic decoder allocation bound.
Per-frame failures do not justify firmware writes or speculative recovery.
Always preserve the raw MQTT record and report the decoder error separately.
+88 -26
View File
@@ -10,18 +10,22 @@ trajectory, camera frame or latency values.
## Active device-to-scene path
```text
K1 lio_pcl / lio_pose
K1 lio_pcl / lio_pose / ModelingReport
|
v
read-only MQTT subscription on TCP 1883
|
+--> raw .k1mqtt + JSONL + SHA-256 summary (written first)
+--> 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 preview queue (32 messages)
bounded latest-wins visual preview queue (4 messages)
|
v
reviewed raw-LZ4/protobuf decoders
reviewed raw-LZ4/protobuf point/pose normalizer
|
v
Rerun Points3D + Transform3D + LineStrips3D
@@ -44,6 +48,10 @@ 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
@@ -107,14 +115,26 @@ directly and use their sibling metadata receive timestamps when present.
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
5. Enter the required project name. Mission Core NFKC-normalizes and trims it,
rejects control/surrogate characters or more than 96 characters, and stores
it as local display metadata rather than a path. The read-only profile does
not send it to K1.
6. When K1 reports a non-AP private address, choose **Подготовить локальный
приём данных**. The stronger “initiate device work” wording appears only for
a future profile that actually authorizes a vendor write.
7. 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
8. Open **Наблюдение → Пространственная сцена**. The K1 plugin's optional scene
block shows the local preparation/acquisition phase and stop action.
9. 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.
10. 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.
11. Double-click K1 again to stop physical scanning and wait for steady green,
then choose **Остановить локальный приём** in the scene or device workflow
so capture, camera archive and summaries are finalized. That UI action does
not stop K1 on the active profile.
Each new live run creates a direct child below `MISSIONCORE_EVIDENCE_DIR`, or
`.runtime/mission-core/evidence/sessions/` by default, with raw MQTT frames,
@@ -123,6 +143,15 @@ per-message metadata and a hash summary. Repository-level
the current writer. The connector subscribes to the fixed report-topic
allowlist and does not publish an application request or modeling command.
The recovered command substrate is intentionally inert. It can encode exact
start/stop request bytes, correlate response identity/action/result and classify
observed device states without importing MQTT or publishing anything. The header
requires explicit ASCII `device_id` and OpenAPI key and derives the session ID
exactly as `${device_id}:ModelingRequest`. Legitimate OpenAPI key
provenance/provisioning and durable save completion after stop are unresolved,
so `vendor_writes_enabled` is false and the physical-button workflow remains
canonical.
## Automatic Rerun source and lifecycle
On the first live or adapter file-replay session, `RerunBridge`:
@@ -149,6 +178,16 @@ served over HTTP(S). No externally hosted viewer UI is involved:
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 |
@@ -159,8 +198,11 @@ stream directly from the Rerun gRPC/proxy endpoint.
| `/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; they are not logged as Rerun entities and
therefore cannot create additional automatic viewer panes.
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
@@ -210,28 +252,46 @@ 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-v7 RRD using the zero-based `session_time`
duration timeline. A SHA sidecar binds the generation to native evidence.
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` remains the evidence master. RRD generation does not use
the bounded live-preview queue, so it retains every frame accepted by the
reviewed normalizer. See
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
`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.
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 762,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,
@@ -272,7 +332,8 @@ listener and its process memory must be closed unconditionally.
separate generic media path. Historical sessions predating that archive have
no recoverable video.
- Physical double-click remains the K1 scan start/stop control. Any MQTT command
publisher needs a separately reviewed state-changing profile.
publisher needs a separately reviewed state-changing profile with legitimate
OpenAPI key provisioning and a proven durable-save completion gate.
- 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
@@ -297,8 +358,9 @@ embedded Web Viewer path without a viewer-side substitute:
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.
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
+66 -11
View File
@@ -15,7 +15,7 @@ and the bounded laboratory runtime seam in
| `apps/control-station/src/core/device-plugins/frontendSdk.ts` | Public React host surface for reviewed frontend contributions | Versioned frontend Plugin SDK package surface |
| `packages/plugin-sdk/` | Installable v0alpha2 Pydantic contracts and JSON Schema export | Portable host/plugin identity, lifecycle, stream and evidence boundary |
| `plugins/xgrids-k1/` | v1alpha2 manifest, exact profile/loader and plugin-owned React connection/acquisition UI | Independently versioned XGRIDS device plugin |
| `plugins/xgrids-k1/frontend/` | XGRIDS provisioning, acquisition/replay, diagnostics, metrics, runtime mapping and scoped CSS | Plugin-owned reviewed frontend contribution |
| `plugins/xgrids-k1/frontend/` | XGRIDS provisioning, project/acquisition/replay, optional spatial controls, diagnostics, metrics, runtime mapping and scoped CSS | Plugin-owned reviewed frontend contribution |
| `src/k1link/web/` | Local FastAPI host, fail-closed runtime handshake/transport seam, in-memory operation/acquisition lifecycle and generic session API | Generic host APIs plus isolated plugin supervisor |
| `src/k1link/device_plugins/xgrids_k1/` | Physically isolated K1 BLE/MQTT/protobuf/LZ4/camera/replay/CLI compatibility implementation | Independently built XGRIDS device plugin process |
| `src/k1link/data_plane/` | Transport-neutral decoded in-process consumer views | Local projections hydrated from portable SDK envelopes |
@@ -32,7 +32,8 @@ The current K1 live/replay data path is:
```text
raw K1 transport
-> raw-first evidence capture
-> bounded preview queue
-> injected K1 ModelingReport observer (device time/distance/speed)
-> bounded four-message visual preview queue
-> XGRIDS normalizer injected by the plugin facade
-> DecodedPointCloudView / DecodedPoseView
-> Rerun bridge
@@ -44,12 +45,25 @@ The host-owned recorded path is separate from the disposable live preview:
```text
sealed or recovered native observation session
-> SQLite catalog and bounded single-worker preparation
-> atomic RRD cache v7 + immutable recorded-media manifest v2
-> capture-clock-bound atomic RRD cache v9 + immutable recorded-media manifest v2
-> generation-bound same-origin HTTP
-> aggregate RRD/camera admission
-> native Rerun HTTP receiver + recorded fMP4 player
```
New K1 source generations durably publish
`captures/mqtt_live/mqtt.timeline.origin.json` before camera production. MQTT
shutdown adds provisional transport envelope `mqtt.timeline.json`; after camera
and MQTT/runtime shutdown, the acquisition owner publishes content-addressed
`mqtt.timeline.session-<sha256>.json` and atomically switches the capture-summary
pointer while still holding the session lease. Only that `session` scope is
eligible to advertise combined media replay. Cache v9 materializes both session
envelope endpoints as real RRD rows and logs archived device-reported scan time,
route distance and speed as scalar time series. A crash-recovered origin-only
point/pose prefix falls back to its last validated message and cannot advertise
camera media. Older valid sessions without the clock contract keep their
first/last-message fallback and cannot acquire camera evidence retroactively.
Rerun does not import K1 protocol code, inspect MQTT topics or receive raw
payloads. `VisualizationRuntime` cannot choose a vendor decoder implicitly; its
composition owner injects one. The local `Decoded*View` classes are in-process
@@ -73,7 +87,20 @@ selects a manifest model and mounts its `device.connection` component. Concrete
XGRIDS source lives under `plugins/xgrids-k1/frontend`, imports the host only via
`@mission-core/plugin-sdk`, and is connected by the single reviewed import in
`apps/control-station/src/composition/devicePlugins.ts`. BLE/Wi-Fi provisioning,
exact-profile confirmation and acquisition/replay UI are not Core components.
exact-profile confirmation, project metadata, acquisition/replay UI and the
optional K1 spatial-scene control block are not Core components. The host owns
only the optional slot contract and generic actions such as opening the scene or
releasing a prior manual/archive selection after a plugin source successfully
starts.
A generic fail-closed guard blocks manual source input/apply/reset,
saved-session switching and persisted replay reattach whenever the runtime
reports a nonterminal acquisition. Unknown future acquisition states block by
default. A plugin clears the previous host-selected source only after its start
returns success, preserving the current scene on failure. That internal
success-result transition is distinct from an operator switch. The guard does
not stop equipment or invent a lifecycle transition; the acquisition must
finish through its owning plugin.
## Experimental vocabulary, not Platform Ontology
@@ -115,6 +142,17 @@ transport mutation. Application-command publishing remains disabled; the
separately reviewed BLE Wi-Fi provisioning write retains its own explicit
operator gate.
An exact but inert K1 modeling-control substrate now lives inside the vendor
plugin. It encodes the recovered start/stop protobuf shapes, strictly correlates
response identity/action/numeric result and maps bounded device-status values
into an observation-only state machine. Header construction is no longer
guessed: callers provide explicit ASCII device ID and ASCII OpenAPI key, and the
session ID is derived exactly as `${device_id}:ModelingRequest`. No MQTT import
or publisher exists in that substrate. Legitimate OpenAPI key
provenance/provisioning and stable-artifact proof after the stop lifecycle are
still unresolved, so `vendor_writes_enabled=false` and every state-machine
snapshot remains false for durable save completion.
## Semantic lifecycle
The transitional facade now creates separate provisional device,
@@ -123,6 +161,20 @@ wait for receiver readiness, then wait for an external physical start, and becom
data, wait for an externally confirmed stop, finalize and complete. Capture-only
stop reports the K1 physical state as unknown.
Preparation requires a project name. The frontend and backend both apply NFKC
normalization plus surrounding-whitespace trimming, reject control/surrogate
characters and names above 96 Unicode characters, and store the result as
session/catalog display metadata. It is not a path component. Because command
publishing is off, the current integration does not claim that the name reaches
K1.
The K1 contribution may mount a `SpatialControlsView` beside the host-owned
viewport. It presents plugin lifecycle wording, the local stop action and live
`ModelingReport` scan time, route distance and speed. Under the current
capture-only profile the stop action seals local reception; it does not claim a
device stop. `ModelingReport` is consumed before the visual preview queue through
an injected observer, preserving the generic normalizer/Rerun boundary.
The operation journal is bounded and in memory. It records IDs, idempotency,
declared deadlines, progress and terminal results without action parameters or secrets.
It is not durable, distributed or recoverable after process restart.
@@ -140,8 +192,7 @@ independently of browser delivery. The generic recorded player, manifest-v2
validation and shared `session_time` controls are connected; historical sessions
created before this archive contract contain no recoverable video, so physical
recorded-camera acceptance remains open. Portable FFmpeg packaging, disk-backed
browser buffering, fan-out and remote delivery also remain open. Device status
and heartbeat remain raw observed channels without semantic decoders. Device
browser buffering, fan-out and remote delivery also remain open. Device
calibration command and sensor-to-vehicle extrinsics are unavailable.
The 2026-07-17 physical acceptance gate confirmed continuously updating point
@@ -149,7 +200,10 @@ clouds, a matching live trajectory, and both camera selections in the same
Mission Core acquisition. The embedded Rerun blueprint owns live-edge following
on `stream_time`; the React shell selects the timeline once and does not drive it
with a timer. Raw captures, camera frames, device identity and network details
remain outside Git. See [`ADR 0007`](adr/0007-k1-camera-preview-copy-remux-gateway.md)
remain outside Git. The bounded acquisition subset of `DeviceStatusReport` and
`ModelingReport` route telemetry are decoded for the exact profile; heartbeat
and nested system/RTK status payloads remain raw observed evidence. See
[`ADR 0007`](adr/0007-k1-camera-preview-copy-remux-gateway.md)
for the measured gate and remaining limits.
## Remaining extraction order
@@ -169,8 +223,9 @@ views.
4. Physically accept a newly archived left/right K1 session, then package the
read-only RTSP/H.264 adapter for each target OS, add disk-backed sealed media
caching and evolve same-host MSE delivery toward an authenticated Edge media
plane; keep the modeling-command publisher disabled until its separate safety
gate closes.
plane. Keep the modeling-command publisher disabled until legitimate OpenAPI
key provisioning and durable-save confirmation close its separate safety
gate.
Complex equipment will likely be assembled from separately useful component
plugins into configured hardware packs, while standalone equipment remains
@@ -209,8 +264,8 @@ single K1 vertical.
- remote Edge split, authenticated WAN relay and fleet orchestration;
- automatic K1 start/stop/calibration commands;
- device-reported camera capability discovery beyond the reviewed K1 profile;
- physical acceptance of shared-timeline recorded-camera playback on a newly
archived K1 session;
- physical acceptance of shared-timeline point-cloud plus one selected-camera
playback on a newly archived K1 session;
- frame-accurate camera/LiDAR calibration, panoramic stitching, disk-backed
browser media cache and remote multi-consumer delivery;
- production retention, replication, encryption and long-run WebViewer/WASM
+124 -41
View File
@@ -1,15 +1,19 @@
# Observation sessions, playback and workspace layout
Status: implemented for native K1 point/pose evidence and host-side camera
archival. Recorded spatial playback is exposed in the Mission Core observation
workspace. The camera archive/player contract is implemented and covered by
tests, but all retained real K1 sessions predate canonical camera archival.
Physical archived-camera playback therefore remains an open acceptance gate;
historical sessions contain no recoverable video.
archival. Valid K1 `ModelingReport` scan time, distance and speed are also
materialized as recorded Rerun time series. Recorded spatial playback is exposed
in the Mission Core observation workspace. The camera archive/player contract
is implemented and covered by tests, but all retained real K1 sessions predate
canonical camera archival. Physical shared-timeline playback of one selected
camera plus point cloud therefore remains an open acceptance gate; historical
sessions contain no recoverable video.
## Operator path
1. Start a normal acquisition from **Парк → Локальное устройство**.
1. Enter the required project name and start a normal acquisition from **Парк →
Локальное устройство**. The NFKC-normalized/trimmed value is local display
metadata, not a filesystem path, and becomes the saved-session catalog name.
2. Open **Наблюдение → Пространственная сцена**. Live point cloud, trajectory
and the selected camera remain live-only while acquisition is running.
3. Stop acquisition normally, or allow the local service to recover an
@@ -35,6 +39,15 @@ historical sessions contain no recoverable video.
opening restores display settings, tool windows and dynamic source-window
positions. This action saves no sensor evidence.
Mission Core does not switch an active acquisition to another spatial source.
Every nonterminal or unknown acquisition state fail-closes saved-session replay,
persisted replay reattach and manual RRD/Rerun input/apply/reset with
**Завершите текущий приём перед сменой источника.** A plugin-owned automatic
source action clears the older source and opens the scene only after its start
returns success; failure leaves the current scene mounted. This internal
success-result transition is not an operator switch. The guard does not stop the
scanner or local receiver automatically.
Display controls have no separate Apply/Reset transaction. Boolean controls
commit immediately; sliders, colors and selects commit on release/blur or after
a short quiet period, and concurrent commits collapse to the newest value. A
@@ -75,12 +88,12 @@ A queued job may legitimately wait behind another export and therefore uses the
overall preparation deadline rather than the active-export heartbeat timeout.
The first preparation of a long capture can take time because every decodable
point/pose message is projected into RRD. Later openings reuse a verified,
digest-bound cache. Derived RRD cache v7 is intentionally incompatible with v6
and older generations. It retains v6's real `session_time = 0` payload anchor
and additionally binds the cache to the owning plugin ID, primary artifact and
ordered set of confined source artifacts. Older generations are rebuilt once in
the background.
point/pose message and every valid K1 `ModelingReport` is projected into RRD.
Later openings reuse a verified, digest-bound cache. Derived RRD cache v9 is
intentionally incompatible with v8 and older generations. In addition to plugin
and ordered-artifact identity, it binds the durable clock origin and active
envelope and materializes real `session_time = 0` origin and sealed completion
rows. Older generations are rebuilt once in the background.
The browser may use the strict `source_url` with `If-Match`, while the embedded
Rerun loader uses the canonical `viewer_source_url` whose lowercase SHA-256
`generation` query is bound to the same launch descriptor. A missing or stale
@@ -106,7 +119,15 @@ checkout:
│ └── sessions/
│ ├── .current_session # present only while a writer owns the root
│ └── <UTC>_viewer_live/
│ ├── captures/ # native MQTT source evidence
│ ├── manifest.redacted.json # normalized local project display metadata
│ ├── captures/
│ │ └── mqtt_live/
│ │ ├── mqtt.raw.k1mqtt
│ │ ├── mqtt.metadata.jsonl
│ │ ├── mqtt.timeline.origin.json
│ │ ├── mqtt.timeline.json # provisional transport envelope
│ │ ├── mqtt.timeline.session-<sha256>.json # sealed generation
│ │ └── mqtt.summary.json # atomic active-envelope pointer
│ └── media/ # canonical camera archives
└── recordings/
├── .export.lock # cross-process conversion/eviction lock
@@ -156,6 +177,9 @@ For the current K1 profile:
```text
MQTT callback
├─ durable native .k1mqtt + aligned metadata (source of record)
├─ durable clock origin (before camera production)
├─ provisional + content-addressed envelopes (transport/session bounds)
├─ ModelingReport -> live product metrics (before visual preview queue)
└─ bounded latest-wins Rerun live preview (disposable)
selected RTSP producer
@@ -166,14 +190,21 @@ selected RTSP producer
The live preview is intentionally allowed to drop frames under load. Native
point/pose evidence and camera archive writes do not traverse that queue.
Native `.k1mqtt` bytes with aligned metadata and canonical camera fMP4 archives
are the evidence source of truth. The derived RRD contains every decodable point
and pose frame from that source, but remains a rebuildable view rather than an
evidence master. A cache entry is rebuilt only when native source
Native `.k1mqtt` bytes with aligned metadata, the capture-clock artifacts and
canonical camera fMP4 archives are the evidence source of truth. The derived RRD
contains every decodable point and pose frame plus every valid `ModelingReport`
distance/speed/scan-time sample from that source, but remains a rebuildable view
rather than an evidence master. A cache entry is rebuilt only when native source
identity/digests change or an incompatible derived-data export revision is
introduced. A UI blueprint or workspace-layout revision never invalidates or
rewrites the data RRD. Cache v4/v5 payloads are not reusable as v6 because they
do not guarantee the real zero-time anchor.
rewrites the data RRD. Cache v8 and older payloads are not reusable as v9 because
they do not bind both real capture-envelope endpoints.
`ModelingReport` time is the device's `ScanTime` counter at two ticks per second;
distance and speed are the reported `MoveDistance`/`MoveSpeed` values. Live state
keeps the current device scan generation and does not accumulate an earlier
distance after the device resets scan time and route distance. These values are
not reconstructed from pose integration or a browser timer.
Expensive cache misses run through the bounded preparation worker and one global
cross-process export gate to cap peak RAM, CPU and temporary-disk use. Crash
@@ -183,6 +214,52 @@ active response leases do not wait behind that gate. The derived cache has an
8 GiB default quota, preserves a 2 GiB default filesystem reserve and evicts
least-recently-used RRDs only; it never deletes native evidence.
## Capture-clock envelope
New K1 captures first publish bounded schema-1
`captures/mqtt_live/mqtt.timeline.origin.json` with start epoch and monotonic
nanoseconds. The writer creates and synchronizes this immutable artifact after
its raw/metadata files are open and before camera production is armed. A crash
therefore cannot leave retained camera evidence whose intended session zero
existed only in memory.
MQTT shutdown then publishes schema-1 `mqtt.timeline.json` with the same start
plus transport completion. Capture-summary schema 2 initially points to it with
`capture_clock_scope: transport`, and binds both origin/envelope names and
SHA-256 values. That provisional file is not rewritten.
After the camera archive and MQTT/runtime stop, the acquisition owner creates
`mqtt.timeline.session-<sha256>.json` with the extended session completion and
atomically switches `mqtt.summary.json` to that content-addressed name, digest
and `capture_clock_scope: session` while still holding the active-session lease.
Publication order is sealed file first, summary pointer second, so a crash leaves
the previous pointer valid and the idempotent seal can converge on retry. The
same summary update records `session_elapsed_seconds`. Files and containing
directories are synchronized at their publication boundaries.
Discovery validates the summary-selected filename, both digests, the shared
origin and the envelope bounds, then catalogs that exact artifact. The
materializer passes its exact staged path to the exporter; it never chooses a
sealed generation by glob or “latest file” ordering. Direct/legacy export keeps
the fixed provisional-name fallback only for evidence outside the cataloged
session contract, and orphan sealed candidates not selected by the validated
summary are ignored.
A provisional `transport` scope is never advertised as combined camera replay.
An interrupted origin-only point/pose prefix may use the durable origin and its
last validated message as a compatibility end, but it cannot advertise camera
media. If the origin, active envelope, summary pointer or digest relationship is
missing/corrupt, new-schema discovery fails closed instead of inventing a
session boundary.
For a sealed generation, `session_time = 0` is the envelope start and the RRD
end is the envelope completion. Cache v9 logs real rows at
`/__mission_core/session_origin` and `/__mission_core/session_end`, so a camera
fragment accepted before the first MQTT message or after the last point/pose
message can remain inside the declared RRD interval. Valid legacy sessions
without this artifact keep their first/last-message compatibility fallback and
cannot gain historical camera coverage.
## Camera archive contract
New acquisitions archive each selected source and codec epoch below the same
@@ -275,6 +352,11 @@ camera acceptance run retained point/pose evidence only.
process or power failure may discard the final uncommitted group. Raw bytes
from an interrupted commit window may survive beyond the durable metadata;
replay uses only the last validated metadata-aligned raw boundary.
- New capture-summary schema 2 binds the durable origin and its active envelope
by name and SHA-256. A normal combined replay requires the owner-sealed,
content-addressed `session` scope. A crash can leave an origin-only or
provisional `transport` generation, but discovery will not use either to
advertise camera coverage. A mismatched origin/envelope/summary fails closed.
- A native capture with a missing final summary is accepted only when the raw
and metadata prefix is bounded, aligned and structurally valid. It is cataloged
as `interrupted`, never silently promoted to `ready`.
@@ -385,15 +467,17 @@ API boundary.
## Current limit
The spatial RRD, trajectory and archived fMP4 cameras use the same operator
scrubber now. Camera epochs are aligned to zero-based `session_time` from the
shared host-arrival monotonic clock and rendered through MSE. The client selects
an epoch only inside its declared inclusive interval and verifies that the
The spatial RRD, trajectory, archived device-metric series and archived fMP4
cameras use the same operator scrubber now. Camera epochs are aligned to
zero-based `session_time` from the shared host-arrival monotonic clock and
rendered through MSE. The metric tab carries device-reported route distance,
speed and scan time at their `ModelingReport` receive times. The client selects
a camera epoch only inside its declared inclusive interval and verifies that the
decoded MSE seekable duration covers that interval. This remains best-effort
correlation: codec PTS and K1 sensor exposure time are not proven to share the
LiDAR clock. A codec epoch whose init segment does not expose a browser-supported
codec or whose timing cannot be proven remains retained evidence and fails
closed in the UI/background preparation.
correlation: codec PTS, K1 sensor exposure time and LiDAR firing time are not
proven to share a device clock. A codec epoch whose init segment does not expose
a browser-supported codec or whose timing cannot be proven remains retained
evidence and fails closed in the UI/background preparation.
Historical sessions with no canonical camera archive honestly show no recorded
video. Recorded blueprints can change accumulation, grid visibility, point and
trajectory visibility, point radius and a uniform custom point color without
@@ -403,21 +487,20 @@ declared RRD has been received and decoded. Height, intensity, distance and RGB
palettes remain baked into current RRD rows; fully dynamic recoloring requires
exporting the corresponding scalar components in a future recording schema.
## Verification checkpoint — 2026-07-17
## Verification boundary — 2026-07-17
The repository state described above passed the complete local pre-push gate:
- `uv sync --frozen --group dev` completed against the locked Python graph;
- `uv run pytest`: **284 passed**;
- `uv run ruff check .`: clean;
- project mypy and strict plugin-SDK mypy: clean across 51 and 12 source files;
- the XGRIDS K1 profile loader and emitted v0alpha2 JSON schema validated;
- clean `npm ci` ran the required Rerun 0.34.1 postinstall patch;
- frontend unit suite: **111 passed, 0 failed**;
- TypeScript project build and Vite production build completed;
- the generated `dist/index.html` and non-empty Rerun WASM artifact were verified.
The current automated gate covers project-name validation, source-switch
fail-closure, optional plugin scene controls, bounded `ModelingReport` decode,
inert start/stop encoding and correlation, capture-clock publication/validation,
cache-v9 origin/end materialization and archived metric series. The standard
repository gate remains Python tests/Ruff/mypy plus frontend unit tests,
TypeScript checking and the Vite production build; exact pass counts belong to
the commit's CI/pre-push result rather than this durable architecture contract.
Vite still reports its expected large-chunk warning for the embedded Rerun
viewer/WASM payload. That is a packaging optimization item, not a failed gate.
No retained physical K1 session contains the new canonical camera archive yet,
so real recorded-camera playback remains an explicit hardware acceptance test.
No retained physical K1 session contains the new canonical camera archive, so a
real point-cloud plus one-camera recorded playback remains an explicit hardware
acceptance test. Automated protocol tests also do not authorize K1 modeling
publishing: legitimate OpenAPI key provisioning and durable save completion
remain separate physical/security gates.
@@ -28,23 +28,41 @@ admission and browser APIs. A plugin owns native evidence discovery, transport
format validation, recovery semantics, timeline-origin extraction and conversion
of its primary artifact into the canonical recorded Rerun artifact.
For new K1 sessions, the plugin also contributes confined clock artifacts. A
durable origin is published before camera production, MQTT shutdown creates a
transport-scoped provisional envelope, and the acquisition owner publishes a
content-addressed session envelope plus an atomic summary-pointer switch after
camera and MQTT/runtime shutdown. A provisional envelope cannot advertise
combined camera replay. Valid older sessions without this contract keep their
first/last-message compatibility fallback.
`ReplayCommand` is now vendor-neutral. It contains an owning `plugin_id`, an
opaque primary artifact ID, an ordered tuple of confined artifacts and a ready
timeline origin. It contains no filename suffix, MQTT topic or sidecar name.
The materializer selects the exporter by `plugin_id` and stages every validated
artifact prefix while preserving only its plugin-owned basename.
Derived RRD cache v7 binds the published RRD to:
Derived RRD cache v9 binds the published RRD to:
- plugin ID and primary artifact ID;
- ordered artifact IDs and media types;
- full file identity and replay boundary for every artifact;
- per-artifact prefix digest;
- derived recording digest and zero-based timeline bounds.
- durable clock origin, active-envelope start/completion, selected clock
artifact identity and both digests when present;
- derived recording digest and zero-based timeline bounds whose origin and end
are both materialized as real RRD rows.
Cache v6 and older sidecars are rebuilt once. The native evidence is not
Cache v8 and older sidecars are rebuilt once. The native evidence is not
rewritten.
The K1 exporter also recognizes bounded `ModelingReport` messages. It writes the
reported route distance, speed and scan time as scalar time series below
`/metrics/device`; it does not infer them from pose integration or a UI timer.
Live observation consumes the same status through a plugin-injected raw-message
observer before the four-message visual preview queue. The generic
`VisualizationRuntime` still has no K1 decoder or topic knowledge.
The concrete implementation is physically located below:
```text
@@ -79,6 +97,11 @@ therefore requires a manifest/runtime contribution and a plugin-owned reviewed
UI contribution, not edits to the session API, preparation queue, Rerun consumer
or main application shell. ADR 0010 defines that frontend boundary.
Additional acceptance tests bind the K1 capture clock into the ordered replay
artifacts, reject corrupt or provisional new-schema boundaries, materialize real
origin/end rows and retain archived device-metric samples without traversing the
lossy live-preview queue.
## Consequences
The semantic and physical vendor boundary is complete for the current
@@ -87,6 +110,12 @@ operator workflows remain compatible. The `k1link` distribution name and CLI
command remain transitional compatibility names; their implementation is now
plugin-owned.
The operator project name is normalized and validated by the K1 contribution,
stored as display/catalog metadata and never used as a path component. This ADR
does not claim that it reaches the scanner: the inert modeling-control codec has
no publisher, and automatic K1 writes remain disabled pending legitimate OpenAPI
credential provisioning and durable post-stop save evidence.
ADR 0011 subsequently places the action control plane behind a versioned
descriptor/handshake/health transport seam. Observation discovery and export
remain host-called plugin contributions and are not yet portable process
@@ -26,10 +26,13 @@ are physically located under:
```text
plugins/xgrids-k1/frontend/src/
plugin.ts
projectName.ts
automaticSourceStart.ts
XgridsK1Connection.tsx
components/
K1ProvisioningPipeline.tsx
K1AcquisitionPipeline.tsx
K1SpatialControls.tsx
K1Diagnostics.tsx
K1Metrics.tsx
```
@@ -43,9 +46,32 @@ NODE.DC UI kit. It may not import Control Station implementation paths.
`apps/control-station/src/composition/devicePlugins.ts` is the only Core file
which imports the concrete XGRIDS frontend. The host owns model selection,
safe deactivation, the `device.connection` slot, navigation, the spatial scene,
saved-session UX and global layout. The plugin owns BLE candidates, exact-profile
attestation, credentials-in-memory form state, K1 endpoint state, connection
instructions and the live/replay acquisition controls.
saved-session UX and global layout. A contribution may additionally expose one
optional `SpatialControlsView`, which the generic scene mounts beside the
viewport without learning its fields or lifecycle wording. The plugin owns BLE
candidates, exact-profile attestation, credentials-in-memory form state, K1
endpoint state, connection instructions, project-name validation, live/replay
acquisition controls and its scene-level status/stop/telemetry block.
The host surface includes `activateAutomaticSpatialSource`. The name refers to
viewer-source ownership, not automatic equipment control. The plugin calls it
only after live or adapter replay start returns success and then opens the scene;
a failed start leaves the existing scene/source intact. The action clears the
earlier manual/archive selection so backend state can supply the successful new
source. A generic fail-closed acquisition guard blocks saved-session switching,
persisted replay reattach and manual source input/apply/reset for every
nonterminal state, cleanup-pending terminal state or unknown acquisition state.
The internal successful-start action is not an operator source switch and
performs no implicit stop.
K1 project names are required, NFKC-normalized and trimmed, rejected for control
or surrogate characters or more than 96 Unicode characters, and sent to the
backend as local display metadata. The active profile has
`vendor_writes_enabled=false`, so its
buttons and scene control say that they prepare/stop local reception. Before an
acquisition exists, device-start wording is gated by active-control capability;
after start, calibration/device-stop wording additionally requires a
plugin-commanded acquisition mode.
Plugin CSS is imported with the contribution and scoped below
`.xgrids-k1-plugin`. A plugin cannot add global navigation, route ownership or
@@ -65,19 +91,24 @@ the correct component for each model. It also fails when:
- generic Core files other than the composition root contain XGRIDS/Lixel/K1
implementation knowledge;
- plugin source imports Control Station implementation paths;
- provisioning, acquisition, diagnostics or scoped-style modules disappear.
- provisioning, acquisition, optional scene controls, diagnostics or
scoped-style modules disappear;
- a nonterminal acquisition can be replaced by a manual or recorded spatial
source, including persisted replay reattach.
The accepted local gate is 114 frontend unit tests, TypeScript strict checking
The accepted local gate is the frontend unit suite, TypeScript strict checking
and a Vite production build. Backend behavior and the physical K1 protocol are
unchanged by this source/UI decomposition.
unchanged by this source/UI decomposition; protocol command authorization is a
separate backend/physical gate.
## Consequences
A second device family can provide a completely different connection pipeline
and UI while reusing the same model catalog, lifecycle transition, observation
scene and saved-session host. Adding it requires its own plugin-owned frontend
contribution and one reviewed composition import, not edits to `App`,
`DeviceWorkspace` or generic runtime state.
and optional scene controls while reusing the same model catalog, lifecycle
transition, observation scene and saved-session host. Adding it requires its own
plugin-owned frontend contribution and one reviewed composition import; the
generic host changes only when a genuinely cross-device slot/action is added,
not for vendor fields or protocol-specific workflow steps.
The remaining isolation gap is deployment-level: frontend contributions are
still compiled into one signed application build, and backend plugins still run