# Verified K1 MQTT stream profile

Status: verified against one owner-controlled LixelKity K1 running firmware
3.0.2. This is a capture and decoder profile, not a claim of vendor support or a
stable public API.

## Transport

After BLE provisioning, the K1 joins the operator's LAN and exposes:

| Property | Verified value |
| --- | --- |
| Transport | TCP |
| Port | 1883 |
| Application protocol | MQTT 3.1.1 |
| TLS | not used by this direct-LAN path |
| Username/password | not used by the observed client setup |
| K1 address | DHCP/private IPv4, retained only in the ignored session |

The direct route for the confirmed K1 address used the Mac Wi-Fi interface even
while the Mac's default route was a VPN tunnel. Commands must resolve the route
for the confirmed K1 address and must not scan the surrounding subnet.

The repository's capture client is subscribe-only. It sends the minimum MQTT
CONNECT, SUBSCRIBE, keepalive and DISCONNECT protocol packets; it has no reason
to publish an application command.

## Verified report topics

The firmware-3 stream set is:

```text
lixel/application/report/heartbeat
lixel/application/report/device_status
lixel/application/report/lio_pcl
lixel/application/report/lio_pose
lixel/application/report/modeling
```

Additional report topics exist for system errors, modeling status, PGO,
control points and upgrades. The safe discovery subscription is restricted to
`lixel/application/report/#`. It does not subscribe to request topics.

The application also retains compatibility subscribers for:

```text
RealtimePointcloud
RealtimePath
DeviceStatus
```

No legacy stream was emitted by the tested firmware during the live run.

## Firmware-3 point cloud

`lixel/application/report/lio_pcl` contains a protobuf compression envelope:

```proto
message MqttCompressMsg {
  Header header = 1;
  CompressionType compression = 2; // 0=LZ4, 1=Zstd
  uint32 compressed_size = 3;      // expected decoded byte count
  bytes compressed_data = 4;
}
```

The verified value is compression enum 0. `compressed_data` is a raw LZ4 block,
not an LZ4 frame. The decoder must supply `compressed_size`, enforce the exact
decoded length and reject enum 1 until a bounded Zstd path is reviewed.

The decoded protobuf is:

```proto
message LioPclReport {
  Header header = 1;
  repeated Point points = 2;
}

message Point {
  sint64 x = 1;
  sint64 y = 2;
  sint64 z = 3;
  uint32 rgbi = 4;
}
```

Coordinates are ZigZag `sint64` values. Sensor-native metric coordinates are
`(x/header.scaler, y/header.scaler, z/header.scaler)`; a zero scaler is invalid.
The tested frames consistently used scaler 1000.

The application uses only `rgbi & 0xff` as intensity/alpha. The connector keeps
the complete `uint32`; interpreting the upper 24 bits as RGB remains unverified.
Raw coordinates are preserved. Any Y/Z swap belongs in a derived visualization,
not in capture storage.

## Firmware-3 pose

`lixel/application/report/lio_pose` is an uncompressed protobuf:

```proto
message LioPoseReport {
  Header header = 1;
  PoseStamped pose = 2;
  float distance = 3;
  float pose_accuracy = 4;
}

message PoseStamped { sint64 stamp = 1; Pose pose = 2; }
message Pose { Point position = 1; Quaternion orientation = 2; }
message Point { double x = 1; double y = 2; double z = 3; }
message Quaternion { double x = 1; double y = 2; double z = 3; double w = 4; }
```

Storage keeps position `(x,y,z)` and quaternion `(x,y,z,w)` exactly as received.
Non-finite values are rejected. Quaternion normalization, coordinate transforms
and sensor-to-vehicle extrinsics are derived operations and must not overwrite
raw values.

## Legacy formats

`RealtimePointcloud` starts with a 12-byte envelope. Little-endian uint32 at
offset 0 is the point stride. Records contain float32 `x,y,z` at offsets 0,4,8,
RGB bytes at 12,13,14 and, when stride is at least 16, intensity at 15. Envelope
bytes 4 through 11 remain uninterpreted and are retained.

`RealtimePath` contains float32 position `x,y,z` at offsets 0,4,8; offset 12 is
unknown; quaternion wire order is `w,x,y,z` at offsets 16,20,24,28. The decoded
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. 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.

An offline bounded audit of the retained owner-controlled captures additionally
proved that the repository encoder reproduces the observed START and STOP
payloads byte-for-byte for this one K1 and exact firmware profile. It also
separated four values which must never be substituted for one another:

- Mission Core's provisional device UUID is local inventory identity and never
  belongs in a vendor request;
