feat(k1): stabilize LAB bridge and isolate onboard device integration

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 10:31:33 +03:00
parent 020a878915
commit 63ea2bed67
115 changed files with 13700 additions and 988 deletions
@@ -762,7 +762,15 @@ export async function probeRecordedPerceptionViewerSource(
return { sourceUrl: endpoint.href, byteLength: declaredLength };
}
export function RerunViewport({
export function RerunViewport(props: RerunViewportProps) {
// Even when source URLs coincide, another profile cannot inherit a mounted
// viewer's playback, blueprint channels, recovery state or scene draft.
const identity = props.profile.kind === "laboratory-result"
? `${props.profile.kind}:${props.profile.resultId}` : props.profile.kind;
return <RerunViewportInstance key={identity} {...props} />;
}
function RerunViewportInstance({
profile,
onStatusChange,
onSelectionChange,
@@ -772,7 +780,7 @@ export function RerunViewport({
onPerceptionLoadChange,
onPointColorLoadChange,
}: RerunViewportProps) {
const recordedProfile = profile.kind === "recorded-session" ? profile : null;
const recordedProfile = profile.kind === "recorded-session" || profile.kind === "laboratory-result" ? profile : null;
const liveProfile = profile.kind === "live-acquisition" ? profile : null;
const sourceUrl = profile.sourceUrl;
const recordedArtifact = recordedProfile?.artifact ?? null;
@@ -34,7 +34,7 @@ import {
} from "../laboratory/CanonicalRecordedLabReplay";
import type { CanonicalLabReplayDescriptor } from "../../core/laboratory/canonicalLabReplay";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
import { laboratoryResultRerunProfile } from "../../core/observation/viewerProfile";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import type { AIViewerLayer } from "../../core/observatory/aiComposition";
import {
@@ -305,7 +305,8 @@ export function CanonicalResultRerunReplay({
accumulationSeconds: showLocalSlam ? sceneDraft.accumulationSeconds : 0,
}), [sceneDraft, showLocalSlam, showSourcePoints, spatialMode]);
const semanticVisible = showDDRNet || showEoMT;
const profile = launch ? recordedSessionRerunProfile({
const profile = launch ? laboratoryResultRerunProfile({
resultId,
sourceUrl: launch.replay.sourceUrl,
artifact: {
sourceUrl: launch.replay.sourceUrl,
@@ -103,6 +103,10 @@ export interface DevicePluginConnectionProps {
}
export interface DeviceUiPlugin {
sensorUi?: {
contributions: readonly import('../../../../../packages/sensor-ui/src/extensions').SensorUiContribution[];
Enrollment?: ComponentType<import('../../../../../packages/sensor-ui/src/extensions').SensorEnrollmentProps>;
};
manifest: DevicePluginManifest;
RuntimeProvider: ComponentType<{
activeModel: DeviceModelDefinition | null;
@@ -113,13 +113,13 @@ const defaultQuickActions: Record<
EnvironmentSurfaceId,
readonly [string | null, string | null]
> = {
home: ["spatial-scene", "local-device"],
fleet: ["contour-health", "local-device"],
observation: ["spatial-scene", "cameras"],
home: ["spatial-scene", "vehicles"],
fleet: ["vehicles", "contour-health"],
observation: ["cameras", "world-map"],
missions: ["mission-planner", null],
data: ["recordings", "datasets"],
system: ["modules", "integrations"],
polygon: ["lab-archive", null],
polygon: ["lab-archive", "local-device"],
};
export function defaultEnvironmentSettings(): EnvironmentSettings {
@@ -85,6 +85,13 @@ export interface RecordedSessionRerunProfile {
lockPerceptionCameraInteraction: boolean;
}
/** A LAB owns its result presentation; it does not inherit session-view settings. */
export interface LaboratoryResultRerunProfile
extends Omit<RecordedSessionRerunProfile, "kind"> {
kind: "laboratory-result";
resultId: string;
}
/**
* Deprecated comparison-only contract for LAB artifacts that have not yet
* been republished as a native Rerun sidecar. It must never be selected by a
@@ -101,7 +108,8 @@ export interface LaboratoryRecordedEvidenceViewerProfile {
export type RerunViewerProfile =
| LiveAcquisitionRerunProfile
| RecordedSessionRerunProfile;
| RecordedSessionRerunProfile
| LaboratoryResultRerunProfile;
export type ObservationViewerProfile =
| RerunViewerProfile
@@ -119,11 +127,18 @@ export const LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE = Object.freeze({
export function liveAcquisitionRerunProfile(
input: Omit<LiveAcquisitionRerunProfile, "kind" | "clock">,
): LiveAcquisitionRerunProfile {
return { kind: "live-acquisition", clock: "stream_time", ...input };
return { ...input, kind: "live-acquisition", clock: "stream_time" };
}
export function recordedSessionRerunProfile(
input: Omit<RecordedSessionRerunProfile, "kind" | "clock">,
): RecordedSessionRerunProfile {
return { kind: "recorded-session", clock: "session_time", ...input };
return { ...input, kind: "recorded-session", clock: "session_time" };
}
export function laboratoryResultRerunProfile(
input: Omit<LaboratoryResultRerunProfile, "kind" | "clock">,
): LaboratoryResultRerunProfile {
if (!input.resultId.trim()) throw new Error("LAB result identity is required");
return { ...input, kind: "laboratory-result", clock: "session_time" };
}
+9 -9
View File
@@ -94,7 +94,7 @@ export const roots: RootDefinition[] = [
label: "Парк",
title: "Аппараты и устройства",
eyebrow: "ПАРК / УСТРОЙСТВА",
description: "Реестр аппаратов, локальное подключение, сенсоры и конфигурации борта.",
description: "Реестр аппаратов, бортовые компьютеры, сенсоры и конфигурации борта.",
statement: "Одинаково подключать одиночный стенд, наземную платформу и будущий рой.",
accent: "АППАРАТЫ И ПОЛЕЗНАЯ НАГРУЗКА",
},
@@ -203,11 +203,11 @@ export const workspaces: WorkspaceDefinition[] = [
},
{
id: "local-device",
root: "fleet",
label: "Подключение",
title: "Подключение",
eyebrow: "ПАРК / ТЕКУЩИЙ АДАПТЕР",
description: "Выбор модели, сценарий установленного плагина и запуск доступного потока.",
root: "polygon",
label: "Тестовые устройства",
title: "Тестовые устройства",
eyebrow: "LAB / ТЕСТОВЫЕ УСТРОЙСТВА",
description: "Подключение устройств к компьютеру оператора для испытаний и записи.",
icon: "network",
kind: "device",
groups: [],
@@ -258,11 +258,11 @@ export const workspaces: WorkspaceDefinition[] = [
},
{
id: "spatial-scene",
root: "observation",
root: "polygon",
label: "Пространственная сцена",
title: "Пространственная сцена",
eyebrow: "НАБЛЮДЕНИЕ / ЭФИР",
description: "Облако точек, траектория, преобразования и пространственные слои в единой 3D-сцене.",
eyebrow: "LAB / ПРОСТРАНСТВЕННАЯ СЦЕНА",
description: "Облако точек, камеры и траектория тестового устройства в реальном времени.",
icon: "globe",
kind: "spatial",
groups: [
@@ -1,13 +1,24 @@
import {useMemo} from 'react';
import {useDevicePluginHost} from '../../core/device-plugins/DevicePluginHost';
import {createIsolatedRerunHost} from '../../components/rerun/isolatedRerunHost';
import {SensorWorkspace} from '../../../../../packages/sensor-ui/src/SensorWorkspace';
import type {SensorInventory,SensorTransport} from '../../../../../packages/sensor-ui/src/contracts';
import {fleetRequest} from '../../core/fleet/useFleet';
export function VehicleSensors({vehicleID,enabled,onDetailChange}:{vehicleID:string;enabled:boolean;onDetailChange:(open:boolean)=>void}){
const {registry}=useDevicePluginHost();
const sensorContributions=useMemo(()=>registry.plugins.flatMap(plugin=>plugin.sensorUi?.contributions??[]),[registry]);
const enrollmentViews=registry.plugins.flatMap(plugin=>plugin.sensorUi?.Enrollment?[plugin.sensorUi.Enrollment]:[]);
const SensorEnrollmentView=enrollmentViews.length===1?enrollmentViews[0]:undefined;
const transport=useMemo<SensorTransport>(()=>({
enrollment:{
state:()=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/enrollment`),
submit:value=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/enrollment/operations`,'POST',value),
operation:id=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/enrollment/operations/${encodeURIComponent(id)}`),
},
inventory:async()=>{const fleet=await fleetRequest<{items:{id:string;connectivity:string;sensor_state:SensorInventory}[]}>();const value=fleet.items.find(v=>v.id===vehicleID);if(!value)throw new Error('Аппарат не найден.');return {...value.sensor_state,fresh:value.connectivity==='online'};},
subscribe:(receive,unavailable)=>{const events=new EventSource('/api/v1/fleet/events');events.onmessage=e=>{try{const value=JSON.parse(e.data).items.find((v:{id:string})=>v.id===vehicleID);if(value)receive({...value.sensor_state,fresh:value.connectivity==='online'});else unavailable();}catch{unavailable();}};events.onerror=unavailable;return()=>events.close();},
submit:value=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/operations`,'POST',value),
operation:id=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/operations/${encodeURIComponent(id)}`),
}),[vehicleID]);
return <SensorWorkspace key={vehicleID} transport={transport} enabled={enabled} onDetailChange={onDetailChange}/>;
return <SensorWorkspace contributions={sensorContributions} EnrollmentView={SensorEnrollmentView} createRerunHost={createIsolatedRerunHost} key={vehicleID} transport={transport} enabled={enabled} onDetailChange={onDetailChange}/>;
}