Add RUN precondition gate for 5-axis simulation

This commit is contained in:
2026-06-22 10:21:12 -04:00
parent ea8e10031b
commit 8ec662334f
10 changed files with 10537 additions and 16 deletions

View File

@@ -4,6 +4,9 @@ import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
import { wrapTaskHalSdk } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
import { createSimulationStore } from "../../app/src/state/store.js";
@@ -20,21 +23,37 @@ const sdk = await createLinuxCncTaskHalSdk({
},
});
const runtime = wrapTaskHalSdk(sdk);
const store = createSimulationStore({
programLines: [
"G0 X1 Y2 Z-3 A10 C20",
"G1 X2 Y3 Z-4 A11 C21",
"M428",
"G1 X3 Y4 Z-5 A12 C22",
],
activeProgram: "task-hal-store-smoke.ngc",
const profile = getFiveAxisProfile("xyzac-trt");
const iniText = readFileSync(
resolve(rootDir, "wasm-port/vendor/linuxcnc", profile.iniPath),
"utf8",
);
const iniConfig = parseLinuxCncIni(iniText, {
path: profile.iniPath,
profileId: profile.id,
});
const store = createSimulationStore();
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
store.dispatch({
type: "ATTACH_KINEMATICS_RUNTIME",
runtime: await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }),
});
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime });
assert.equal(store.getState().taskHalRuntimeReadiness.taskRuntimeReady, true);
assert.equal(store.getState().taskHalRuntimeReadiness.motionRuntimeReady, true);
assert.equal(store.getState().taskHalRuntimeReadiness.halRuntimeReady, true);
await store.stageMachineFiles();
store.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
});
await waitForState(store, (state) => (
state.machineFileStaging.selectedGcodeSourceRel?.endsWith("xyzac_switchkins_test_1.ngc") &&
state.taskHalSession?.programPath?.endsWith("xyzac_switchkins_test_1.ngc")
));
store.dispatch({ type: "TOGGLE_POWER" });
await waitForTaskHal(store);
store.dispatch({ type: "HOME" });
@@ -47,11 +66,14 @@ let state = store.getState();
assert.equal(state.taskHalStatus.summary.taskRuntimeReady, true);
assert.equal(state.taskHalStatus.summary.halSyncReady, true);
assert.equal(state.taskHalStatus.ui.activeLine >= 1, true);
assert.equal(state.axisPose.x >= 1, true);
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
assert.equal(state.fullExecutionBoundary.nativeTaskReady, true);
assert.equal(state.fullExecutionBoundary.nativeHalSyncReady, true);
await store.initializeTaskHalSession({ openProgram: true });
store.dispatch({ type: "TOGGLE_POWER" });
await waitForTaskHal(store);
store.dispatch({ type: "HOME" });
store.dispatch({ type: "SET_MODE", mode: "mdi" });
await waitForTaskHal(store);
store.dispatch({ type: "RUN_MDI", command: "M428" });
@@ -111,6 +133,15 @@ async function waitForTaskHal(store) {
throw new Error("task/HAL store command did not settle");
}
async function waitForState(store, predicate) {
for (let attempt = 0; attempt < 60; attempt += 1) {
const state = store.getState();
if (predicate(state)) return state;
await new Promise((resolve) => setTimeout(resolve, 0));
}
throw new Error("store state condition did not settle");
}
function assertNear(actual, expected, message) {
assert.equal(Math.abs(Number(actual) - Number(expected)) < 1e-9, true, `${message}: ${actual} !== ${expected}`);
}

View File

