import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; const serviceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); const DEFAULT_RUNNER = resolve(serviceRoot, "scripts", "jsbsim_runner.py"); const runnerPath = process.env.JSBSIM_RUNNER ?? DEFAULT_RUNNER; const pythonCommand = process.env.JSBSIM_PYTHON ?? "python"; export const backendInfo = { id: "jsbsim", name: "JSBSim", ready: true, runner: runnerPath, }; export const AIRCRAFT = [ { id: "f22cobra-jsbsim", name: "F-22 Cobra Research JSBSim", source: "JSBSim", capabilities: { basic6dof: true, highAlpha: true, postStall: "experimental", thrustVectoring: true, requiresNoTrimForManeuverResearch: true, }, }, { id: "f22-jsbsim", name: "F-22 JSBSim", source: "JSBSim", capabilities: { basic6dof: true, highAlpha: true, postStall: "experimental", thrustVectoring: true, requiresNoTrimForManeuverResearch: true, }, }, { id: "f16-jsbsim", name: "F-16 JSBSim", source: "JSBSim", capabilities: { basic6dof: true, highAlpha: "limited", postStall: false, thrustVectoring: false, }, }, { id: "c172p-jsbsim", name: "Cessna 172P JSBSim", source: "JSBSim", capabilities: { basic6dof: true, highAlpha: false, postStall: false, thrustVectoring: false, }, }, ]; export function trimInitialState(request) { validateTrimRequest(request); return runJsbsimRunner({ operation: "trim", aircraftId: request.aircraftId, sampleRateHz: 30, integrationRateHz: numberOrDefault(request.integrationRateHz, 120), trim: request, }); } export function simulateManeuver(request) { validateSimulateRequest(request); return runJsbsimRunner({ operation: "simulate", aircraftId: request.aircraftId.replace("-jsbsim", ""), sampleRateHz: numberOrDefault(request.sampleRateHz, 30), integrationRateHz: numberOrDefault(request.integrationRateHz, 120), durationSec: numberOrDefault(request.durationSec, 10), trim: request.trim, initialState: request.initialState, maneuver: request.maneuver, }); } function runJsbsimRunner(payload) { const result = spawnSync(pythonCommand, [runnerPath], { input: JSON.stringify(payload), encoding: "utf8", maxBuffer: 20 * 1024 * 1024, }); if (result.error) { throw new Error( `Unable to run JSBSim runner with "${pythonCommand}". Install Python and jsbsim, or set JSBSIM_PYTHON/JSBSIM_RUNNER. ${result.error.message}` ); } if (result.status !== 0) { if (result.status === 9009) { throw new Error( `Unable to run JSBSim runner with "${pythonCommand}". Python is not available on PATH; install Python and jsbsim, or set JSBSIM_PYTHON to a Python executable.` ); } throw new Error( `JSBSim runner failed with exit code ${result.status}: ${result.stderr.trim()}` ); } try { const response = JSON.parse(extractJson(result.stdout)); return { ...response, backend: backendInfo.id, }; } catch { throw new Error(`JSBSim runner did not return valid JSON: ${result.stdout}`); } } function extractJson(stdout) { const firstObject = stdout.indexOf("{"); if (firstObject < 0) { throw new Error("No JSON payload found."); } return stdout.slice(firstObject).trim(); } function validateTrimRequest(request) { if (!request || typeof request !== "object") { throw new Error("Request body must be a JSON object."); } if (!AIRCRAFT.some((aircraft) => aircraft.id === request.aircraftId)) { throw new Error(`Unknown aircraftId: ${request.aircraftId}`); } } function validateSimulateRequest(request) { if (!request || typeof request !== "object") { throw new Error("Request body must be a JSON object."); } if (!AIRCRAFT.some((aircraft) => aircraft.id === request.aircraftId)) { throw new Error(`Unknown aircraftId: ${request.aircraftId}`); } if (!request.initialState || typeof request.initialState !== "object") { throw new Error("initialState is required."); } const maneuverType = request.maneuver?.type; const supported = new Set(["straight", "climb", "descent", "control-script", "cobra"]); if (!supported.has(maneuverType)) { throw new Error( `JSBSim minimal backend supports straight/climb/descent/control-script/cobra only. Unsupported maneuver.type: ${maneuverType}` ); } } function numberOrDefault(value, fallback) { return Number.isFinite(value) ? value : fallback; }