NODEDC_MISSION_CORE/plugins/xgrids-k1/frontend/src/observationSources.ts

299 lines
11 KiB
TypeScript

import type {
DeviceModelDefinition,
ObservationSourceAvailability,
ObservationSourceDelivery,
ObservationSourceDescriptor,
ObservationSourceProvider,
} from "@mission-core/plugin-sdk";
import { confirmedRuntimeSourceMode, effectiveAcquisition } from "./lifecycle";
import { xgridsK1Manifest } from "./manifest";
import type {
XgridsCameraPreviewDelivery,
XgridsK1State,
XgridsSensorCatalogStream,
} from "./api";
function providerFor(
state: XgridsK1State,
activeModel: DeviceModelDefinition,
): ObservationSourceProvider {
return {
pluginId: xgridsK1Manifest.metadata.id,
pluginVersion: xgridsK1Manifest.metadata.version,
modelId: state.device_ref?.model_id || activeModel.id,
compatibilityProfileId:
state.device_session?.compatibility_profile_id ?? state.compatibility?.profile_id ?? null,
};
}
function bindingFor(state: XgridsK1State) {
const acquisition = effectiveAcquisition(state);
return {
deviceId: state.device_ref?.device_id ?? null,
deviceSessionId: state.device_session?.device_session_id ?? null,
acquisitionId: acquisition?.acquisition_id ?? null,
};
}
function catalogDeclares(state: XgridsK1State, streamId: string): boolean {
return Boolean(state.sensor_catalog?.streams?.some((stream) => stream.stream_id === streamId));
}
function spatialAvailability(state: XgridsK1State): ObservationSourceAvailability {
const mode = confirmedRuntimeSourceMode(state);
if (mode !== "idle" && state.rerun_grpc_url?.trim()) return "streaming";
if (state.rerun_grpc_url?.trim()) return "available";
if (state.device_session?.connectivity === "degraded") return "degraded";
if (state.device_session?.connectivity === "connected") return "available";
return catalogDeclares(state, "spatial.point-cloud.live") ? "declared" : "unavailable";
}
function catalogAvailability(value: string | null | undefined): ObservationSourceAvailability {
switch (value?.trim().toLowerCase()) {
case "observed":
case "available":
return "available";
case "connecting":
return "connecting";
case "streaming":
return "streaming";
case "degraded":
return "degraded";
case "unavailable":
return "unavailable";
case "error":
return "error";
case "declared":
return "declared";
default:
return "unverified";
}
}
function cameraCatalogEntry(stream: XgridsSensorCatalogStream): boolean {
const sourceId = stream.source_id?.trim();
const semanticChannelId = stream.semantic_channel_id?.trim();
if (!sourceId || !semanticChannelId) return false;
const modality = stream.modality?.trim().toLowerCase();
const sensorKind = stream.sensor_kind?.trim().toLowerCase();
return (
modality === "encoded-video" ||
modality === "video" ||
sensorKind === "camera" ||
semanticChannelId.startsWith("camera.")
);
}
function safeEndpointLabel(value: string | null | undefined): string | null {
const label = value?.trim();
if (!label || label.length > 128) return null;
if (label.includes("://") || /(?:^|\D)(?:\d{1,3}\.){3}\d{1,3}(?:\D|$)/.test(label)) {
return null;
}
return label;
}
const SENSITIVE_QUERY_KEYS = new Set([
"accesskey", "accesstoken", "apikey", "auth", "authorization", "clientsecret",
"credential", "credentials", "password", "passwd", "passphrase", "privatekey",
"psk", "pwd", "refreshtoken", "secret", "sessionkey", "token", "user",
"username", "wifipassword",
]);
function decodedForInspection(value: string): string | null {
let decoded = value;
try {
for (let depth = 0; depth < 4; depth += 1) {
const next = decodeURIComponent(decoded);
if (next === decoded) break;
decoded = next;
}
} catch {
return null;
}
return decoded;
}
function sensitiveQueryKey(value: string): boolean {
const canonical = value.toLowerCase().replace(/[^a-z0-9]/g, "");
return SENSITIVE_QUERY_KEYS.has(canonical) ||
/(?:token|secret|password|passwd|passphrase|credential|credentials)$/.test(canonical);
}
function safeBrowserDeliveryUrl(value: string): boolean {
if (value.length > 4_096 || !value.startsWith("/") || value.startsWith("//")) return false;
const inspected = decodedForInspection(value);
if (!inspected) return false;
if (
/[\\\r\n\u0000-\u001f\u007f]/.test(inspected) ||
/\brtsps?\s*:/i.test(inspected) ||
/(?:^|\D)(?:\d{1,3}\.){3}\d{1,3}(?:\D|$)/.test(inspected) ||
/\[[0-9a-f:]+\]/i.test(inspected) ||
/(?:^|[/?#&=])[^/?#&=\s:@]+:[^/?#&\s@]+@/.test(inspected) ||
/\b(?:basic|bearer)\s+[a-z0-9._~+/=-]+/i.test(inspected)
) return false;
const base = new URL("https://mission-core.invalid/");
let parsed: URL;
let inspectedParsed: URL;
try {
parsed = new URL(value, base);
inspectedParsed = new URL(inspected, base);
} catch {
return false;
}
if (
parsed.origin !== base.origin || inspectedParsed.origin !== base.origin ||
parsed.username || parsed.password || parsed.hash ||
inspectedParsed.username || inspectedParsed.password || inspectedParsed.hash
) return false;
for (const [key] of inspectedParsed.searchParams) {
if (sensitiveQueryKey(key)) return false;
}
return true;
}
function browserDelivery(
value: XgridsCameraPreviewDelivery | null | undefined,
): ObservationSourceDelivery | null {
const id = value?.id?.trim();
const url = value?.url?.trim();
const mediaType = value?.media_type?.trim();
if (!id || value?.kind !== "mse-fmp4-websocket" || !url || !mediaType) return null;
// Browser delivery is host-owned and same-origin. A device RTSP/IP endpoint
// must never cross the plugin boundary into the host descriptor catalog.
if (!safeBrowserDeliveryUrl(url)) return null;
if (!/^video\/mp4(?:\s*;|$)/i.test(mediaType)) return null;
return { id, kind: value.kind, url, mediaType };
}
function cameraAvailability(
state: XgridsK1State,
stream: XgridsSensorCatalogStream,
selected: boolean,
delivery: ObservationSourceDelivery | null,
attested: boolean,
): ObservationSourceAvailability {
if (!attested) return "unverified";
if (state.device_session?.connectivity === "degraded") return "degraded";
const base = catalogAvailability(stream.availability);
if (!selected) return base === "streaming" || base === "connecting" ? "available" : base;
const phase = state.camera_preview?.phase?.trim().toLowerCase();
if (phase === "error" || base === "error") return "error";
if (delivery && (phase === "streaming" || phase === "active" || phase === "ready")) {
return "streaming";
}
if (phase === "degraded") return "degraded";
return "connecting";
}
export function xgridsK1ObservationSources(
state: XgridsK1State,
activeModel: DeviceModelDefinition,
): ObservationSourceDescriptor[] {
const provider = providerFor(state, activeModel);
const binding = bindingFor(state);
const clockId = binding.acquisitionId ?? binding.deviceSessionId ?? binding.deviceId ?? null;
const descriptorId = (sourceId: string) =>
`${provider.pluginId}:${provider.modelId}:${sourceId}`;
const pointCloud: ObservationSourceDescriptor = {
id: descriptorId("sensor.lidar.primary"),
sourceId: "sensor.lidar.primary",
semanticChannelId: "spatial.point-cloud.live",
label: "K1 · облако точек",
description: "Облако точек, поза и траектория в общей 3D-сцене",
modality: "point-cloud",
role: "primary",
availability: spatialAvailability(state),
transport: "rerun-grpc",
endpointLabel: state.rerun_grpc_url?.trim() ? "Rerun gRPC" : "MQTT → Rerun",
previewUrl: state.rerun_grpc_url?.trim() || null,
delivery: null,
activation: null,
provider,
binding,
capabilities: {
overlay: false,
fullscreen: true,
resizable: false,
defaultVisible: true,
timelineMode: "live-only",
// Rerun can hold replay data, but the Mission Core host timeline is not
// wired to its time controller yet.
seekable: false,
sessionRecording: false,
clockId,
spatialRegistration: "native",
},
};
const cameraRows = (state.sensor_catalog?.streams ?? []).filter(cameraCatalogEntry);
const sourceIdCounts = new Map<string, number>();
for (const stream of cameraRows) {
const sourceId = stream.source_id?.trim();
if (sourceId) sourceIdCounts.set(sourceId, (sourceIdCounts.get(sourceId) ?? 0) + 1);
}
const attested = Boolean(provider.compatibilityProfileId && binding.deviceSessionId);
const sessionScope = binding.deviceSessionId ?? binding.deviceId ?? "unbound";
const activeSourceId = state.camera_preview?.active_source_id?.trim() ?? null;
const cameras = cameraRows.flatMap<ObservationSourceDescriptor>((stream) => {
const sourceId = stream.source_id?.trim();
const semanticChannelId = stream.semantic_channel_id?.trim();
if (!sourceId || !semanticChannelId || sourceIdCounts.get(sourceId) !== 1) return [];
const rawActivation = stream.activation;
const groupId = rawActivation?.group_id?.trim();
const maxActive = rawActivation?.max_active;
const activationValid = Boolean(
groupId && Number.isInteger(maxActive) && (maxActive ?? 0) > 0,
);
const selected = Boolean(
attested && activationValid && rawActivation?.selected === true && activeSourceId === sourceId,
);
const activation = activationValid
? {
groupId: `${provider.pluginId}:${sessionScope}:${groupId}`,
maxActive: maxActive as number,
selected,
controllable: Boolean(attested && rawActivation?.controllable),
}
: null;
const candidateDelivery = stream.delivery ?? state.camera_preview?.delivery;
const delivery = selected ? browserDelivery(candidateDelivery) : null;
const label = stream.label?.trim() || sourceId;
return [{
id: descriptorId(sourceId),
sourceId,
semanticChannelId,
label,
description: "Видеоканал, опубликованный активным device-плагином",
modality: "video",
role: "auxiliary",
availability: cameraAvailability(state, stream, selected, delivery, attested),
transport: delivery ? "websocket" : "other",
endpointLabel: safeEndpointLabel(stream.endpoint_label) ?? "Локальный video adapter",
previewUrl: null,
delivery,
activation,
provider,
binding,
capabilities: {
overlay: true,
fullscreen: true,
resizable: true,
defaultVisible: selected && Boolean(delivery),
timelineMode: "live-only",
seekable: false,
sessionRecording: false,
clockId,
spatialRegistration: "unresolved",
},
}];
});
return [pointCloud, ...cameras];
}