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.
655 lines
18 KiB
TypeScript
655 lines
18 KiB
TypeScript
import type { ViewerSettings } from "@mission-core/plugin-sdk";
|
|
import { xgridsK1Actions, xgridsK1Manifest } from "./manifest";
|
|
|
|
const PLUGIN_ID = xgridsK1Manifest.metadata.id;
|
|
|
|
export interface BleDevice {
|
|
device_id: string;
|
|
name?: string | null;
|
|
rssi?: number | null;
|
|
address?: string | null;
|
|
connectable?: boolean | null;
|
|
likely_k1?: boolean | null;
|
|
}
|
|
|
|
export type SourceMode = "idle" | "live" | "replay";
|
|
|
|
export type AcquisitionState =
|
|
| "preparing"
|
|
| "prepared"
|
|
| "awaiting_external_start"
|
|
| "starting"
|
|
| "acquiring"
|
|
| "awaiting_external_stop"
|
|
| "stopping"
|
|
| "finalizing"
|
|
| "completed"
|
|
| "failed"
|
|
| "aborted"
|
|
| "interrupted";
|
|
|
|
export type OperationStatus =
|
|
| "accepted"
|
|
| "running"
|
|
| "operator_action_required"
|
|
| "succeeded"
|
|
| "failed"
|
|
| "cancelled"
|
|
| "timed_out"
|
|
| "interrupted";
|
|
|
|
export interface XgridsDeviceRef {
|
|
device_id: string;
|
|
model_id: string;
|
|
identity_stability: "stable" | "provisional";
|
|
identity_basis:
|
|
| "hardware-identifier"
|
|
| "plugin-derived"
|
|
| "operator-assigned"
|
|
| "transport-local";
|
|
transport_alias?: string | null;
|
|
}
|
|
|
|
export interface XgridsDeviceSession {
|
|
device_session_id: string;
|
|
device_id: string;
|
|
opened_at?: string | null;
|
|
compatibility_profile_id?: string | null;
|
|
connectivity?: "unknown" | "offline" | "connecting" | "connected" | "degraded";
|
|
}
|
|
|
|
export interface XgridsCompatibilityState {
|
|
profile_id?: string | null;
|
|
decision?: "compatible" | "limited" | "unknown" | "incompatible";
|
|
permitted_mode?: "blocked" | "evidence-only" | "read-only" | "active-control";
|
|
firmware_claim?: string | null;
|
|
vendor_writes_enabled?: boolean;
|
|
camera_preview?: string | null;
|
|
attestation?: CompatibilityAttestation | null;
|
|
}
|
|
|
|
export interface XgridsModelingControlSafety {
|
|
mode: "shadow-only";
|
|
status_reports_observed: number;
|
|
decode_errors: number;
|
|
vendor_identity_observed: boolean;
|
|
device_serial_observed: boolean;
|
|
identity_conflict: boolean;
|
|
session_state?: string | null;
|
|
project_bound: boolean;
|
|
ready_for_start_shadow: boolean;
|
|
ready_for_stop_shadow: boolean;
|
|
vendor_writes_enabled: false;
|
|
publisher_installed: false;
|
|
}
|
|
|
|
export interface XgridsApplicationControlExecution {
|
|
mode: "dormant-write-disabled";
|
|
state: "disarmed" | "armed-shadow-only" | "expired" | "closed";
|
|
lease?: {
|
|
state: "armed" | "expired" | "closed";
|
|
remaining_seconds: number;
|
|
authority_cached: boolean;
|
|
exportable: false;
|
|
} | null;
|
|
orchestrator?: Record<string, unknown> | null;
|
|
publisher: {
|
|
mode: "write-disabled";
|
|
denied_attempts: number;
|
|
transport_calls: 0;
|
|
vendor_writes_enabled: false;
|
|
publisher_armed: false;
|
|
automatic_retry: false;
|
|
};
|
|
live_transport_installed: false;
|
|
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;
|
|
reason_code?: string;
|
|
message?: string;
|
|
failed_phase?: string | null;
|
|
dialogue_stage?: string | null;
|
|
transport_state?: string | null;
|
|
publish_attempts?: number | null;
|
|
qos2_completions?: number | null;
|
|
correlated_responses?: number | null;
|
|
ignored_known_responses?: number | null;
|
|
late_known_responses?: number | null;
|
|
modeling_command_attempted?: boolean | null;
|
|
diagnostic_snapshot_unavailable?: string[];
|
|
diagnostic_evidence_unavailable?: string[];
|
|
correlation_failure?: {
|
|
phase?: string;
|
|
operation_key?: string;
|
|
response_topic?: string;
|
|
reason_code?: string;
|
|
reason?: string;
|
|
} | null;
|
|
compatibility_failure?: {
|
|
phase?: string;
|
|
operation_key?: string;
|
|
response_topic?: string;
|
|
reason_code?: string;
|
|
expected?: Record<string, unknown>;
|
|
observed?: Record<string, unknown>;
|
|
} | null;
|
|
safe_to_retry?: boolean;
|
|
} | null;
|
|
dialogue?: Record<string, unknown> | null;
|
|
transport?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface XgridsAcquisition {
|
|
schema_version?: string;
|
|
acquisition_id: string;
|
|
device_id: string;
|
|
device_session_id: string;
|
|
compatibility_profile_id: string;
|
|
control_mode: "operator-manual" | "plugin-commanded" | "observe-only";
|
|
project_name?: string | null;
|
|
mount_type?: "handheld" | null;
|
|
gnss_mode?: "none" | null;
|
|
cleanup_pending?: boolean;
|
|
requested_streams: string[];
|
|
target_host: string;
|
|
duration_seconds: number | null;
|
|
evidence_policy: "required" | "best-effort" | "disabled";
|
|
state: AcquisitionState;
|
|
state_revision: number;
|
|
created_at?: string | null;
|
|
updated_at?: string | null;
|
|
message_code?: string | null;
|
|
operator_instructions?: string[];
|
|
result?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface XgridsOperation {
|
|
schema_version?: string;
|
|
operation_id: string;
|
|
action: string;
|
|
status: OperationStatus;
|
|
accepted_at?: string | null;
|
|
completed_at?: string | null;
|
|
deadline_at?: string | null;
|
|
device_id?: string | null;
|
|
device_session_id?: string | null;
|
|
idempotency_key?: string | null;
|
|
stage_code?: string | null;
|
|
message_code?: string | null;
|
|
sequence?: number;
|
|
state_revision?: number;
|
|
cancellable?: boolean;
|
|
cancel_requested?: boolean;
|
|
result?: Record<string, unknown> | null;
|
|
error?: Record<string, unknown> | null;
|
|
evidence_refs?: string[];
|
|
}
|
|
|
|
export interface XgridsK1Metrics {
|
|
mqtt_to_decode_ms?: number | null;
|
|
decode_ms?: number | null;
|
|
publish_ms?: number | null;
|
|
pipeline_ms?: number | null;
|
|
end_to_end_ms?: number | null;
|
|
frame_rate?: number | null;
|
|
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;
|
|
device_speed_mps?: number | null;
|
|
elapsed_seconds?: number | null;
|
|
route_distance_meters?: number | null;
|
|
speed_meters_per_second?: number | null;
|
|
[key: string]: number | null | undefined;
|
|
}
|
|
|
|
export interface XgridsCameraPreviewDelivery {
|
|
id: string;
|
|
kind: "mse-fmp4-websocket";
|
|
url: string;
|
|
media_type: string;
|
|
}
|
|
|
|
export interface XgridsCameraPreviewActivation {
|
|
group_id: string;
|
|
max_active: number;
|
|
selected: boolean;
|
|
controllable: boolean;
|
|
}
|
|
|
|
export interface XgridsCameraPreviewState {
|
|
phase?: string | null;
|
|
revision?: number | null;
|
|
generation?: number | null;
|
|
active_source_id?: string | null;
|
|
delivery?: XgridsCameraPreviewDelivery | null;
|
|
}
|
|
|
|
export interface XgridsSensorCatalogStream {
|
|
stream_id: string;
|
|
source_id?: string | null;
|
|
semantic_channel_id?: string | null;
|
|
label?: string | null;
|
|
sensor_kind?: string | null;
|
|
modality?: string | null;
|
|
availability?: string | null;
|
|
endpoint_label?: string | null;
|
|
activation?: XgridsCameraPreviewActivation | null;
|
|
delivery?: XgridsCameraPreviewDelivery | null;
|
|
decode_status?: string | null;
|
|
frame_id?: string | null;
|
|
coordinate_convention?: string | null;
|
|
}
|
|
|
|
export interface XgridsSensorCatalog {
|
|
schema_version?: string | null;
|
|
revision?: string | null;
|
|
streams?: XgridsSensorCatalogStream[];
|
|
}
|
|
|
|
export interface XgridsK1State {
|
|
contract_version?: string | null;
|
|
phase?: string | null;
|
|
message?: string | null;
|
|
devices?: BleDevice[];
|
|
selected_device_id?: string | null;
|
|
k1_ip?: string | null;
|
|
connection_mode?: "bridge" | "quick-connect" | "direct-connect" | null;
|
|
foxglove_ws_url?: string | null;
|
|
foxglove_viewer_url?: string | null;
|
|
rerun_grpc_url?: string | null;
|
|
viewer_settings?: ViewerSettings | null;
|
|
source_mode?: SourceMode | null;
|
|
metrics?: XgridsK1Metrics;
|
|
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;
|
|
operations?: XgridsOperation[];
|
|
last_operation?: XgridsOperation | null;
|
|
sensor_catalog?: XgridsSensorCatalog | null;
|
|
camera_preview?: XgridsCameraPreviewState | null;
|
|
}
|
|
|
|
export interface HealthResponse {
|
|
ok?: boolean;
|
|
status?: string;
|
|
service?: string;
|
|
version?: string;
|
|
}
|
|
|
|
export interface ScanRequest {
|
|
duration_seconds?: number;
|
|
}
|
|
|
|
export interface CompatibilityAttestation {
|
|
firmware_version: "3.0.2";
|
|
topology: "direct-lan" | "device-ap" | "controller-hotspot";
|
|
verification: "live-device-info";
|
|
}
|
|
|
|
export interface ConnectRequest {
|
|
device_id: string;
|
|
ssid?: string;
|
|
password?: string;
|
|
connection_mode: "bridge" | "quick-connect" | "direct-connect";
|
|
compatibility_attestation: CompatibilityAttestation;
|
|
operation_id?: string;
|
|
idempotency_key?: string;
|
|
}
|
|
|
|
export interface PrepareAcquisitionRequest {
|
|
project_name: string;
|
|
mount_type: "handheld";
|
|
gnss_mode: "none";
|
|
host?: string;
|
|
duration_seconds?: number;
|
|
requested_streams?: RequestedStreamId[];
|
|
evidence_policy?: "required" | "best-effort" | "disabled";
|
|
compatibility_attestation: CompatibilityAttestation;
|
|
operation_id?: string;
|
|
idempotency_key?: string;
|
|
deadline_seconds?: number;
|
|
}
|
|
|
|
export type RequestedStreamId =
|
|
| "spatial.point-cloud.live"
|
|
| "spatial.pose.live"
|
|
| "device.modeling.live"
|
|
| "device.status.live"
|
|
| "device.heartbeat.live";
|
|
|
|
export interface StartAcquisitionRequest {
|
|
acquisition_id: string;
|
|
expected_state_revision?: number;
|
|
operation_id?: string;
|
|
idempotency_key?: string;
|
|
deadline_seconds?: number;
|
|
physical_acceptance?: OperatorPresenceConfirmation;
|
|
}
|
|
|
|
export interface StopAcquisitionRequest {
|
|
acquisition_id: string;
|
|
mode: "capture-only" | "graceful";
|
|
operator_confirmed?: boolean;
|
|
operation_id?: string;
|
|
idempotency_key?: string;
|
|
deadline_seconds?: number;
|
|
physical_acceptance?: OperatorPresenceConfirmation;
|
|
}
|
|
|
|
export interface AbortAcquisitionRequest {
|
|
acquisition_id: string;
|
|
operation_id?: string;
|
|
idempotency_key?: string;
|
|
deadline_seconds?: number;
|
|
}
|
|
|
|
export interface CompatibilityLiveRequest {
|
|
project_name: string;
|
|
host?: string;
|
|
duration_seconds?: number;
|
|
compatibility_attestation: CompatibilityAttestation;
|
|
}
|
|
|
|
export interface ReplayRequest {
|
|
path: string;
|
|
speed?: number;
|
|
loop?: boolean;
|
|
}
|
|
|
|
export interface SelectCameraPreviewRequest {
|
|
source_id: string;
|
|
device_session_id: string;
|
|
}
|
|
|
|
export interface StopCameraPreviewRequest {
|
|
device_session_id: string;
|
|
generation: number;
|
|
}
|
|
|
|
export interface ShadowApplicationControlArmRequest {
|
|
operator_confirmed: true;
|
|
lease_seconds?: number;
|
|
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;
|
|
readonly transportUnavailable: boolean;
|
|
|
|
constructor(message: string, status = 0, transportUnavailable = false) {
|
|
super(message);
|
|
this.name = "ApiError";
|
|
this.status = status;
|
|
this.transportUnavailable = transportUnavailable;
|
|
}
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function unwrapState(payload: unknown): XgridsK1State {
|
|
const value = isRecord(payload) && isRecord(payload.state) ? payload.state : payload;
|
|
|
|
if (!isRecord(value)) {
|
|
throw new ApiError("Локальный сервер вернул некорректное состояние.");
|
|
}
|
|
|
|
return value as XgridsK1State;
|
|
}
|
|
|
|
async function requestJson(path: string, init?: RequestInit): Promise<unknown> {
|
|
let response: Response;
|
|
|
|
try {
|
|
response = await fetch(path, {
|
|
...init,
|
|
headers: {
|
|
Accept: "application/json",
|
|
...(init?.body ? { "Content-Type": "application/json" } : {}),
|
|
...init?.headers,
|
|
},
|
|
});
|
|
} catch {
|
|
throw new ApiError(
|
|
"Не удалось подключиться к локальному сервису устройства.",
|
|
0,
|
|
true,
|
|
);
|
|
}
|
|
|
|
const bodyText = await response.text();
|
|
let body: unknown;
|
|
|
|
if (bodyText) {
|
|
try {
|
|
body = JSON.parse(bodyText) as unknown;
|
|
} catch {
|
|
body = bodyText;
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const detail =
|
|
isRecord(body) && typeof body.detail === "string"
|
|
? body.detail
|
|
: typeof body === "string" && body.trim()
|
|
? body.trim()
|
|
: `Запрос к API устройства завершился ошибкой HTTP ${response.status}.`;
|
|
throw new ApiError(
|
|
detail || `Запрос к API устройства завершился ошибкой HTTP ${response.status}.`,
|
|
response.status,
|
|
);
|
|
}
|
|
|
|
return body;
|
|
}
|
|
|
|
async function postState(path: string, body?: object): Promise<XgridsK1State> {
|
|
const payload = await requestJson(path, {
|
|
method: "POST",
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
|
|
if (payload === undefined) {
|
|
return xgridsK1Api.getState();
|
|
}
|
|
|
|
return unwrapState(payload);
|
|
}
|
|
|
|
function invokeState(actionId: string, input: object = {}): Promise<XgridsK1State> {
|
|
return postState(
|
|
`/api/v1/device-plugins/${encodeURIComponent(PLUGIN_ID)}/actions/${encodeURIComponent(actionId)}`,
|
|
{ input },
|
|
);
|
|
}
|
|
|
|
export const xgridsK1Api = {
|
|
async getHealth(): Promise<HealthResponse> {
|
|
const payload = await requestJson("/api/health");
|
|
if (!isRecord(payload)) {
|
|
throw new ApiError("Локальный сервер вернул некорректный ответ проверки.");
|
|
}
|
|
return payload as HealthResponse;
|
|
},
|
|
|
|
async getState(): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.stateRead);
|
|
},
|
|
|
|
scanBle(body: ScanRequest = {}): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.discoveryScan, body);
|
|
},
|
|
|
|
connect(body: ConnectRequest): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.networkProvision, body);
|
|
},
|
|
|
|
prepareAcquisition(body: PrepareAcquisitionRequest): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.acquisitionPrepare, body);
|
|
},
|
|
|
|
startAcquisition(body: StartAcquisitionRequest): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.acquisitionStart, body);
|
|
},
|
|
|
|
stopAcquisition(body: StopAcquisitionRequest): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.acquisitionStop, body);
|
|
},
|
|
|
|
abortAcquisition(body: AbortAcquisitionRequest): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.acquisitionAbort, body);
|
|
},
|
|
|
|
startReplay(body: ReplayRequest): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.streamStartReplay, body);
|
|
},
|
|
|
|
startLiveCompatibility(body: CompatibilityLiveRequest): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.compatibilityStreamStartLive, body);
|
|
},
|
|
|
|
stopSessionCompatibility(): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.compatibilityStreamStop);
|
|
},
|
|
|
|
selectCameraPreview(body: SelectCameraPreviewRequest): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.cameraPreviewSelect, body);
|
|
},
|
|
|
|
stopCameraPreview(body: StopCameraPreviewRequest): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.cameraPreviewStop, body);
|
|
},
|
|
|
|
updateViewerSettings(body: ViewerSettings): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.viewerSettingsUpdate, body);
|
|
},
|
|
|
|
getShadowApplicationControlState(): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.applicationControlShadowState);
|
|
},
|
|
|
|
armShadowApplicationControl(
|
|
body: ShadowApplicationControlArmRequest,
|
|
): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.applicationControlShadowArm, body);
|
|
},
|
|
|
|
disarmShadowApplicationControl(): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.applicationControlShadowDisarm);
|
|
},
|
|
|
|
openApplicationControlSession(
|
|
body: OpenApplicationControlSessionRequest,
|
|
): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.applicationControlSessionOpen, body);
|
|
},
|
|
|
|
enterApplicationWorkspace(
|
|
body: EnterApplicationWorkspaceRequest,
|
|
): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.applicationControlWorkspaceEnter, body);
|
|
},
|
|
|
|
closeApplicationControlSession(): Promise<XgridsK1State> {
|
|
return invokeState(xgridsK1Actions.applicationControlSessionClose);
|
|
},
|
|
};
|
|
|
|
export type EventSocketStatus = "connecting" | "open" | "closed" | "error";
|
|
|
|
function eventSocketUrl(): string {
|
|
const url = new URL(
|
|
`/api/v1/device-plugins/${encodeURIComponent(PLUGIN_ID)}/events`,
|
|
window.location.href,
|
|
);
|
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
return url.toString();
|
|
}
|
|
|
|
export function openEventSocket(
|
|
onState: (state: XgridsK1State) => void,
|
|
onStatus: (status: EventSocketStatus) => void,
|
|
): () => void {
|
|
onStatus("connecting");
|
|
const socket = new WebSocket(eventSocketUrl());
|
|
|
|
socket.addEventListener("open", () => onStatus("open"));
|
|
socket.addEventListener("message", (event) => {
|
|
try {
|
|
const payload = JSON.parse(String(event.data)) as unknown;
|
|
onState(unwrapState(payload));
|
|
} catch {
|
|
// The REST poll remains authoritative if an unrelated event is received.
|
|
}
|
|
});
|
|
socket.addEventListener("error", () => onStatus("error"));
|
|
socket.addEventListener("close", () => onStatus("closed"));
|
|
|
|
return () => socket.close(1000, "Пункт управления закрыт");
|
|
}
|