- the vendor device ID comes from live K1 status and occupies the request header;
- the K1 serial is a separate live device-binding value;
- the OpenAPI value is private application-level material embedded by the
  observed LixelGO build; it is shared across all retained request types and is
  not derived from the current scanner while the application value is present.

Consequently, support for another K1 never replays captured payload bytes and
does not require a manually maintained scanner profile. BLE selection and its
provisioning response bind the transport to that unit; an initial
`DeviceInfoRequest` without device ID returns the live vendor ID, serial, model,
activation and version facts. Those values bind later requests and must agree
with the live status stream. Any mismatch, identity drift, malformed response,
replay-only evidence or unexpected lifecycle state fails closed.

The clean LixelGO cycle contains exactly ten requests before START, all QoS 2
and retain false: `DeviceInfo`, `ModelingStatus`, `GetRtkAdvance`, a bound
`DeviceConfig` time/timezone sync, then `DeviceInfo`, `GetRtkAdvance`,
`GetNtripProfile`, `GetCloudServerConfig`, `GetRtkAdvance`, `DeviceInfo`. The
special time-sync session is exactly
`${device_id}:DeviceConfigRequest:Publish_Proto_DeviceConfig_SetTime`. The
repository shadow bootstrap reproduces all ten retained payloads and their
topic order byte-for-byte. Time sync is the only mutation before START; the
other nine requests are reads. Its Unix timestamp is protobuf `sint64`, not
`int64`/`uint64`: the retained value `3568430060` ZigZag-decodes to
`1784215030` (`2026-07-16 15:17:10 UTC`). The runtime encoder must therefore
ZigZag-encode the current epoch before writing field 1 of the nested time
configuration.

The first ten requests contain five observed publish groups but only four
application-response windows. The initial DeviceInfo response is the identity
binding barrier; ordinals 2–6 then retain their exact publish order without an
invented response barrier between ordinals 3 and 4. Two independent owner-operated START
captures contain the same 14-operation order: the ten requests above, START,
one bound `ModelingStatus` read, then a bound `DeviceInfo` plus
`ModelingStatus` refresh about 24–25 seconds after START. The first capture
reached START 202.423 seconds after its initial DeviceInfo; the faster capture
reached it after 54.130 seconds. This variation is operator/UI time, while the
stage order is invariant. In both captures the original control MQTT connection
remained open throughout initialization and the later refresh; no DISCONNECT or
UNSUBSCRIBE occurred. DeviceInfo response establishes identity before later
bound requests. Required response barriers are correlated by operation key, topic, exact
session, vendor identity, application authority and numeric success. The
initial and immediate post-START ModelingStatus reads are intentionally not
treated as mandatory synchronous responses. In both retained captures operation
12 has no application response; the single bound ModelingStatus response in
that part of the lifecycle arrives only after operation 14 and belongs to it.
An unanswered optional operation therefore cannot reserve FIFO ownership over
the required operation-14 response. Readiness belongs to live DeviceStatus plus
point/pose observation.

The UI/event correlation is also fixed by the retained capture. Requests 1–6
follow successful data connection. Entering the scan workspace causes request
7. Opening the record/project-name prompt causes reads 8–10. The project name is
not sent by a separate save mutation: it first appears in the START payload when
the operator confirms the prompt. K1 then reports `SCAN_STARTING`; completion is
the live transition to `SCANNING` with a bound project and `init_ready=true`.
Operations 13–14 coincide with that state in both captures. The observed
24–25-second initialization duration is therefore evidence/telemetry, not a
hard-coded transition delay.

The repository now contains an inert bounded encoder/response parser and a
fail-closed device-status state machine for this exact profile. A live-only
shadow planner accepts START only from `READY` with no project and STOP only
from `SCANNING` with a bound project. It emits only a non-executable digest and
wire metadata, uses QoS 2 with retain false, and declares automatic retry
forbidden. It has no MQTT publish dependency and cannot send a command.

Legacy shadow-plan publishing remains deliberately disabled. The exact 36-byte authority has a
read-only macOS Keychain loader with no environment, file, browser or API
fallback. A facade-owned dormant coordinator can arm it only after explicit
operator confirmation, a connected/attested K1, idle runtime and no active
acquisition. Its non-exportable process-memory lease is limited to 15–300
seconds and is revoked by expiry, disarm, reprovision, acquisition preparation
or shutdown. The current one-shot boundary is structurally write-disabled,
exposes no emission method and cannot touch its injected sink.

