feat: add passive K1 local surface replay

This commit is contained in:
DCCONSTRUCTIONS
2026-07-25 22:47:12 +03:00
parent cfe17e49bb
commit 04a658b218
16 changed files with 2715 additions and 21 deletions
@@ -0,0 +1,602 @@
export interface LidarLocalSurfaceDistribution {
sampleCount: number;
minimum: number | null;
mean: number | null;
p50: number | null;
p95: number | null;
maximum: number | null;
}
export interface LidarLocalSurfaceAnchor {
key: string;
label: string;
frameIndex: number;
sourceFrameIndex: number;
sessionSeconds: number;
valid: boolean;
}
export interface LidarLocalSurfaceModel {
modelId: string;
displayName: string;
sessionId: string;
sourcePackId: string;
status: "diagnostic-only";
source: {
frameCount: number;
availableLidarFrames: number;
pointCount: number;
timelineStartSeconds: number;
timelineEndSeconds: number;
immutable: true;
passiveProcessingOnly: true;
firmwareOrDeviceCommandsUsed: false;
};
metrics: {
frames: {
total: number;
sourceAvailable: number;
valid: number;
sourceUnavailable: number;
poseStale: number;
insufficientSurface: number;
fitFailed: number;
};
sensorHeightM: LidarLocalSurfaceDistribution;
slopeDeg: LidarLocalSurfaceDistribution;
roughnessM: LidarLocalSurfaceDistribution;
confidence: LidarLocalSurfaceDistribution;
poseBindingAgeMs: LidarLocalSurfaceDistribution;
surfaceMaxAgeMs: LidarLocalSurfaceDistribution;
};
anchors: LidarLocalSurfaceAnchor[];
occupancyPolicy: {
absenceOfPointsMeansFree: false;
unknownIsTraversable: false;
persistentReconstructionMutated: false;
dynamicObjectLayerAvailable: false;
};
createdAtUtc: string | null;
groundTruth: false;
authority: {
commandsEnabled: false;
navigationOrSafetyAccepted: false;
};
}
export interface LidarLocalSurfaceCatalog {
configured: boolean;
validTotal: number;
invalidTotal: number;
items: LidarLocalSurfaceModel[];
}
export interface LidarLocalSurfaceFrame {
modelId: string;
sourcePackId: string;
sessionId: string;
frameIndex: number;
frameCount: number;
sourceFrameIndex: number;
sessionSeconds: number;
sourceAvailable: boolean;
valid: boolean;
failureCode: number;
pointCount: number;
coordinateFrame: "map";
distanceUnit: "m";
pointsXyzM: Array<[number, number, number]>;
pointClass: number[];
pointHeightM: number[];
pose: {
positionXyzM: [number, number, number];
orientationXyzw: [number, number, number, number];
bindingAgeMs: number;
};
surface: {
planeCoefficientsMap: [number, number, number, number];
sensorHeightM: number;
slopeDeg: number;
roughnessM: number;
confidence: number;
surfaceMaxAgeMs: number;
cellCount: number;
inlierCellCount: number;
};
counts: {
classified: number;
surface: number;
occupied: number;
belowSurface: number;
};
occupancyPolicy: LidarLocalSurfaceModel["occupancyPolicy"];
groundTruth: false;
authority: LidarLocalSurfaceModel["authority"];
}
export class LidarLocalSurfaceContractError extends Error {}
export class LidarLocalSurfaceApiError extends Error {
constructor(message: string, readonly status: number | null = null) {
super(message);
}
}
type LidarFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
const LOCAL_SURFACE_SCHEMA_PREFIX = ["missioncore.", "k", "1", "-local-surface"].join("");
const LOCAL_SURFACE_MODEL_PREFIX = ["k", "1", "-local-surface-"].join("");
const SAFE_MODEL_ID = new RegExp(`^${LOCAL_SURFACE_MODEL_PREFIX}[a-f0-9]{64}$`);
const SAFE_PACK_ID = /^e10-lidar-pack-[a-f0-9]{64}$/;
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/;
const SAFE_KEY = /^[a-z0-9][a-z0-9-]{0,63}$/;
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new LidarLocalSurfaceContractError(`${label}: ожидался объект`);
}
return value as Record<string, unknown>;
}
function array(value: unknown, label: string): unknown[] {
if (!Array.isArray(value)) {
throw new LidarLocalSurfaceContractError(`${label}: ожидался массив`);
}
return value;
}
function text(
value: unknown,
label: string,
pattern?: RegExp,
): string {
if (
typeof value !== "string"
|| !value.trim()
|| value.length > 240
|| (pattern && !pattern.test(value))
) {
throw new LidarLocalSurfaceContractError(`${label}: некорректная строка`);
}
return value;
}
function integer(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
throw new LidarLocalSurfaceContractError(`${label}: ожидалось целое число`);
}
return value;
}
function finite(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new LidarLocalSurfaceContractError(`${label}: ожидалось конечное число`);
}
return value;
}
function boolean(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new LidarLocalSurfaceContractError(`${label}: ожидался boolean`);
}
return value;
}
function tuple(
value: unknown,
length: number,
label: string,
): number[] {
const values = array(value, label).map((item, index) =>
finite(item, `${label}[${index}]`)
);
if (values.length !== length) {
throw new LidarLocalSurfaceContractError(`${label}: неверная длина`);
}
return values;
}
function distribution(
value: unknown,
label: string,
): LidarLocalSurfaceDistribution {
const source = record(value, label);
const sampleCount = integer(source.sample_count, `${label}.sample_count`);
const metric = (key: string): number | null => {
const item = source[key];
if (item === null) return null;
return finite(item, `${label}.${key}`);
};
const result = {
sampleCount,
minimum: metric("minimum"),
mean: metric("mean"),
p50: metric("p50"),
p95: metric("p95"),
maximum: metric("maximum"),
};
if (
(sampleCount === 0
&& Object.entries(result).some(
([key, item]) => key !== "sampleCount" && item !== null,
))
|| (sampleCount > 0
&& Object.entries(result).some(
([key, item]) => key !== "sampleCount" && item === null,
))
) {
throw new LidarLocalSurfaceContractError(`${label}: несовместимая выборка`);
}
return result;
}
function occupancyPolicy(
value: unknown,
): LidarLocalSurfaceModel["occupancyPolicy"] {
const source = record(value, "occupancy_policy");
if (
source.absence_of_points_means_free !== false
|| source.unknown_is_traversable !== false
|| source.persistent_reconstruction_mutated !== false
|| source.dynamic_object_layer_available !== false
) {
throw new LidarLocalSurfaceContractError(
"Local-surface policy завышает доступное знание",
);
}
return {
absenceOfPointsMeansFree: false,
unknownIsTraversable: false,
persistentReconstructionMutated: false,
dynamicObjectLayerAvailable: false,
};
}
function authority(
value: unknown,
): LidarLocalSurfaceModel["authority"] {
const source = record(value, "authority");
if (
source.commands_enabled !== false
|| source.navigation_or_safety_accepted !== false
) {
throw new LidarLocalSurfaceContractError(
"Local-surface authority несовместим",
);
}
return {
commandsEnabled: false,
navigationOrSafetyAccepted: false,
};
}
function anchor(value: unknown): LidarLocalSurfaceAnchor {
const source = record(value, "anchor");
return {
key: text(source.key, "anchor.key", SAFE_KEY),
label: text(source.label, "anchor.label"),
frameIndex: integer(source.frame_index, "anchor.frame_index"),
sourceFrameIndex: integer(
source.source_frame_index,
"anchor.source_frame_index",
),
sessionSeconds: finite(source.session_seconds, "anchor.session_seconds"),
valid: boolean(source.valid, "anchor.valid"),
};
}
function model(value: unknown): LidarLocalSurfaceModel {
const source = record(value, "LiDAR local-surface model");
const sourceEvidence = record(source.source, "source");
const metrics = record(source.metrics, "metrics");
const frames = record(metrics.frames, "metrics.frames");
if (
source.status !== "diagnostic-only"
|| source.ground_truth !== false
|| sourceEvidence.immutable !== true
|| sourceEvidence.passive_processing_only !== true
|| sourceEvidence.firmware_or_device_commands_used !== false
) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface меняет источник или завышает статус",
);
}
const frameMetrics = {
total: integer(frames.total, "frames.total"),
sourceAvailable: integer(frames.source_available, "frames.source_available"),
valid: integer(frames.valid, "frames.valid"),
sourceUnavailable: integer(
frames.source_unavailable,
"frames.source_unavailable",
),
poseStale: integer(frames.pose_stale, "frames.pose_stale"),
insufficientSurface: integer(
frames.insufficient_surface,
"frames.insufficient_surface",
),
fitFailed: integer(frames.fit_failed, "frames.fit_failed"),
};
if (
frameMetrics.sourceAvailable + frameMetrics.sourceUnavailable
!== frameMetrics.total
|| frameMetrics.valid > frameMetrics.sourceAvailable
) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface frame totals расходятся",
);
}
const anchors = array(source.anchors, "anchors").map(anchor);
if (!anchors.length || anchors.some((item) => item.frameIndex >= frameMetrics.total)) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface anchors несовместимы",
);
}
return {
modelId: text(source.model_id, "model_id", SAFE_MODEL_ID),
displayName: text(source.display_name, "display_name"),
sessionId: text(source.session_id, "session_id", SAFE_ID),
sourcePackId: text(source.source_pack_id, "source_pack_id", SAFE_PACK_ID),
status: "diagnostic-only",
source: {
frameCount: integer(sourceEvidence.frame_count, "source.frame_count"),
availableLidarFrames: integer(
sourceEvidence.available_lidar_frames,
"source.available_lidar_frames",
),
pointCount: integer(sourceEvidence.point_count, "source.point_count"),
timelineStartSeconds: finite(
sourceEvidence.timeline_start_seconds,
"source.timeline_start_seconds",
),
timelineEndSeconds: finite(
sourceEvidence.timeline_end_seconds,
"source.timeline_end_seconds",
),
immutable: true,
passiveProcessingOnly: true,
firmwareOrDeviceCommandsUsed: false,
},
metrics: {
frames: frameMetrics,
sensorHeightM: distribution(metrics.sensor_height_m, "sensor_height_m"),
slopeDeg: distribution(metrics.slope_deg, "slope_deg"),
roughnessM: distribution(metrics.roughness_m, "roughness_m"),
confidence: distribution(metrics.confidence, "confidence"),
poseBindingAgeMs: distribution(
metrics.pose_binding_age_ms,
"pose_binding_age_ms",
),
surfaceMaxAgeMs: distribution(
metrics.surface_max_age_ms,
"surface_max_age_ms",
),
},
anchors,
occupancyPolicy: occupancyPolicy(source.occupancy_policy),
createdAtUtc:
source.created_at_utc === null || source.created_at_utc === undefined
? null
: text(source.created_at_utc, "created_at_utc"),
groundTruth: false,
authority: authority(source.authority),
};
}
export function parseLidarLocalSurfaceCatalog(
value: unknown,
): LidarLocalSurfaceCatalog {
const source = record(value, "LiDAR local-surface catalog");
if (
source.schema_version !== `${LOCAL_SURFACE_SCHEMA_PREFIX}-catalog/v1`
|| source.access !== "read-only"
) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface catalog несовместим",
);
}
return {
configured: boolean(source.configured, "configured"),
validTotal: integer(source.valid_total, "valid_total"),
invalidTotal: integer(source.invalid_total, "invalid_total"),
items: array(source.items, "items").map(model),
};
}
export function parseLidarLocalSurfaceFrame(
value: unknown,
): LidarLocalSurfaceFrame {
const source = record(value, "LiDAR local-surface frame");
if (
source.schema_version !== `${LOCAL_SURFACE_SCHEMA_PREFIX}-frame/v1`
|| source.access !== "read-only"
|| source.ground_truth !== false
|| source.coordinate_frame !== "map"
|| source.distance_unit !== "m"
) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface frame несовместим",
);
}
const pointCount = integer(source.point_count, "point_count");
if (pointCount > 200_000) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface frame слишком большой",
);
}
const pointsXyzM = array(source.points_xyz_m, "points_xyz_m").map(
(item, index): [number, number, number] => {
const values = tuple(item, 3, `points_xyz_m[${index}]`);
return [values[0], values[1], values[2]];
},
);
const pointClass = array(source.point_class, "point_class").map(
(item, index) => {
const value = integer(item, `point_class[${index}]`);
if (value > 3) {
throw new LidarLocalSurfaceContractError(
"Неизвестный local-surface class",
);
}
return value;
},
);
const pointHeightM = array(source.point_height_m, "point_height_m").map(
(item, index) => finite(item, `point_height_m[${index}]`),
);
if (
pointsXyzM.length !== pointCount
|| pointClass.length !== pointCount
|| pointHeightM.length !== pointCount
) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface point arrays расходятся",
);
}
const pose = record(source.pose, "pose");
const surface = record(source.surface, "surface");
const counts = record(source.counts, "counts");
const parsedCounts = {
classified: integer(counts.classified, "counts.classified"),
surface: integer(counts.surface, "counts.surface"),
occupied: integer(counts.occupied, "counts.occupied"),
belowSurface: integer(counts.below_surface, "counts.below_surface"),
};
if (
parsedCounts.classified !== pointClass.filter((item) => item !== 0).length
|| parsedCounts.surface !== pointClass.filter((item) => item === 1).length
|| parsedCounts.occupied !== pointClass.filter((item) => item === 2).length
|| parsedCounts.belowSurface
!== pointClass.filter((item) => item === 3).length
) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface counts расходятся",
);
}
const position = tuple(pose.position_xyz_m, 3, "pose.position_xyz_m");
const orientation = tuple(
pose.orientation_xyzw,
4,
"pose.orientation_xyzw",
);
const plane = tuple(
surface.plane_coefficients_map,
4,
"surface.plane_coefficients_map",
);
return {
modelId: text(source.model_id, "model_id", SAFE_MODEL_ID),
sourcePackId: text(source.source_pack_id, "source_pack_id", SAFE_PACK_ID),
sessionId: text(source.session_id, "session_id", SAFE_ID),
frameIndex: integer(source.frame_index, "frame_index"),
frameCount: integer(source.frame_count, "frame_count"),
sourceFrameIndex: integer(source.source_frame_index, "source_frame_index"),
sessionSeconds: finite(source.session_seconds, "session_seconds"),
sourceAvailable: boolean(source.source_available, "source_available"),
valid: boolean(source.valid, "valid"),
failureCode: integer(source.failure_code, "failure_code"),
pointCount,
coordinateFrame: "map",
distanceUnit: "m",
pointsXyzM,
pointClass,
pointHeightM,
pose: {
positionXyzM: [position[0], position[1], position[2]],
orientationXyzw: [
orientation[0],
orientation[1],
orientation[2],
orientation[3],
],
bindingAgeMs: finite(pose.binding_age_ms, "pose.binding_age_ms"),
},
surface: {
planeCoefficientsMap: [plane[0], plane[1], plane[2], plane[3]],
sensorHeightM: finite(surface.sensor_height_m, "surface.sensor_height_m"),
slopeDeg: finite(surface.slope_deg, "surface.slope_deg"),
roughnessM: finite(surface.roughness_m, "surface.roughness_m"),
confidence: finite(surface.confidence, "surface.confidence"),
surfaceMaxAgeMs: finite(
surface.surface_max_age_ms,
"surface.surface_max_age_ms",
),
cellCount: integer(surface.cell_count, "surface.cell_count"),
inlierCellCount: integer(
surface.inlier_cell_count,
"surface.inlier_cell_count",
),
},
counts: parsedCounts,
occupancyPolicy: occupancyPolicy(source.occupancy_policy),
groundTruth: false,
authority: authority(source.authority),
};
}
async function responseJson(
response: Response,
fallback: string,
): Promise<unknown> {
let payload: unknown = null;
try {
payload = await response.json();
} catch {
// Preserve the status-aware fallback below.
}
if (!response.ok) {
const detail =
payload && typeof payload === "object" && "detail" in payload
? String((payload as { detail?: unknown }).detail)
: fallback;
throw new LidarLocalSurfaceApiError(detail, response.status);
}
return payload;
}
export async function fetchLidarLocalSurfaces(
options: { signal?: AbortSignal; fetcher?: LidarFetch } = {},
): Promise<LidarLocalSurfaceCatalog> {
const fetcher = options.fetcher ?? fetch;
const response = await fetcher("/api/v1/lidar/local-surfaces?limit=10", {
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
});
return parseLidarLocalSurfaceCatalog(
await responseJson(response, "Не удалось получить LiDAR local-surface."),
);
}
export async function fetchLidarLocalSurfaceFrame(
modelId: string,
frameIndex: number,
options: { signal?: AbortSignal; fetcher?: LidarFetch } = {},
): Promise<LidarLocalSurfaceFrame> {
if (
!SAFE_MODEL_ID.test(modelId)
|| !Number.isSafeInteger(frameIndex)
|| frameIndex < 0
) {
throw new LidarLocalSurfaceContractError(
"Некорректный LiDAR local-surface frame",
);
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/lidar/local-surfaces/${modelId}/frames/${frameIndex}`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseLidarLocalSurfaceFrame(
await responseJson(
response,
"Не удалось получить LiDAR local-surface frame.",
),
);
}