/** * Mock KSP telemetry publisher. * * Generates a universe snapshot at a configurable cadence (default 1Hz) * using the KERBOL_SYSTEM catalog and a starter fleet of vessels. The * vessel orbits are propagated forward in time using the * @kerbal-rt/orbital-math library — exactly the same math the live-map * uses to render the scene, so the publisher and renderer stay in sync. * * Each generated snapshot is POSTed to the API at /api/v1/ingest. * Connect the hub's /debug page (or any WS client) and you'll see the * state update in real time. */ import { meanMotion } from '@kerbal-rt/orbital-math'; import type { CelestialBody, KeplerianElements, UniverseSnapshot, Vessel, } from '@kerbal-rt/shared-types'; import { KERBOL_SYSTEM, STARTER_FLEET } from './catalog.js'; const API_URL = process.env.MOCK_API_URL ?? 'http://localhost:4000'; const API_KEY = process.env.INGEST_API_KEY ?? ''; const INTERVAL_MS = Number(process.env.MOCK_INTERVAL_MS ?? 1000); const START_UT = Number(process.env.MOCK_START_UT ?? 4_700_000); interface MutableVessel { id: string; name: string; type: string | null; owner: string | null; situation: Vessel['situation']; status: Vessel['status']; referenceBodyId: string; /** Working keplerian elements at the last emitted time. */ elements: KeplerianElements; createdAt: string; retiredAt: string | null; } const bodies: CelestialBody[] = KERBOL_SYSTEM; const bodiesById = new Map(bodies.map((b) => [b.id, b])); const vessels: MutableVessel[] = STARTER_FLEET.map((v) => ({ id: v.id, name: v.name, type: v.type, owner: v.owner, situation: v.situation, status: v.status, referenceBodyId: v.referenceBodyId, elements: { ...v.initialOrbit, epoch: START_UT }, createdAt: v.createdAt, retiredAt: null, })); let currentUt = START_UT; let snapshotCount = 0; let lastError: string | null = null; async function publish(snap: UniverseSnapshot): Promise { const headers: Record = { 'content-type': 'application/json' }; if (API_KEY) headers['x-api-key'] = API_KEY; try { const res = await fetch(`${API_URL}/api/v1/ingest`, { method: 'POST', headers, body: JSON.stringify(snap), }); if (!res.ok) { lastError = `HTTP ${res.status}`; } else { lastError = null; } } catch (err) { lastError = String((err as Error).message ?? err); } } function buildSnapshot(ut: number): UniverseSnapshot { // Advance each vessel's mean anomaly by n·dt from its last known epoch. for (const v of vessels) { const ref = bodiesById.get(v.referenceBodyId); if (!ref) continue; const dt = ut - v.elements.epoch; if (dt !== 0) { const n = meanMotion(v.elements.semiMajorAxis, ref.gravitationalParameter); v.elements = { ...v.elements, meanAnomalyAtEpoch: v.elements.meanAnomalyAtEpoch + n * dt, epoch: ut, }; } } const vesselsOut: Vessel[] = vessels.map((v) => ({ id: v.id, name: v.name, type: v.type, owner: v.owner, situation: v.situation, status: v.status, orbit: v.elements, referenceBodyId: v.referenceBodyId, createdAt: v.createdAt, retiredAt: v.retiredAt, })); return { ut, capturedAt: new Date().toISOString(), activeVesselId: vessels[0]?.id ?? null, bodies, vessels: vesselsOut, groundStations: [ { id: 'montana', name: 'Montana DSN', bodyId: 'kerbin', lat: 47.0, lon: -110.0, alt: 1200, }, ], }; } async function tick(): Promise { currentUt += INTERVAL_MS / 1000; // 1s wall = 1s KSP UT (1x speed) const snap = buildSnapshot(currentUt); snapshotCount += 1; await publish(snap); } function printStatus(): void { const status = lastError ? `ERROR (${lastError})` : 'OK'; // eslint-disable-next-line no-console console.log( `[mock-telemetry] ut=${currentUt.toFixed(0)} snapshots=${snapshotCount} → ${API_URL} ${status}`, ); } async function main(): Promise { // eslint-disable-next-line no-console console.log( `[mock-telemetry] starting — API=${API_URL} interval=${INTERVAL_MS}ms key=${API_KEY ? 'set' : 'unset'} start_ut=${START_UT}`, ); // eslint-disable-next-line no-console console.log(`[mock-telemetry] publishing ${bodies.length} bodies, ${vessels.length} vessels`); // Send the first snapshot immediately, then on the interval. await tick(); printStatus(); setInterval(async () => { await tick(); }, INTERVAL_MS); setInterval(printStatus, 10_000); } main().catch((err) => { // eslint-disable-next-line no-console console.error('[mock-telemetry] fatal:', err); process.exit(1); });