NODEDC_MISSION_CORE/plugins/xgrids-k1/frontend/src/runtimeContext.tsx

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;
}