Add JSBSim service backend
This commit is contained in:
171
service/src/backends/jsbsimBackend.js
Normal file
171
service/src/backends/jsbsimBackend.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user