A separate operator-driven physical-acceptance implementation is installed in
plugin v0.5.0 through the facade/runtime. Retained-PCAP re-audit fixes its MQTT
contract at 3.1.1,
`clean_session=false`, keepalive 60, the three ordered 9/5/42-topic control
subscription groups, separation from the point-cloud client, QoS 2
request/PUBCOMP completion and
`lixel/application/response/modeling`. It performs one connection attempt,
consumes every ordinal/action operation key before publish, never reconnects or
retries, rejects stale/duplicate/unexpected responses and poisons every unknown
post-publish outcome. A 15–120 second permit requires explicit operator-present,
owner-controlled-device, LixelGO-closed, battery/storage and expected-state
confirmations. START additionally requires the complete four-window bootstrap;
STOP requires its own separate permit. No plugin state read, poll, navigation or
automatic lifecycle event may invoke either action. On 2026-07-18 its first
operator-present physical attempt made one
connection and emitted bootstrap ordinals 1–6. All six QoS 2 publishes completed
and five required responses were collected, but the old topic-only collector
misrouted one of the two `GetRtkAdvance` responses in its third window. The
executor stopped before building or publishing START;
K1 remained `READY`, project binding stayed absent and point/pose remained zero.
No automatic retry was attempted. A second explicitly permitted attempt then
correlated all ten bootstrap responses and one START success response, with 11
QoS2 completions, but it compressed the exchange into 365 ms and ended without
the retained post-START reads or durable control-session ownership. K1 remained
in `SCAN_STARTING` for about 24 seconds, emitted two identical
`report/system_error` values `0x32040133`, returned to `OTHER_STATUS`, produced
no point/pose frames and showed steady red. The bounded system-error decoder maps
the `0x32040000 + 307` value to the recovered `ALGORITHM_ERROR` state. A normal
operator power cycle restored steady green.

This disproves the earlier assumption that byte-matched pre-START payloads plus
a correlated START acknowledgement are sufficient acceptance. The legacy
collapsed `run_bootstrap()` executor now rejects before emission. Its
replacement is an operator-driven acceptance session owner. It requires the
observed connection, workspace-entry and project-prompt checkpoints instead of
wall-clock floors, sends the immediate post-START ModelingStatus read, services
the same MQTT socket until live `SCANNING + project + init_ready`, then emits the
DeviceInfo/ModelingStatus refresh. It continues servicing that socket until an
explicit STOP request and afterwards until live unbound `READY`. That device
status is the canonical standby evidence; no second visual acknowledgement is
required. System-error and DeviceStatus reports are decoded into redacted safety
state; a fault forbids further automatic action. Standalone START/STOP are
disabled. MQTT keepalive/report processing also continues while the operator is
between UI actions; the 15–120-second command permit is created at the actual
START or STOP confirmation, not at connection time. Plugin v0.5.0 now exposes
this path through separate plugin-owned UI actions. Repository tests use only
in-memory transports; physical execution occurs only from an explicit operator
click against an already selected direct-LAN K1.

The one-time Keychain administration command uses `security -w` as the final
argument so Apple's own TTY prompt receives the authority. Mission Core never
accepts it through CLI argv and validates the resulting fixed item through the
production loader. The macOS CLI approval path timed out during the first lab
attempt; a temporary Security.framework-backed item was used only inside the
one-shot process and deleted immediately afterward. It is not the product
authority architecture. The acceptance executor now records only ordinal,
message type, response topic, payload size, SHA-256 and a safe correlation
reason; it never exposes response bytes or authority in diagnostics. The
retained clean-cycle now completes all nine bootstrap barriers offline while
ordinals 3 and 6 are simultaneously in flight and routes them by exact
session/device identity. After repairing the PCAP-proven protobuf `sint64` time
field, the next operator-run physical cycle completed all 14 canonical requests
on one socket, reached `SCANNING + project + init_ready`, delivered real
point/pose data and completed one correlated STOP. Streams quiesced, K1 returned
to unbound `READY`; no retry or fallback STOP was sent. The operator also
observed stable green, but that observation is not a second protocol gate. The
local archive sealed and passed digest-bound RRD and recorded-
camera admission. Appearance and reuse of the vendor-native project through
LixelGO or USB remains a separate gate. Physical double-click remains the
operational fallback.

## Decoder and raw-capture bounds

The semantic decoder applies these default defensive limits before allocation
or iteration:

- 2 MiB maximum MQTT payload;
- 1 MiB maximum compressed block;
- 8 MiB maximum decoded block;
- 64:1 maximum claimed expansion ratio;
- 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.

## Camera result outside MQTT

No camera payload was observed in the report-topic MQTT run documented above.
The later LixelGO/iPhone experiment found the preview on a separate transport:
left/right RTSP sessions on TCP `8554` with interleaved RTP/TCP and H.264. This
proves compressed preview availability, not full-resolution raw frames or
camera calibration. See Lab 002.
