diff --git a/README.md b/README.md
index 808218c..9405dfe 100644
--- a/README.md
+++ b/README.md
@@ -133,9 +133,9 @@ BLE Wi-Fi provisioning write, read-only MQTT live capture, native `.k1mqtt` and
reviewed-TSV replay, raw-first evidence storage, device-reported scan
time/distance/speed and measured preview metrics. Its frontend contribution now
also owns an optional spatial-scene control block, including acquisition phase,
-telemetry and the stop action. On the active laboratory profile that stop action
-finalizes local reception only; physical K1 scanning is still started and
-stopped by the verified double-click.
+telemetry and the stop action. Operator-manual acquisition still finalizes only
+local reception; plugin-commanded v0.5.0 acquisition uses the separately gated
+canonical K1 START/STOP dialogue.
The exact recovered `ModelingRequest` start/stop encoder, response correlator
and device-status state machine are implemented as inert protocol components.
@@ -147,16 +147,18 @@ as one private application-level value in the observed client, not a manual
per-scanner profile. The whole pre-START order and START/STOP payloads are
byte-matched offline. A fixed macOS Keychain loader now supplies the exact
application authority without env/file/browser fallback, and a five-batch
-orchestrator passes all retained response barriers. The installed publisher
-boundary is structurally write-disabled and cannot call its sink. Plugin v0.4.0
-wires a dormant coordinator into the facade: explicit shadow arm holds authority
+orchestrator passes all retained response barriers. The legacy shadow publisher
+boundary remains structurally write-disabled and cannot call its sink. Its
+dormant coordinator can hold authority
for at most 300 seconds and expiry/disarm/reprovision/acquisition/shutdown revoke
-it together with the orchestrator. A separate, uninstalled physical-acceptance
-transport now reproduces MQTT 3.1.1 `clean_session=false`, keepalive 60, exact
+it together with the orchestrator. Plugin v0.5.0 installs a separate
+physical-acceptance transport reproducing MQTT 3.1.1 `clean_session=false`,
+keepalive 60, exact
response subscriptions, QoS2 completion and the five response-gated batches.
It consumes operation keys before publish and poisons unknown outcomes without
-retry. Neither component is wired to facade/UI; `vendor_writes_enabled` remains
-false. The first operator-present physical attempt on 2026-07-18 emitted only
+retry. One background owner services that socket between separate connection,
+workspace, project, START, STOP and steady-green UI actions; navigation cannot
+emit a command. The first operator-present physical attempt on 2026-07-18 emitted only
the first six bootstrap requests, then failed closed while correlating the third
response batch; START was not emitted. A second one-shot attempt correlated all
ten pre-START requests and the START acknowledgement, but incorrectly collapsed
diff --git a/apps/control-station/README.md b/apps/control-station/README.md
index 774fd16..736aa64 100644
--- a/apps/control-station/README.md
+++ b/apps/control-station/README.md
@@ -322,12 +322,13 @@ facts и старые presentation-поля `phase`, `message`, `devices`,
очищается после успешного ответа и не сохраняется в URL/local storage.
- BLE-подключение выполняет только отдельно рассмотренную provisioning-запись;
случайные GATT writes и автоматические повторы запрещены.
-- MQTT live/replay не публикует команды устройству. Запуск и остановка
- физического сканирования остаются за кнопкой K1.
-- Exact start/stop codec, response correlator и device-status state machine
- остаются inert: они не импортируют MQTT transport. Header derivation известен,
- но legitimate OpenAPI key provisioning и durable post-stop save gate не
- доказаны, поэтому `vendor_writes_enabled=false`.
+- MQTT data live/replay остаётся subscribe-only. Команды изолированы в
+ plugin-owned canonical control session и доступны только через отдельные
+ operator-present UI checkpoints.
+- Exact start/stop codec, response correlator и device-status state machine не
+ владеют transport. Plugin v0.5.0 связывает их отдельным single-owner MQTT
+ контуром без wall-clock переходов, reconnect или automatic retry; после STOP
+ обязательны live READY и подтверждённый постоянный зелёный индикатор.
- Live-сессии сначала сохраняют сырые сообщения и camera segments, затем
формируют disposable preview. При перегрузке preview может быть отброшен,
native evidence сохраняется. MQTT durability имеет bounded group-commit RPO,
diff --git a/docs/01_IMPLEMENTATION_PLAN.md b/docs/01_IMPLEMENTATION_PLAN.md
index b2e677f..efc01a8 100644
--- a/docs/01_IMPLEMENTATION_PLAN.md
+++ b/docs/01_IMPLEMENTATION_PLAN.md
@@ -21,7 +21,7 @@ Each gate produces evidence and an explicit GO, PAUSE or BLOCKED result.
| 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 |
-| K1 application control | PAUSE (accepted START followed by `ALGORITHM_ERROR`) — first live attempt stopped before START on a batch-3 correlation failure; a second attempt correlated all 10 pre-START requests plus START but collapsed UI stages into 365 ms and omitted the retained post-START/control-session lifecycle. The collapsed path and standalone START/STOP are disabled. An uninstalled offline executor now uses explicit UI checkpoints plus live DeviceStatus gates; no further physical write is authorized yet |
+| K1 application control | OPERATOR ACCEPTANCE READY — the failed collapsed path and standalone START/STOP remain disabled. Plugin v0.5.0 installs one continuous control-session owner behind separate connection, workspace, project, START, STOP and steady-green UI actions. No wall-clock transition, automatic retry, navigation-triggered command or repository-test equipment I/O exists. A full operator-run physical cycle and native-project verification remain open |
| 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
diff --git a/docs/05_K1_MQTT_STREAM_PROFILE.md b/docs/05_K1_MQTT_STREAM_PROFILE.md
index 4dc4b1c..56048e8 100644
--- a/docs/05_K1_MQTT_STREAM_PROFILE.md
+++ b/docs/05_K1_MQTT_STREAM_PROFILE.md
@@ -260,7 +260,7 @@ 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 uninstalled acceptance-only session owner. It requires the
+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
@@ -270,9 +270,10 @@ standby. 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. This path is
-synthetic-test-only and remains outside facade/UI, so it
-does not authorize a new physical attempt.
+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
diff --git a/docs/06_K1_LIVE_VIEWER.md b/docs/06_K1_LIVE_VIEWER.md
index c4bf48c..dd3d726 100644
--- a/docs/06_K1_LIVE_VIEWER.md
+++ b/docs/06_K1_LIVE_VIEWER.md
@@ -115,44 +115,45 @@ 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. 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.
-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.
+5. Confirm operator presence, closed LixelGO, storage/power and steady green;
+ choose **Подключить управление K1**. This emits only connection operations
+ 1–6 and then waits indefinitely for the next UI action.
+6. Choose **Открыть рабочее пространство K1** for operation 7.
+7. Enter the required project name and choose **Сохранить проект и подготовить
+ локальный приём**. Mission Core normalizes it, prepares evidence reception
+ and releases only operations 8–10; the name itself is not sent yet.
+8. After the project step is ready, choose **Запустить сканирование K1** once.
+ The project name is carried by canonical START. Mission Core opens
+ **Наблюдение → Пространственная сцена** and shows calibration while the same
+ control socket waits for `SCANNING + project + init_ready`.
+9. 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.
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.
+11. Choose **Остановить устройство и запись** in the scene. The
+ plugin-commanded path emits one canonical STOP on the original socket,
+ waits for live unbound READY, and asks the operator to confirm the constant
+ green indicator before capture, camera archive and summaries are finalized.
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 connector subscribes to the fixed report-topic
-allowlist and does not publish an application request or modeling command.
+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 by explicit staged UI actions.
-The recovered command substrate is intentionally inert. It byte-matches the ten
+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 without importing MQTT or
-publishing anything. One private application-level OpenAPI authority is kept
+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, bounded execution lease and dormant facade
-orchestrator now exist. No emission method or live MQTT sink is installed, and
-an acceptance-only transport exists outside the facade. Keychain provisioning,
-physical START/STOP and durable save completion remain open, so
-`vendor_writes_enabled` is false and the physical-button workflow is canonical.
+The fixed Keychain loader and dormant shadow coordinator remain available for
+inspection. Plugin v0.5.0 additionally installs a continuous interactive MQTT
+owner: operations 1–6 at connection, 7 at workspace entry, 8–10 at project
+preparation, then separately permitted START and STOP. Physical START/STOP and
+durable native-project completion still require operator acceptance.
## Automatic Rerun source and lifecycle
diff --git a/docs/07_MISSION_CORE_MONOREPO.md b/docs/07_MISSION_CORE_MONOREPO.md
index 88e04b5..495827b 100644
--- a/docs/07_MISSION_CORE_MONOREPO.md
+++ b/docs/07_MISSION_CORE_MONOREPO.md
@@ -138,21 +138,20 @@ after explicit operator attestation of firmware `3.0.2` and direct-LAN topology,
and records the basis as `operator-attested`. That claim is not device-derived
proof. The profile independently records observation, decode, replay,
physical-verification and write evidence. Loading it cannot authorize a
-transport mutation. Application-command publishing remains disabled; the
-separately reviewed BLE Wi-Fi provisioning write retains its own explicit
-operator gate.
+transport mutation. BLE Wi-Fi provisioning and application control retain
+separate explicit operator gates.
-An exact but inert K1 application-control substrate now lives inside the vendor
+An exact K1 application-control substrate now lives inside the vendor
plugin. It byte-matches the retained ten-request pre-START dialogue plus
START/STOP, correlates live DeviceInfo/modeling responses and maps bounded
-device-status values into an observation-only state machine. The OpenAPI value
+device-status values into a fail-closed state machine. The OpenAPI value
is one private application-level authority; vendor ID and serial come from the
-BLE-selected live K1 and are cross-checked against status. No MQTT publisher
-exists. A fixed Keychain loader, bounded lease and dormant facade orchestrator
-are present, but there is no emission method or live MQTT sink. Keychain item
-provisioning and stable-artifact proof after STOP remain unresolved. A separate
-acceptance-only transport is not imported by the facade, so
-`vendor_writes_enabled=false` remains authoritative.
+BLE-selected live K1 and are cross-checked against status. Plugin v0.5.0 owns a
+single interactive MQTT session from connection through STOP/standby, with one
+explicit UI action per recorded lifecycle boundary and no automatic retry. A
+fixed Keychain loader and dormant shadow coordinator remain present. Stable
+physical proof after STOP remains unresolved; write capability is exposed only
+while that interactive socket owner is active.
## Semantic lifecycle
diff --git a/docs/adr/0010-plugin-owned-frontend-device-workflows.md b/docs/adr/0010-plugin-owned-frontend-device-workflows.md
index d121c0d..905c421 100644
--- a/docs/adr/0010-plugin-owned-frontend-device-workflows.md
+++ b/docs/adr/0010-plugin-owned-frontend-device-workflows.md
@@ -66,13 +66,16 @@ 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
+backend as local display metadata. At the time of this ADR the active profile
+had `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.
+The application-control portion is superseded by ADR 0012 and plugin v0.5.0;
+the plugin ownership and host-boundary decisions remain unchanged.
+
Plugin CSS is imported with the contribution and scoped below
`.xgrids-k1-plugin`. A plugin cannot add global navigation, route ownership or
unscoped global selectors.
diff --git a/docs/adr/0012-device-bound-k1-command-authority.md b/docs/adr/0012-device-bound-k1-command-authority.md
index 2fdf285..6159123 100644
--- a/docs/adr/0012-device-bound-k1-command-authority.md
+++ b/docs/adr/0012-device-bound-k1-command-authority.md
@@ -110,8 +110,8 @@ shadow arm/disarm/state actions and redacted snapshots. The coordinator has no
request-emission method and the installed sink is explicitly uninstalled; this
runtime wiring does not widen write authority.
-`application_mqtt.py` and `application_acceptance.py` provide the next, still
-uninstalled physical-acceptance boundary. The MQTT type repeats the retained
+`application_mqtt.py` and `application_acceptance.py` provide the reviewed
+physical-acceptance boundary. The MQTT type repeats the retained
3.1.1 session (`clean_session=false`, keepalive 60), subscribes only to exact
known response topics, admits only reviewed request topics and QoS2/retain-false
envelopes, and consumes explicit bootstrap ordinals/modeling actions before the
@@ -134,8 +134,18 @@ MQTT through explicit STOP and waits for live READY plus operator-confirmed
standby. Fault/status observations are redacted and any fault forbids automatic
continuation. The socket remains serviced during operator/UI waits, and each
short action permit is created at its button confirmation rather than at
-connection time. These types are synthetic-test-only, not imported by the facade,
-declared as plugin actions or represented by UI controls.
+connection time.
+
+Plugin v0.5.0 installs this boundary through
+`application_session.py`. One background owner is the only thread allowed to
+touch the control MQTT client. REST/UI actions release exactly one checkpoint:
+session open (1–6), workspace entry (7), project preparation (8–10), START, STOP
+and steady-green confirmation. Preparing local reception happens before the
+START event is released. After STOP, local capture remains owned until live
+unbound READY and explicit visual confirmation. Page close, model switching,
+polling and service state reads cannot emit START/STOP; model switching is
+blocked while the socket is open. Repository tests replace the transport with
+in-memory fakes and perform no equipment I/O.
`MacOSKeychainApplicationAuthorityProvisioner` invokes Apple's interactive
`security add-generic-password ... -w` prompt with `-w` last. The private value
diff --git a/plugins/xgrids-k1/README.md b/plugins/xgrids-k1/README.md
index 347e400..ec34594 100644
--- a/plugins/xgrids-k1/README.md
+++ b/plugins/xgrids-k1/README.md
@@ -21,7 +21,8 @@ The plugin owns:
- BLE discovery hints and K1 GATT metadata;
- the reviewed firmware-3 Wi-Fi provisioning profile;
- K1 LAN status and private-address validation;
-- subscribe-only MQTT transport and report-topic allowlist;
+- subscribe-only data MQTT transport plus a separately bounded canonical
+ application-control transport;
- native `.k1mqtt` capture;
- firmware-scoped protobuf/LZ4 and legacy codecs;
- normalization of K1 point cloud and pose, plus raw-only preservation of the
@@ -31,8 +32,9 @@ The plugin owns:
- K1-specific operator instructions and compatibility tests.
- the scoped React `device.connection` contribution and its BLE/Wi-Fi and
acquisition pipeline UI.
-- the dormant application-control facade boundary: explicit shadow arm/disarm,
- a 15–300 second Keychain-backed in-memory authority lease, and redacted state.
+- the interactive canonical application-control session: one socket owner,
+ separate workspace/project/START/STOP UI checkpoints, live status gates,
+ single-action physical permits and no automatic retry.
The plugin does not own:
@@ -61,10 +63,11 @@ legacy router; both paths now cross the same runtime transport and delegate to t
`XgridsK1CompatibilityService` methods. Synchronous capture/runtime operations
run outside the FastAPI event loop.
-Model switching calls the plugin deactivation hook. An active acquisition uses
-semantic `acquisition.stop` in `capture-only` mode; replay and pre-v1alpha2
-sessions retain the legacy `stream.stop` shim. Neither path claims that the
-physical K1 stopped without separate operator evidence. BLE, MQTT, codec,
+Model switching calls the plugin deactivation hook. It is rejected while the
+canonical K1 control socket is open, so navigation can never become an implicit
+device STOP. Operator-manual acquisition uses semantic `acquisition.stop` in
+`capture-only` mode; replay and pre-v1alpha2 sessions retain the legacy
+`stream.stop` shim. BLE, MQTT, codec,
archive discovery and RRD export modules now live under the plugin package
after replay parity and physical K1 regression. The current transport remains
in-process and its health is lifecycle-only; process isolation, crash/restart
@@ -81,18 +84,18 @@ device I/O with:
uv run python plugins/xgrids-k1/profile_loader.py
```
-Plugin v0.4.0 does not widen device authority. Its application-control
-coordinator has no request-emission method, no live MQTT sink and no UI control;
-`vendor_writes_enabled` remains false. A separate uninstalled acceptance-only
-transport now implements exact subscriptions, QoS2 completion, response
-barriers and no-retry poisoning. The admin CLI can provision the fixed Keychain
-item through Apple's hidden prompt without accepting the value as an argument.
-Neither path is imported by the facade. The first physical attempt emitted only
-bootstrap ordinals 1–6 and failed closed on live batch-3 response correlation;
-START was not emitted and the K1 remained READY. The temporary lab Keychain item
-was deleted. An OS-independent plugin/edge authority provider, redacted live
-response comparison and a newly permitted full START/STOP acceptance remain
-required.
+Plugin v0.5.0 installs that reviewed transport behind explicit plugin actions.
+Opening control performs only the connection-owned operations 1–6. Workspace
+entry releases operation 7; saving the project and preparing local reception
+releases operations 8–10; a separate START click carries the project name and
+then waits for bound `SCANNING + project + init_ready` before operations 13–14.
+STOP is separately permitted, never retried, and keeps the same socket until K1
+reports unbound READY and the operator confirms a steady green indicator. The
+admin CLI still provisions the fixed Keychain item through Apple's hidden
+prompt without accepting the private value as an argument. No physical command
+is emitted merely by loading the plugin, opening the page, navigating, polling
+state or running repository tests. Full v0.5.0 physical acceptance remains an
+operator-run gate.
The optional owner-controlled iPhone/LixelGO observation tool lives under
[`lab/iphone-capture/`](lab/iphone-capture/). It pins `pymobiledevice3` in a
diff --git a/plugins/xgrids-k1/frontend/README.md b/plugins/xgrids-k1/frontend/README.md
index 0d2c302..6b53013 100644
--- a/plugins/xgrids-k1/frontend/README.md
+++ b/plugins/xgrids-k1/frontend/README.md
@@ -8,12 +8,14 @@ The contribution contains:
- `K1ProvisioningPipeline` for power confirmation, BLE discovery and the
reviewed Wi-Fi provisioning write;
-- `K1AcquisitionPipeline` for live receiver preparation, operator-manual K1
- start/stop and compatibility file replay;
+- `K1AcquisitionPipeline` for explicit canonical connection/workspace/project/
+ START checkpoints, local receiver preparation and compatibility file replay;
+- `K1SpatialControls` for an explicit no-retry STOP followed by the separate
+ READY plus steady-green completion gate;
- plugin-local diagnostics, metrics, API state, lifecycle mapping,
observation-source mapping and scoped styles;
-- typed v0.4.0 shadow application-control state/arm/disarm contracts. No button
- is rendered because the plugin has no live command transport;
+- typed v0.5.0 interactive application-control state plus legacy shadow
+ inspection contracts;
- `plugin.ts`, which binds the manifest `device.connection` component key to
the runtime provider and connection view.
diff --git a/plugins/xgrids-k1/frontend/src/api.ts b/plugins/xgrids-k1/frontend/src/api.ts
index 0ae19b7..e9f9002 100644
--- a/plugins/xgrids-k1/frontend/src/api.ts
+++ b/plugins/xgrids-k1/frontend/src/api.ts
@@ -104,6 +104,47 @@ export interface XgridsApplicationControlExecution {
can_emit_requests: false;
}
+export type XgridsApplicationControlPhase =
+ | "idle"
+ | "connecting"
+ | "connection-ready"
+ | "workspace-requested"
+ | "workspace-ready"
+ | "project-requested"
+ | "project-ready"
+ | "start-requested"
+ | "initializing"
+ | "scanning"
+ | "stop-requested"
+ | "stopping"
+ | "awaiting-standby-confirmation"
+ | "completed"
+ | "closed"
+ | "failed";
+
+export interface XgridsApplicationControlSession {
+ mode: "interactive-canonical";
+ state: XgridsApplicationControlPhase;
+ control_socket_open: boolean;
+ can_open: boolean;
+ can_enter_workspace: boolean;
+ can_prepare_project: boolean;
+ can_start: boolean;
+ can_stop: boolean;
+ can_confirm_standby: boolean;
+ pending_operator_action?: string | null;
+ scripted_transitions: false;
+ automatic_retry: false;
+ outcome_unknown: boolean;
+ failure?: {
+ code?: string;
+ message?: string;
+ safe_to_retry?: boolean;
+ } | null;
+ dialogue?: Record | null;
+ transport?: Record | null;
+}
+
export interface XgridsAcquisition {
schema_version?: string;
acquisition_id: string;
@@ -228,6 +269,7 @@ export interface XgridsK1State {
compatibility?: XgridsCompatibilityState | null;
modeling_control_safety?: XgridsModelingControlSafety | null;
application_control_execution?: XgridsApplicationControlExecution | null;
+ application_control_session?: XgridsApplicationControlSession | null;
device_ref?: XgridsDeviceRef | null;
device_session?: XgridsDeviceSession | null;
acquisition?: XgridsAcquisition | null;
@@ -288,6 +330,7 @@ export interface StartAcquisitionRequest {
operation_id?: string;
idempotency_key?: string;
deadline_seconds?: number;
+ physical_acceptance?: OperatorPresenceConfirmation;
}
export interface StopAcquisitionRequest {
@@ -297,6 +340,7 @@ export interface StopAcquisitionRequest {
operation_id?: string;
idempotency_key?: string;
deadline_seconds?: number;
+ physical_acceptance?: OperatorPresenceConfirmation;
}
export interface AbortAcquisitionRequest {
@@ -335,6 +379,23 @@ export interface ShadowApplicationControlArmRequest {
timezone_name: string;
}
+export interface OperatorPresenceConfirmation {
+ operator_present: true;
+ owner_controlled_device: true;
+ lixelgo_closed: true;
+ battery_storage_confirmed: true;
+ expected_physical_state_confirmed: true;
+}
+
+export interface OpenApplicationControlSessionRequest
+ extends OperatorPresenceConfirmation {
+ timezone_name: string;
+}
+
+export interface EnterApplicationWorkspaceRequest {
+ operator_confirmed: true;
+}
+
export class ApiError extends Error {
readonly status: number;
@@ -496,6 +557,22 @@ export const xgridsK1Api = {
disarmShadowApplicationControl(): Promise {
return invokeState(xgridsK1Actions.applicationControlShadowDisarm);
},
+
+ openApplicationControlSession(
+ body: OpenApplicationControlSessionRequest,
+ ): Promise {
+ return invokeState(xgridsK1Actions.applicationControlSessionOpen, body);
+ },
+
+ enterApplicationWorkspace(
+ body: EnterApplicationWorkspaceRequest,
+ ): Promise {
+ return invokeState(xgridsK1Actions.applicationControlWorkspaceEnter, body);
+ },
+
+ closeApplicationControlSession(): Promise {
+ return invokeState(xgridsK1Actions.applicationControlSessionClose);
+ },
};
export type EventSocketStatus = "connecting" | "open" | "closed" | "error";
diff --git a/plugins/xgrids-k1/frontend/src/components/K1AcquisitionPipeline.tsx b/plugins/xgrids-k1/frontend/src/components/K1AcquisitionPipeline.tsx
index 767a61b..21432b7 100644
--- a/plugins/xgrids-k1/frontend/src/components/K1AcquisitionPipeline.tsx
+++ b/plugins/xgrids-k1/frontend/src/components/K1AcquisitionPipeline.tsx
@@ -14,6 +14,7 @@ import { EXACT_PROFILE_ATTESTATION } from "../compatibility";
import { runAutomaticSpatialSourceStart } from "../automaticSourceStart";
import {
isConfirmedLiveState,
+ isSoftwareCommandedAcquisition,
isSourceRuntimeBusy,
isVendorWriteCapable,
recoverableAcquisition,
@@ -21,6 +22,7 @@ import {
} from "../lifecycle";
import { normalizeProjectName, validateProjectName } from "../projectName";
import type { XgridsK1Controller } from "../runtimeContext";
+import type { OperatorPresenceConfirmation } from "../api";
type SessionIntent = "live" | "replay";
@@ -29,6 +31,14 @@ const sessionItems = [
{ value: "replay", label: "Повтор записи" },
] satisfies Array<{ value: SessionIntent; label: string }>;
+const PHYSICAL_ACCEPTANCE = {
+ operator_present: true,
+ owner_controlled_device: true,
+ lixelgo_closed: true,
+ battery_storage_confirmed: true,
+ expected_physical_state_confirmed: true,
+} satisfies OperatorPresenceConfirmation;
+
export function K1AcquisitionPipeline({
controller,
profileConfirmed,
@@ -43,9 +53,14 @@ export function K1AcquisitionPipeline({
const {
state,
pendingAction,
- prepareAndStartAcquisition,
+ openApplicationControlSession,
+ enterApplicationWorkspace,
+ closeApplicationControlSession,
+ prepareAcquisition,
+ startPreparedAcquisition,
startReplay,
stop,
+ confirmStoppedAtSteadyGreen,
abort,
} = controller;
const [sessionIntent, setSessionIntent] = useState("live");
@@ -55,12 +70,15 @@ export function K1AcquisitionPipeline({
const [replayPath, setReplayPath] = useState("");
const [replaySpeed, setReplaySpeed] = useState("1");
const [replayLoop, setReplayLoop] = useState(false);
+ const [physicalAcceptanceConfirmed, setPhysicalAcceptanceConfirmed] = useState(false);
const hydratedAcquisitionId = useRef(null);
const activeAcquisition = recoverableAcquisition(state);
const preparedAcquisition = activeAcquisition?.state === "prepared" ? activeAcquisition : null;
const projectNameValidation = validateProjectName(projectName);
const vendorWriteCapable = isVendorWriteCapable(state);
+ const control = state?.application_control_session;
+ const controlPhase = control?.state ?? "idle";
useEffect(() => {
if (state?.source_mode === "live" || state?.source_mode === "replay") {
@@ -103,16 +121,29 @@ export function K1AcquisitionPipeline({
[sessionLocked],
);
- const submitLive = async () => {
+ const openControl = async () => {
+ if (!physicalAcceptanceConfirmed || !state?.k1_ip) return;
+ const timezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC";
+ await openApplicationControlSession({
+ ...PHYSICAL_ACCEPTANCE,
+ timezone_name: timezoneName,
+ });
+ };
+
+ const prepareProject = async () => {
setProjectNameTouched(true);
if (!profileConfirmed || sourceRuntimeBusy || projectNameValidation.error) return;
const targetHost = liveHost.trim();
+ await prepareAcquisition({
+ project_name: projectNameValidation.value,
+ ...(targetHost ? { host: targetHost } : {}),
+ compatibility_attestation: EXACT_PROFILE_ATTESTATION,
+ });
+ };
+
+ const startLive = async () => {
await runAutomaticSpatialSourceStart(
- () => prepareAndStartAcquisition({
- project_name: projectNameValidation.value,
- ...(targetHost ? { host: targetHost } : {}),
- compatibility_attestation: EXACT_PROFILE_ATTESTATION,
- }),
+ () => startPreparedAcquisition(PHYSICAL_ACCEPTANCE),
activateAutomaticSpatialSource,
openSpatialScene,
);
@@ -148,52 +179,114 @@ export function K1AcquisitionPipeline({
/>
{effectiveSessionIntent === "live" ? (
{!profileConfirmed
? "Сначала вручную подтвердите FW 3.0.2 и direct-LAN. Интерфейс не аттестует устройство автоматически."
- : liveTargetReady
- ? vendorWriteCapable
- ? "Mission Core подготовит локальную запись и отправит профилированную команду запуска K1. После подтверждения запуска начнётся статическая инициализация — не перемещайте устройство до появления потока."
- : "Лабораторный профиль подготовит локальный приёмник и перейдёт в ожидание. Затем физически запустите сканирование двойным нажатием кнопки устройства. Программная команда запуска на K1 не отправляется; поток подтверждается только реальными кадрами."
- : "Сначала подключите устройство к Wi‑Fi или укажите локальный адрес."}
+ : controlPhase === "failed"
+ ? `Диалог остановлен без автоповтора: ${control?.failure?.message || "требуется ручная проверка K1"}`
+ : controlPhase === "connecting"
+ ? "Выполняются только операции 1–6 записанного диалога. Следующий этап начнётся только по вашей кнопке."
+ : controlPhase === "workspace-requested"
+ ? "Выполняется только операция входа в рабочее пространство."
+ : controlPhase === "project-requested"
+ ? "Выполняются операции открытия проектного шага; имя ещё не отправляется на K1."
+ : controlPhase === "start-requested" || controlPhase === "initializing"
+ ? "Калибровка оборудования. Не перемещайте K1; никаких временных автопереходов и повторов нет."
+ : controlPhase === "scanning"
+ ? "K1 подтвердил SCANNING и инициализацию. Остановка доступна в пространственной сцене."
+ : "Каждый этап канонического диалога запускается отдельным действием оператора."}
) : (
@@ -219,7 +312,22 @@ export function K1AcquisitionPipeline({
: "Остановка завершает только локальный приём и сохранение. Физическое состояние сканера остаётся неизвестным."
: "Активного источника сейчас нет."}