feat(plugins): isolate device integrations

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 19:29:32 +03:00
parent f9ffb7bd1c
commit 24a47318f2
122 changed files with 3304 additions and 1892 deletions
+461
View File
@@ -0,0 +1,461 @@
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;
}
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";
requested_streams: string[];
target_host: string;
duration_seconds: number;
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;
[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;
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;
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";
operator_confirmed: true;
}
export interface ConnectRequest {
device_id: string;
ssid: string;
password: string;
compatibility_attestation: CompatibilityAttestation;
operation_id?: string;
idempotency_key?: string;
}
export interface PrepareAcquisitionRequest {
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.status.live"
| "device.heartbeat.live";
export interface StartAcquisitionRequest {
acquisition_id: string;
expected_state_revision?: number;
operation_id?: string;
idempotency_key?: string;
deadline_seconds?: number;
}
export interface StopAcquisitionRequest {
acquisition_id: string;
mode: "capture-only" | "graceful";
operator_confirmed?: boolean;
operation_id?: string;
idempotency_key?: string;
deadline_seconds?: number;
}
export interface AbortAcquisitionRequest {
acquisition_id: string;
operation_id?: string;
idempotency_key?: string;
deadline_seconds?: number;
}
export interface CompatibilityLiveRequest {
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 class ApiError extends Error {
readonly status: number;
constructor(message: string, status = 0) {
super(message);
this.name = "ApiError";
this.status = status;
}
}
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("Не удалось подключиться к локальному сервису устройства.");
}
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);
},
};
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, "Пункт управления закрыт");
}