feat(perception): integrate calibrated operator pipeline
Add calibrated K1 projection, recorded and near-live perception qualification, unified Rerun operator layers, bounded replay admission, audited viewer controls, worker experiments, and lab evidence.
This commit is contained in:
@@ -186,7 +186,7 @@ export interface XgridsAcquisition {
|
||||
cleanup_pending?: boolean;
|
||||
requested_streams: string[];
|
||||
target_host: string;
|
||||
duration_seconds: number;
|
||||
duration_seconds: number | null;
|
||||
evidence_policy: "required" | "best-effort" | "disabled";
|
||||
state: AcquisitionState;
|
||||
state_revision: number;
|
||||
@@ -229,6 +229,11 @@ export interface XgridsK1Metrics {
|
||||
frame_rate_hz?: number | null;
|
||||
point_count?: number | null;
|
||||
dropped_preview_frames?: number | null;
|
||||
ai_end_to_end_ms?: number | null;
|
||||
ai_end_to_end_p95_ms?: number | null;
|
||||
ai_frame_rate_hz?: number | null;
|
||||
ai_dropped_frames?: number | null;
|
||||
ai_stale_ms?: number | null;
|
||||
device_elapsed_seconds?: number | null;
|
||||
device_route_distance_meters?: number | null;
|
||||
device_speed_meters_per_second?: number | null;
|
||||
@@ -329,8 +334,8 @@ export interface CompatibilityAttestation {
|
||||
|
||||
export interface ConnectRequest {
|
||||
device_id: string;
|
||||
ssid: string;
|
||||
password: string;
|
||||
ssid?: string;
|
||||
password?: string;
|
||||
connection_mode: "bridge" | "quick-connect" | "direct-connect";
|
||||
compatibility_attestation: CompatibilityAttestation;
|
||||
operation_id?: string;
|
||||
|
||||
@@ -23,8 +23,8 @@ import type { XgridsK1Controller } from "../runtimeContext";
|
||||
|
||||
const connectionCopy: Record<ConnectionMode, {
|
||||
stepTitle: string;
|
||||
ssidLabel: string;
|
||||
ssidPlaceholder: string;
|
||||
ssidLabel?: string;
|
||||
ssidPlaceholder?: string;
|
||||
buttonLabel: string;
|
||||
safetyNote: string;
|
||||
}> = {
|
||||
@@ -36,18 +36,16 @@ const connectionCopy: Record<ConnectionMode, {
|
||||
safetyNote: "K1 получит реквизиты существующей сети одним рассмотренным BLE-запросом без автоматического повтора.",
|
||||
},
|
||||
"quick-connect": {
|
||||
stepTitle: "Подключитесь к точке доступа K1",
|
||||
ssidLabel: "Название точки доступа K1",
|
||||
ssidPlaceholder: "SSID сканера, например XGR-…",
|
||||
buttonLabel: "Подключить этот Mac к K1",
|
||||
safetyNote: "Введите SSID и пароль точки доступа вашего K1. Mac сменит текущую Wi‑Fi сеть одним CoreWLAN-запросом; недокументированный BLE-секрет не читается и K1 не получает BLE-запись.",
|
||||
stepTitle: "Включите точку доступа K1 и подключитесь к ней",
|
||||
buttonLabel: "Включить точку K1 и подключиться",
|
||||
safetyNote: "Mission Core сначала проверит локальный device-scoped профиль выбранного K1. Если профиль отсутствует, операция остановится до BLE-записи. После preflight Mission Core отправит один рассмотренный AP-enable кадр и найдёт точный SSID выбранного устройства.",
|
||||
},
|
||||
"direct-connect": {
|
||||
stepTitle: "Подключите K1 к хотспоту контроллера",
|
||||
ssidLabel: "Название хотспота контроллера",
|
||||
ssidPlaceholder: "SSID управляющего устройства",
|
||||
buttonLabel: "Подключить K1 к хотспоту",
|
||||
safetyNote: "Хотспот должен быть уже включён, а этот Mac — иметь к нему маршрут. K1 получит его реквизиты одним рассмотренным BLE-запросом.",
|
||||
safetyNote: "Хотспот должен быть уже включён, а управляющее устройство — иметь к нему маршрут. K1 получит его реквизиты одним рассмотренным BLE-запросом.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -131,7 +129,8 @@ export function K1ProvisioningPipeline({
|
||||
const provisioningIntentRef = useRef<string | null>(null);
|
||||
const devices = state?.devices ?? [];
|
||||
const isBusy = pendingAction !== null;
|
||||
const credentialsReady = ssid.trim().length > 0 && password.length > 0;
|
||||
const credentialsReady = connectionMode === "quick-connect"
|
||||
|| (ssid.trim().length > 0 && password.length > 0);
|
||||
const canConnect = powerConfirmed && selectedDeviceId.length > 0 && credentialsReady && !isBusy;
|
||||
const modeCopy = connectionCopy[connectionMode];
|
||||
const selectedModeConnected = Boolean(
|
||||
@@ -170,10 +169,12 @@ export function K1ProvisioningPipeline({
|
||||
if (!canConnect) return;
|
||||
const idempotencyKey = provisioningIntentKey(provisioningIntentRef.current);
|
||||
provisioningIntentRef.current = idempotencyKey;
|
||||
const networkCredentials = connectionMode === "quick-connect"
|
||||
? {}
|
||||
: { ssid: ssid.trim(), password };
|
||||
const succeeded = await connect({
|
||||
device_id: selectedDeviceId,
|
||||
ssid: ssid.trim(),
|
||||
password,
|
||||
...networkCredentials,
|
||||
connection_mode: connectionMode,
|
||||
compatibility_attestation: profileSelectionForConnectionMode(connectionMode),
|
||||
idempotency_key: idempotencyKey,
|
||||
@@ -181,6 +182,11 @@ export function K1ProvisioningPipeline({
|
||||
if (succeeded) {
|
||||
provisioningIntentRef.current = null;
|
||||
setPassword("");
|
||||
} else if (connectionMode === "quick-connect") {
|
||||
// The backend has already persisted and reconciled the failed bounded
|
||||
// attempt. A later click is a new explicit Quick Connect intent, not an
|
||||
// automatic replay of the consumed operation key.
|
||||
provisioningIntentRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -252,15 +258,27 @@ export function K1ProvisioningPipeline({
|
||||
status={selectedModeConnected ? "Адрес получен" : "Ожидает подключения"}
|
||||
tone={selectedModeConnected ? "success" : "neutral"}
|
||||
>
|
||||
<div className="field-stack">
|
||||
<TextField label={modeCopy.ssidLabel} hint="SSID" value={ssid} onChange={(event) => { setSsid(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" spellCheck={false} placeholder={modeCopy.ssidPlaceholder} />
|
||||
<TextField label="Пароль Wi‑Fi" hint="Только в оперативной памяти" type="password" value={password} onChange={(event) => { setPassword(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" placeholder="Введите пароль" />
|
||||
</div>
|
||||
{connectionMode === "quick-connect" ? (
|
||||
<div className="connection-summary">
|
||||
<span>Канонический путь</span>
|
||||
<strong>BLE включает AP → macOS подключает Mac к AP выбранного K1</strong>
|
||||
</div>
|
||||
) : (
|
||||
<div className="field-stack">
|
||||
<TextField label={modeCopy.ssidLabel ?? "Название сети Wi‑Fi"} hint="SSID" value={ssid} onChange={(event) => { setSsid(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" spellCheck={false} placeholder={modeCopy.ssidPlaceholder} />
|
||||
<TextField label="Пароль Wi‑Fi" hint="Только в оперативной памяти" type="password" value={password} onChange={(event) => { setPassword(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" placeholder="Введите пароль" />
|
||||
</div>
|
||||
)}
|
||||
<div className="connection-summary"><span>Устройство</span><strong>{deviceSummary?.name || selectedDeviceId || "Сначала выберите устройство"}</strong></div>
|
||||
<Button width="full" variant="primary" icon={<Icon name="network" />} disabled={!canConnect} onClick={() => void submitConnect()}>
|
||||
{pendingAction === "connect" ? "Подключаем…" : modeCopy.buttonLabel}
|
||||
{pendingAction === "connect"
|
||||
? connectionMode === "quick-connect" ? "Включаем точку и подключаем…" : "Подключаем…"
|
||||
: modeCopy.buttonLabel}
|
||||
</Button>
|
||||
<p className="safety-note">{modeCopy.safetyNote} Пароль передаётся только локальному сервису, не сохраняется в браузере и удаляется из формы после успеха.</p>
|
||||
<p className="safety-note">
|
||||
{modeCopy.safetyNote}
|
||||
{connectionMode === "quick-connect" ? " Это лабораторный путь для уже подготовленного хоста: credential provider должен существовать в системном хранилище заранее. На чистом Mac операция завершится до BLE-записи; браузер, API, журналы и evidence секрета не получают." : " Пароль передаётся только локальному сервису, не сохраняется в браузере и удаляется из формы после успеха."}
|
||||
</p>
|
||||
</WizardStep>
|
||||
</div>
|
||||
</GlassSurface>
|
||||
|
||||
@@ -17,7 +17,7 @@ export const connectionModeOptions: Array<SelectOption<ConnectionMode>> = [
|
||||
{
|
||||
value: "quick-connect",
|
||||
label: "Точка доступа K1 · Quick Connect",
|
||||
description: "Этот Mac один раз подключается к Wi‑Fi сканера; K1 остаётся точкой доступа.",
|
||||
description: "Лабораторный режим: Mission Core включает AP K1 и подключает только заранее подготовленный хост. Для обычной работы используйте Bridge.",
|
||||
},
|
||||
{
|
||||
value: "direct-connect",
|
||||
|
||||
@@ -108,6 +108,10 @@ function normalizeState(
|
||||
frameRateHz: finiteMetric(metrics?.frame_rate ?? metrics?.frame_rate_hz),
|
||||
pointCount: finiteMetric(metrics?.point_count),
|
||||
droppedPreviewFrames: finiteMetric(metrics?.dropped_preview_frames),
|
||||
aiLatencyMs: finiteMetric(metrics?.ai_end_to_end_ms),
|
||||
aiFrameRateHz: finiteMetric(metrics?.ai_frame_rate_hz),
|
||||
aiDroppedFrames: finiteMetric(metrics?.ai_dropped_frames),
|
||||
aiStaleMs: finiteMetric(metrics?.ai_stale_ms),
|
||||
elapsedSeconds: telemetry.elapsedSeconds,
|
||||
routeDistanceMeters: telemetry.routeDistanceMeters,
|
||||
speedMetersPerSecond: telemetry.speedMetersPerSecond,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type OperatorPresenceConfirmation,
|
||||
type PrepareAcquisitionRequest,
|
||||
type ReplayRequest,
|
||||
type XgridsOperation,
|
||||
type XgridsApplicationControlPhase,
|
||||
type XgridsK1State,
|
||||
} from "./api";
|
||||
@@ -186,6 +187,34 @@ function messageFor(error: unknown): string {
|
||||
return "Запрос к локальному сервису устройства завершился ошибкой.";
|
||||
}
|
||||
|
||||
function networkProvisionFailureMessage(
|
||||
operation: XgridsOperation | null | undefined,
|
||||
): string | null {
|
||||
if (!operation || operation.status !== "failed") return null;
|
||||
const code = operation.error?.code;
|
||||
if (typeof code !== "string") return null;
|
||||
|
||||
const messages: Record<string, string> = {
|
||||
"network-not-found":
|
||||
"Точка доступа выбранного K1 не найдена. Команда включения точки не повторялась; проверьте питание и состояние K1.",
|
||||
"credential-entry-cancelled":
|
||||
"Первичная регистрация пароля K1 отменена. Получите пароль сохранённой сети этого K1 на авторизованном устройстве и повторите подключение отдельным действием.",
|
||||
"credential-invalid":
|
||||
"Пароль точки доступа K1 имеет недопустимую длину. Получите сохранённый пароль этого K1 в LixelGO/iPhone и повторите подключение.",
|
||||
"host-wifi-operation-timeout":
|
||||
"Первичное системное подключение к K1 не было завершено вовремя. BLE-команда автоматически не повторялась; получите пароль сохранённой сети этого K1 и запустите подключение заново.",
|
||||
"profile-ssid-mismatch":
|
||||
"Сохранённый профиль относится к другому K1. Подключение остановлено без повторной команды сканеру.",
|
||||
"corewlan-error":
|
||||
"macOS не смогла подключиться к точке доступа K1. Проверьте пароль сохранённой сети этого K1; автоматического повтора не было.",
|
||||
"wifi-interface-unavailable":
|
||||
"Системный Wi-Fi-интерфейс macOS недоступен. Команда сканеру автоматически не повторялась.",
|
||||
"unsupported-platform":
|
||||
"Для этой операционной системы адаптер подключения к точке K1 ещё не реализован.",
|
||||
};
|
||||
return messages[code] ?? null;
|
||||
}
|
||||
|
||||
function measuredLatency(state: XgridsK1State | null): number | null {
|
||||
if (state?.source_mode !== "live") return null;
|
||||
const metrics = state?.metrics;
|
||||
@@ -318,7 +347,29 @@ export function useXgridsK1Runtime(enabled: boolean) {
|
||||
);
|
||||
}
|
||||
|
||||
const nextState = await xgridsK1Api.connect(request);
|
||||
let nextState: XgridsK1State;
|
||||
try {
|
||||
nextState = await xgridsK1Api.connect(request);
|
||||
} catch (connectError) {
|
||||
// A failed network action is terminal and persisted by the backend.
|
||||
// Re-read state only; never replay the device command. This replaces
|
||||
// an opaque HTTP 502 banner with the exact host-association outcome.
|
||||
let failedState: XgridsK1State;
|
||||
try {
|
||||
failedState = await xgridsK1Api.getState();
|
||||
} catch {
|
||||
throw connectError;
|
||||
}
|
||||
acceptState(failedState);
|
||||
const failedOperation = operationByIdempotencyKey(
|
||||
failedState,
|
||||
"network.provision",
|
||||
request.idempotency_key,
|
||||
);
|
||||
const failureMessage = networkProvisionFailureMessage(failedOperation);
|
||||
if (failureMessage) throw new ApiError(failureMessage);
|
||||
throw connectError;
|
||||
}
|
||||
const operation = operationByIdempotencyKey(
|
||||
nextState,
|
||||
"network.provision",
|
||||
@@ -334,7 +385,7 @@ export function useXgridsK1Runtime(enabled: boolean) {
|
||||
}
|
||||
return nextState;
|
||||
}),
|
||||
[run, state],
|
||||
[acceptState, run, state],
|
||||
);
|
||||
|
||||
const openApplicationControlSession = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user