172 lines
4.6 KiB
JavaScript
172 lines
4.6 KiB
JavaScript
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;
|
|
}
|