NODEDC_MISSION_CORE/docs/adr/0012-device-bound-k1-comman...

6.1 KiB

ADR 0012: live-bound K1 control with application-level authority

  • Status: accepted
  • Date: 2026-07-18
  • Extends: ADR 0004, ADR 0005, ADR 0009 and ADR 0010

Context

Owner-controlled LixelGO traffic proves the K1 preparation and modeling dialogue for one activated K1 running the exact xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1 profile. Offline regression proves that Mission Core reproduces all ten requests before START, plus START and STOP, byte-for-byte. This describes the current device dialogue but does not authorize writes to any K1.

The protocol exposes identities with different ownership. Mission Core's inventory UUID and the macOS CoreBluetooth UUID are local transport identities. The vendor device ID and serial identify the live scanner. The OpenAPI value is private application material: retained traffic uses one value across every request type, and decompiled LixelGO uses its embedded application value before the serial-derived fallback. It is therefore not a manual per-scanner profile.

The selected BLE peripheral returns the LAN address of that same unit during the reviewed Wi-Fi provisioning exchange. On MQTT, LixelGO first sends DeviceInfoRequest without a device ID. DeviceInfoResponse supplies vendor ID, serial, model, activation and version facts; subsequent headers bind to that identity. Native project durability remains separate from command acceptance.

Decision

K1 application control is exact-profile, live-bound and fail-closed.

  1. Mission Core never substitutes its inventory UUID, BLE UUID or serial for the vendor request identity.
  2. One private application-level OpenAPI authority is loaded outside Git, Ops, browser state, APIs and logs. It is not copied into scanner profiles.
  3. The BLE-selected transport is followed by an unbound DeviceInfo exchange. Its live vendor ID, serial, activation and FW 3.0.2 facts form the transient device binding; a saved profile is optional metadata, not protocol authority.
  4. The ten retained pre-START requests keep their exact order and response boundaries. The only mutation is the observed time/timezone sync; the other nine requests are reads. No request is batched, skipped or automatically retried.
  5. Live DeviceInfo binding must agree with the live DeviceStatus stream before START/STOP planning. Identity drift, malformed data, inactive equipment, profile mismatch or an unexpected lifecycle state fails closed.
  6. START retains record-and-calculate mode 2, LCC mode 1, handheld mount 0 and the validated project name. STOP contains only bound header and action.
  7. MQTT application semantics remain QoS 2, retain false. An unknown outcome is reconciled from correlated response, status and physical evidence before any new operator-authorized attempt.
  8. No firmware, activation, account, update or vendor-cloud mutation belongs to this path. GetCloudServerConfig is a local K1 read.
  9. STOP completion has separate gates: correlated result, local stream quiescence/evidence sealing, READY plus steady green, then independent native project verification.

Recovered pre-START sequence

The clean cycle contains:

  1. unbound DeviceInfoRequest;
  2. unbound ModelingStatusRequest;
  3. unbound GetRtkAdvanceRequest;
  4. bound DeviceConfigRequest time/timezone sync;
  5. bound DeviceInfoRequest;
  6. bound GetRtkAdvanceRequest;
  7. bound GetNtripProfileRequest;
  8. bound GetCloudServerConfigRequest;
  9. bound GetRtkAdvanceRequest;
  10. bound DeviceInfoRequest;
  11. bound ModelingRequest START.

Normal sessions are ${device_id_or_empty}:${MessageType}. Time sync uses the captured special relation ${device_id}:DeviceConfigRequest:Publish_Proto_DeviceConfig_SetTime.

Current implementation boundary

application_bootstrap.py provides DeviceInfo decoding, the exact ten-request shadow plan and a five-batch response-barrier orchestrator. Its private retained regression completes with 10/10 request payload/topic matches and correlated required responses. ModelingStatusRequest remains non-blocking exactly where the capture had no synchronous response. application_authority.py reads the exact 36-byte authority only from a fixed macOS Keychain item and offers no plaintext fallback. LiveModelingControlSafety independently binds DeviceInfo identity to live status and creates non-executable START/STOP plans.

Public state contains only booleans, counts and wire metadata. Private identity, authority and payload bytes stay out of repr/API output. application_publish.py defines reviewed one-shot envelopes plus a current write-disabled publisher type. That type has no enable switch and raises before its injected transport sink can be called. Both shadow paths remain blocked by vendor-writes-disabled and publisher-not-installed; no MQTT publisher or automatic retry path exists.

Promotion gate

A future publisher requires separate review and an operator-present physical acceptance:

  1. provision the reviewed private authority into the fixed local Keychain item and load it for one operator-owned execution lease;
  2. confirm battery/storage and select one K1 over BLE;
  3. run the exact response-gated bootstrap and attest activated FW 3.0.2;
  4. confirm READY identity against DeviceInfo and review shadow metadata;
  5. send one START without retry and correlate its response;
  6. observe calibration and first point/pose/camera data;
  7. send one STOP without retry and correlate its response;
  8. seal local evidence while accepting the bounded stream tail;
  9. wait for READY and steady green;
  10. verify the native project through the vendor-supported workflow.

Any unknown response, identity change, fault, low battery/storage or unexpected transition pauses the test. It never triggers a guessed recovery command.

Consequences

Adding another identical K1 means selecting it over BLE and deriving a fresh live DeviceInfo binding, not creating or copying a command profile. A second physical K1 remains a useful portability acceptance, but another packet capture is not required before implementing the reviewed current profile. The design preserves native record-and-calculate behavior while keeping vendor control in the XGRIDS plugin and out of generic Mission Core.