@@ -0,0 +1,133 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
import {
applyIniConfigToProfile,
parseLinuxCncIni,
} from "../../app/src/runtime/linuxcnc-ini-runtime.js";
import { stageProfileMachineFiles } from "../../app/src/runtime/linuxcnc-machine-file-staging.js";
import { buildTaskHalSessionFromMachineFiles } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
import {
createSimulationStore,
validateRunPreconditions,
} from "../../app/src/state/store.js";
const cases = [
{
profileId: "xyzac-trt",
machineName: "sim-xyzac-trt-kins (switchkins)",
coordinates: "XYZAC",
kinematicsName: "xyzac-trt-kins",
kinematicsModuleId: "xyzac-trt",
wasmFile: "linuxcnc_xyzac_trt_kinematics.wasm",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
},
{
profileId: "xyzbc-trt",
machineName: "sim-xyzbc-trt-kins (switchkins)",
coordinates: "XYZBC",
kinematicsName: "xyzbc-trt-kins",
kinematicsModuleId: "xyzbc-trt",
wasmFile: "linuxcnc_xyzbc_trt_kinematics.wasm",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
},
];
for (const item of cases) {
const baseProfile = getFiveAxisProfile(item.profileId);
const iniText = await readLinuxCncFile(baseProfile.iniPath);
const iniConfig = parseLinuxCncIni(iniText, {
path: baseProfile.iniPath,
profileId: item.profileId,
});
assert.equal(iniConfig.profileId, item.profileId);
assert.equal(iniConfig.machineName, item.machineName);
assert.equal(iniConfig.traj.coordinates, item.coordinates);
assert.equal(iniConfig.kinematics.name, item.kinematicsName);
assert.equal(iniConfig.kinematicsModuleId, item.kinematicsModuleId);
assert.equal(iniConfig.validation.ready, true);
const profile = applyIniConfigToProfile(baseProfile, iniConfig);
assert.equal(profile.traj.coordinates, item.coordinates);
assert.equal(profile.kinematicsModuleId, item.kinematicsModuleId);
assert.equal(profile.axisLimits[item.coordinates[0]].max > 0, true);
assert.equal(profile.jointConfig.length, item.coordinates.length);
const kinematicsRuntime = await createLinuxCncKinematicsRuntime({ moduleId: item.kinematicsModuleId });
assert.equal(kinematicsRuntime.readiness().moduleId, item.kinematicsModuleId);
assert.equal(kinematicsRuntime.wasmFile, item.wasmFile);
assert.equal(kinematicsRuntime.frameForJoints([10, 20, 30, 5, 15]).moduleId, item.kinematicsModuleId);
const staged = await stageProfileMachineFiles(profile);
const selectedPlan = staged.plan.selectedProgramSourceRel === item.sourceRel
? staged.plan
: {
...staged.plan,
wasmProgramPath: staged.save.files.find((file) => file.sourceRel === item.sourceRel)?.wasmPath,
selectedProgramSourceRel: item.sourceRel,
};
const session = buildTaskHalSessionFromMachineFiles({
profile,
plan: selectedPlan,
save: staged.save,
selectedProgramRel: item.sourceRel,
});
assert.equal(session.profileId, item.profileId);
assert.equal(session.iniPath.endsWith(`${item.profileId}.ini`), true);
assert.equal(session.programSourceRel, item.sourceRel);
assert.equal(session.programPath.endsWith(item.sourceRel.split("/").at(-1)), true);
}
const initialStore = createSimulationStore();
let check = validateRunPreconditions(initialStore.getState(), {
requireTaskHalRuntime: false,
requireTaskHalSession: false,
});
assert.equal(check.ok, false);
assert.equal(check.operatorMessage, "run blocked: LinuxCNC INI not loaded");
const profile = getFiveAxisProfile("xyzac-trt");
const iniText = await readLinuxCncFile(profile.iniPath);
const iniConfig = parseLinuxCncIni(iniText, {
path: profile.iniPath,
profileId: profile.id,
});
const store = createSimulationStore();
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
store.dispatch({
type: "ATTACH_KINEMATICS_RUNTIME",
runtime: await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }),
});
await store.stageMachineFiles();
store.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
});
check = validateRunPreconditions(store.getState(), {
requireTaskHalRuntime: false,
requireTaskHalSession: false,
});
assert.equal(check.ok, true);
assert.equal(check.profileId, "xyzac-trt");
assert.equal(check.coordinates, "XYZAC");
assert.equal(check.kinematicsModuleId, "xyzac-trt");
assert.equal(check.selectedGcodeSourceRel.endsWith("xyzac_switchkins_test_1.ngc"), true);
check = validateRunPreconditions(store.getState(), {
requireTaskHalRuntime: true,
requireTaskHalSession: false,
});
assert.equal(check.ok, false);
assert.equal(check.operatorMessage, "run blocked: task/HAL runtime not ready");
console.log("run_preconditions_ini_profile_smoke=ok");
console.log("run_preconditions_kinematics_smoke=ok");
console.log("run_preconditions_machine_file_smoke=ok");
async function readLinuxCncFile(sourceRel) {
return readFile(new URL(`../../../wasm-port/vendor/linuxcnc/${sourceRel}`, import.meta.url), "utf8");
}