Add JSBSim service backend

This commit is contained in:
moriy
2026-04-28 17:53:29 +08:00
parent b025708b5a
commit 25297601d7
14 changed files with 5053 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
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;
}