170 lines
6.0 KiB
TypeScript
170 lines
6.0 KiB
TypeScript
import { createContext, useContext, useEffect, type ReactNode } from "react";
|
|
|
|
import {
|
|
MissionRuntimeProvider,
|
|
useMissionRuntime,
|
|
type DeviceModelDefinition,
|
|
type MissionRuntimeController,
|
|
type MissionRuntimeState,
|
|
} from "@mission-core/plugin-sdk";
|
|
import {
|
|
confirmedRuntimeSourceMode,
|
|
effectiveAcquisition,
|
|
normalizeRuntimePhase,
|
|
spatialSourceId,
|
|
} from "./lifecycle";
|
|
import { localizeRuntimeMessage } from "./messages";
|
|
import { xgridsK1Manifest } from "./manifest";
|
|
import { deviceTelemetry, finiteMetric, pipelineLatency } from "./presentation";
|
|
import { xgridsK1ObservationSources } from "./observationSources";
|
|
import { useXgridsK1Runtime } from "./useXgridsK1Runtime";
|
|
|
|
export type XgridsK1Controller = ReturnType<typeof useXgridsK1Runtime>;
|
|
|
|
const XgridsK1RuntimeContext = createContext<XgridsK1Controller | null>(null);
|
|
|
|
function normalizeState(
|
|
controller: XgridsK1Controller,
|
|
activeModel: DeviceModelDefinition,
|
|
): MissionRuntimeState | null {
|
|
const state = controller.state;
|
|
if (!state) return null;
|
|
const metrics = state.metrics;
|
|
const telemetry = deviceTelemetry(metrics);
|
|
const deviceRef = state.device_ref;
|
|
const deviceSession = state.device_session;
|
|
const acquisition = effectiveAcquisition(state);
|
|
const sourceUrl = state.rerun_grpc_url?.trim() ?? "";
|
|
const resolvedSpatialSourceId =
|
|
state.source_mode === "replay"
|
|
? spatialSourceId(state, sourceUrl)
|
|
: acquisition?.acquisition_id ?? spatialSourceId(state, sourceUrl);
|
|
|
|
return {
|
|
phase: normalizeRuntimePhase(state),
|
|
message: localizeRuntimeMessage(state.message),
|
|
activeDevice: deviceRef
|
|
? {
|
|
pluginId: xgridsK1Manifest.metadata.id,
|
|
modelId: deviceRef.model_id || activeModel.id,
|
|
displayName: activeModel.displayName,
|
|
instanceId: deviceRef.device_id,
|
|
endpointLabel: state.k1_ip,
|
|
}
|
|
: null,
|
|
deviceSession: deviceSession
|
|
? {
|
|
sessionId: deviceSession.device_session_id,
|
|
deviceId: deviceSession.device_id,
|
|
compatibilityProfileId: deviceSession.compatibility_profile_id,
|
|
connectivity: deviceSession.connectivity,
|
|
}
|
|
: null,
|
|
acquisition: acquisition
|
|
? {
|
|
acquisitionId: acquisition.acquisition_id,
|
|
deviceId: acquisition.device_id,
|
|
deviceSessionId: acquisition.device_session_id,
|
|
compatibilityProfileId: acquisition.compatibility_profile_id,
|
|
controlMode: acquisition.control_mode,
|
|
state: acquisition.state,
|
|
stateRevision: acquisition.state_revision,
|
|
operatorInstructions: acquisition.operator_instructions ?? [],
|
|
cleanupPending: acquisition.cleanup_pending === true,
|
|
}
|
|
: null,
|
|
operations: (state.operations ?? []).map((operation) => ({
|
|
operationId: operation.operation_id,
|
|
action: operation.action,
|
|
status: operation.status,
|
|
stageCode: operation.stage_code,
|
|
messageCode: operation.message_code,
|
|
})),
|
|
spatialSource: sourceUrl && resolvedSpatialSourceId
|
|
? {
|
|
id: resolvedSpatialSourceId,
|
|
url: sourceUrl,
|
|
label:
|
|
state.source_mode === "replay"
|
|
? "Повтор пространственной записи"
|
|
: "Локальный пространственный поток",
|
|
kind: "rerun-grpc",
|
|
}
|
|
: null,
|
|
observationSources: xgridsK1ObservationSources(state, activeModel),
|
|
observationTimeline: {
|
|
// This is the shared host timeline. A replayable Rerun source alone does
|
|
// not make camera and point-cloud time jointly seekable.
|
|
mode: "live-only",
|
|
seekable: false,
|
|
sessionRecording: false,
|
|
synchronization: "host-arrival-best-effort",
|
|
range: null,
|
|
},
|
|
viewerSettings: state.viewer_settings,
|
|
sourceMode: confirmedRuntimeSourceMode(state),
|
|
metrics: {
|
|
latencyMs: pipelineLatency(metrics),
|
|
frameRateHz: finiteMetric(metrics?.frame_rate ?? metrics?.frame_rate_hz),
|
|
pointCount: finiteMetric(metrics?.point_count),
|
|
droppedPreviewFrames: finiteMetric(metrics?.dropped_preview_frames),
|
|
elapsedSeconds: telemetry.elapsedSeconds,
|
|
routeDistanceMeters: telemetry.routeDistanceMeters,
|
|
speedMetersPerSecond: telemetry.speedMetersPerSecond,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function XgridsK1RuntimeProvider({
|
|
activeModel,
|
|
registerDeactivation,
|
|
children,
|
|
}: {
|
|
activeModel: DeviceModelDefinition | null;
|
|
registerDeactivation: (handler: () => Promise<boolean>) => () => void;
|
|
children: ReactNode;
|
|
}) {
|
|
const active = activeModel !== null;
|
|
const inheritedRuntime = useMissionRuntime();
|
|
const controller = useXgridsK1Runtime(active);
|
|
const missionRuntime: MissionRuntimeController = {
|
|
state: activeModel ? normalizeState(controller, activeModel) : null,
|
|
backendStatus: controller.backendStatus,
|
|
pendingAction: controller.pendingAction,
|
|
refresh: controller.refresh,
|
|
updateViewerSettings: controller.updateViewerSettings,
|
|
setObservationSourceActive: controller.setObservationSourceActive,
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!activeModel) return;
|
|
return registerDeactivation(async () => {
|
|
if (controller.pendingAction !== null) return false;
|
|
// Always ask the backend to stop. The browser snapshot may be stale or not
|
|
// loaded yet, while a previous local capture is still alive.
|
|
return controller.stop();
|
|
});
|
|
}, [
|
|
activeModel,
|
|
controller.pendingAction,
|
|
controller.stop,
|
|
registerDeactivation,
|
|
]);
|
|
|
|
return (
|
|
<XgridsK1RuntimeContext.Provider value={active ? controller : null}>
|
|
<MissionRuntimeProvider value={active ? missionRuntime : inheritedRuntime}>
|
|
{children}
|
|
</MissionRuntimeProvider>
|
|
</XgridsK1RuntimeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useXgridsK1Controller(): XgridsK1Controller {
|
|
const controller = useContext(XgridsK1RuntimeContext);
|
|
if (!controller) {
|
|
throw new Error("XGRIDS K1 runtime используется вне собственного plugin provider.");
|
|
}
|
|
return controller;
|
|
}
|