feat(observation): add dynamic camera sources
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let xgridsK1Manifest;
|
||||
let xgridsK1ObservationSources;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({ xgridsK1Manifest } = await server.ssrLoadModule(
|
||||
"/src/device-plugins/xgrids-k1/manifest.ts",
|
||||
));
|
||||
({ xgridsK1ObservationSources } = await server.ssrLoadModule(
|
||||
"/src/device-plugins/xgrids-k1/observationSources.ts",
|
||||
));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
function model() {
|
||||
const activeModel = xgridsK1Manifest.spec.models[0];
|
||||
assert.ok(activeModel, "XGRIDS plugin must declare at least one model");
|
||||
return activeModel;
|
||||
}
|
||||
|
||||
function declaredState() {
|
||||
const activeModel = model();
|
||||
return {
|
||||
phase: "connected",
|
||||
source_mode: "idle",
|
||||
k1_ip: "192.168.7.10",
|
||||
foxglove_ws_url: "ws://192.168.7.10:8765",
|
||||
foxglove_viewer_url: "http://192.168.7.10:8765/vendor-viewer",
|
||||
compatibility: {
|
||||
profile_id: "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
|
||||
camera_preview: "rtsp://192.168.7.10:8554/vendor-preview",
|
||||
},
|
||||
device_ref: {
|
||||
device_id: "device-k1-001",
|
||||
model_id: activeModel.id,
|
||||
identity_stability: "stable",
|
||||
identity_basis: "hardware-identifier",
|
||||
},
|
||||
device_session: {
|
||||
device_session_id: "device-session-001",
|
||||
device_id: "device-k1-001",
|
||||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
|
||||
connectivity: "connected",
|
||||
},
|
||||
sensor_catalog: {
|
||||
schema_version: "missioncore.sensor-catalog/v1alpha2",
|
||||
revision: "test-profile",
|
||||
streams: [
|
||||
{ stream_id: "spatial.point-cloud.live", modality: "point-cloud", availability: "observed" },
|
||||
{ stream_id: "camera.preview.live", modality: "encoded-video", availability: "observed" },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function streamingState() {
|
||||
return {
|
||||
...declaredState(),
|
||||
phase: "streaming",
|
||||
source_mode: "live",
|
||||
rerun_grpc_url: "rerun+http://127.0.0.1:9877/proxy",
|
||||
acquisition: {
|
||||
acquisition_id: "acquisition-001",
|
||||
device_id: "device-k1-001",
|
||||
device_session_id: "device-session-001",
|
||||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
|
||||
control_mode: "operator-manual",
|
||||
requested_streams: ["spatial.point-cloud.live"],
|
||||
target_host: "127.0.0.1",
|
||||
duration_seconds: 0,
|
||||
evidence_policy: "required",
|
||||
state: "acquiring",
|
||||
state_revision: 3,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function collectUrlLikeStrings(value, found = []) {
|
||||
if (typeof value === "string") {
|
||||
if (value.includes("://")) found.push(value);
|
||||
return found;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) collectUrlLikeStrings(item, found);
|
||||
return found;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
for (const item of Object.values(value)) collectUrlLikeStrings(item, found);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
test("K1 maps to one primary point cloud and two auxiliary camera channels", () => {
|
||||
const sources = xgridsK1ObservationSources(declaredState(), model());
|
||||
|
||||
assert.equal(sources.length, 3);
|
||||
assert.deepEqual(
|
||||
sources.map(({ sourceId, semanticChannelId, modality, role }) => ({
|
||||
sourceId,
|
||||
semanticChannelId,
|
||||
modality,
|
||||
role,
|
||||
})),
|
||||
[
|
||||
{
|
||||
sourceId: "sensor.lidar.primary",
|
||||
semanticChannelId: "spatial.point-cloud.live",
|
||||
modality: "point-cloud",
|
||||
role: "primary",
|
||||
},
|
||||
{
|
||||
sourceId: "sensor.camera.left",
|
||||
semanticChannelId: "camera.preview.live",
|
||||
modality: "video",
|
||||
role: "auxiliary",
|
||||
},
|
||||
{
|
||||
sourceId: "sensor.camera.right",
|
||||
semanticChannelId: "camera.preview.live",
|
||||
modality: "video",
|
||||
role: "auxiliary",
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
assert.equal(new Set(sources.map(({ id }) => id)).size, sources.length);
|
||||
assert.ok(sources.every(({ provider }) => provider.pluginId && provider.modelId));
|
||||
assert.ok(sources.every(({ provider }) => provider.pluginVersion));
|
||||
assert.ok(sources.every(({ binding }) => binding.deviceId === "device-k1-001"));
|
||||
assert.ok(sources.every(({ capabilities }) => capabilities.timelineMode === "live-only"));
|
||||
});
|
||||
|
||||
test("observation source ids remain stable while runtime availability changes", () => {
|
||||
const declared = xgridsK1ObservationSources(declaredState(), model());
|
||||
const streaming = xgridsK1ObservationSources(streamingState(), model());
|
||||
|
||||
assert.deepEqual(
|
||||
streaming.map(({ id }) => id),
|
||||
declared.map(({ id }) => id),
|
||||
);
|
||||
assert.equal(declared[0].availability, "available");
|
||||
assert.equal(streaming[0].availability, "streaming");
|
||||
assert.equal(streaming[0].previewUrl, "rerun+http://127.0.0.1:9877/proxy");
|
||||
});
|
||||
|
||||
test("camera descriptors stay declared and URL-free until a browser delivery adapter exists", () => {
|
||||
const sources = xgridsK1ObservationSources(streamingState(), model());
|
||||
const cameras = sources.filter(({ modality }) => modality === "video");
|
||||
|
||||
assert.equal(cameras.length, 2);
|
||||
for (const camera of cameras) {
|
||||
assert.equal(camera.availability, "declared");
|
||||
assert.equal(camera.transport, "rtsp");
|
||||
assert.equal(camera.previewUrl, null);
|
||||
assert.equal(camera.capabilities.spatialRegistration, "unresolved");
|
||||
assert.equal(camera.capabilities.resizable, true);
|
||||
assert.doesNotMatch(camera.endpointLabel ?? "", /:\/\//);
|
||||
}
|
||||
|
||||
assert.deepEqual(collectUrlLikeStrings(sources), [
|
||||
"rerun+http://127.0.0.1:9877/proxy",
|
||||
]);
|
||||
const serialized = JSON.stringify(sources);
|
||||
assert.doesNotMatch(serialized, /rtsp:\/\//i);
|
||||
assert.doesNotMatch(serialized, /192\.168\.7\.10/);
|
||||
assert.doesNotMatch(serialized, /vendor-(?:preview|viewer)/);
|
||||
});
|
||||
|
||||
test("unattested camera catalog entries remain unverified", () => {
|
||||
const state = declaredState();
|
||||
state.compatibility.profile_id = null;
|
||||
state.device_session.compatibility_profile_id = null;
|
||||
state.sensor_catalog.streams = state.sensor_catalog.streams.map((stream) =>
|
||||
stream.stream_id === "camera.preview.live"
|
||||
? { ...stream, availability: "unverified", decode_status: "profile-not-attested" }
|
||||
: stream);
|
||||
|
||||
const cameras = xgridsK1ObservationSources(state, model()).filter(
|
||||
({ modality }) => modality === "video",
|
||||
);
|
||||
assert.equal(cameras.length, 2);
|
||||
assert.ok(cameras.every(({ availability }) => availability === "unverified"));
|
||||
assert.ok(cameras.every(({ previewUrl }) => previewUrl === null));
|
||||
});
|
||||
Reference in New Issue
Block a user