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

@@ -7,7 +7,7 @@
"build": "node scripts/build-static.mjs",
"dev": "python3 -m http.server 4173",
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_run_preconditions.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
},
"dependencies": {},
"devDependencies": {}

View File

@@ -1064,11 +1064,12 @@ export function createSimulationStore(seed = {}) {
break;
}
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
{ type: "EMC_TASK_PLAN_RUN", line: Math.max(Number(state.activeLine || 1) - 1, 0) },
], { taskCycles: 5, operatorMessage: "task/HAL program run" }).catch(() => {});
const preconditions = validateRunPreconditions(state, { requireTaskHalSession: false });
if (!preconditions.ok) {
setState({ operatorMessage: preconditions.operatorMessage });
break;
}
runValidatedTaskHalProgramRun().catch(() => {});
break;
}
const playback = nextProgramRuntimeSamplePlayback(state, 5);
@@ -1569,12 +1570,42 @@ export function createSimulationStore(seed = {}) {
return session;
};
const runValidatedTaskHalProgramRun = async () => {
const preflight = validateRunPreconditions(state, { requireTaskHalSession: false });
if (!preflight.ok) {
setState({ operatorMessage: preflight.operatorMessage });
return null;
}
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
await initializeTaskHalSession({ openProgram: true });
}
const ready = validateRunPreconditions(state, { requireTaskHalSession: true });
if (!ready.ok) {
setState({ operatorMessage: ready.operatorMessage });
return null;
}
return runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
{ type: "EMC_TASK_PLAN_RUN", line: Math.max(Number(state.activeLine || 1) - Number(state.programStartLine || 1), 0) },
], {
taskCycles: 5,
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
allowFixtureSession: false,
});
};
const runTaskHalCommandSequence = async (commands, {
taskCycles = 1,
taskPeriodNs = 10000000,
servoPeriodNs = 1000000,
operatorMessage = "task/HAL command complete",
pendingJogCommand = null,
allowFixtureSession = true,
} = {}) => {
if (!state.taskHalRuntime?.loaded) {
throw new Error("LinuxCNC task/HAL runtime not attached");
@@ -1589,9 +1620,12 @@ export function createSimulationStore(seed = {}) {
try {
if (!state.taskHalSession && state.machineFileStaging?.save?.files?.length) {
await initializeTaskHalSession({ openProgram: true });
} else if (!state.taskHalSession && state.programLines?.length) {
} else if (allowFixtureSession && !state.taskHalSession && state.programLines?.length) {
await initializeFixtureTaskHalSessionForState();
}
if (!state.taskHalSession) {
throw new Error("LinuxCNC task/HAL session not initialized");
}
for (const command of commands) {
await state.taskHalRuntime.sendCommand(command);
}
@@ -1773,6 +1807,142 @@ function isAsyncKinematicsRuntime(runtime) {
return runtime?.executionContext === "worker";
}
export function validateRunPreconditions(state = {}, {
requireTaskHalRuntime = true,
requireTaskHalSession = true,
} = {}) {
const profile = state.profile || {};
const profileId = profile.id || state.machineProfile || "unknown";
const supportedProfiles = new Set(["xyzac-trt", "xyzbc-trt"]);
const fail = (operatorMessage, detail = {}) => ({
apiName: "web-rtcp-5axis-run-preconditions",
ok: false,
operatorMessage,
profileId,
iniPath: profile.iniPath || null,
coordinates: normalizeCoordinates(profile.traj?.coordinates),
kinematicsModuleId: profile.kinematicsModuleId || state.machineProfile || null,
...detail,
});
if (!supportedProfiles.has(profileId)) {
return fail(`run blocked: unsupported five-axis profile ${profileId}`);
}
if (!profile.iniPath || state.iniConfigReadiness?.loaded !== true || state.iniConfigReadiness?.ready !== true) {
return fail("run blocked: LinuxCNC INI not loaded");
}
if (state.linuxCncIniConfig?.path && state.linuxCncIniConfig.path !== profile.iniPath) {
return fail("run blocked: machine profile and INI path mismatch", {
iniPath: state.linuxCncIniConfig.path,
expectedIniPath: profile.iniPath,
});
}
const profileCoordinates = normalizeCoordinates(profile.traj?.coordinates);
const iniCoordinates = normalizeCoordinates(state.iniConfigReadiness?.coordinates || state.linuxCncIniConfig?.traj?.coordinates);
if (!profileCoordinates || !iniCoordinates || profileCoordinates !== iniCoordinates) {
return fail("run blocked: machine profile and INI coordinates mismatch", {
coordinates: iniCoordinates || null,
expectedCoordinates: profileCoordinates || null,
});
}
const profileKinematicsModuleId = profile.kinematicsModuleId || state.machineProfile || null;
const iniKinematicsModuleId = state.linuxCncIniConfig?.kinematicsModuleId || profileKinematicsModuleId;
if (!profileKinematicsModuleId || profileKinematicsModuleId !== iniKinematicsModuleId) {
return fail("run blocked: machine profile and INI kinematics mismatch", {
kinematicsModuleId: profileKinematicsModuleId,
expectedKinematicsModuleId: iniKinematicsModuleId,
});
}
const kinematicsReadiness = state.kinematicsRuntimeReadiness || {};
const runtimeKinematicsModuleId = kinematicsReadiness.moduleId || state.kinematicsRuntime?.moduleId || null;
if (kinematicsReadiness.loaded !== true || state.kinematicsRuntime?.loaded !== true) {
return fail("run blocked: LinuxCNC kinematics runtime not ready", {
kinematicsModuleId: profileKinematicsModuleId,
runtimeKinematicsModuleId,
});
}
if (runtimeKinematicsModuleId !== profileKinematicsModuleId) {
return fail("run blocked: LinuxCNC kinematics module mismatch", {
kinematicsModuleId: profileKinematicsModuleId,
runtimeKinematicsModuleId,
});
}
if (state.rtcpFrame?.sourceMode !== "source-derived-kinematics-wasm") {
return fail("run blocked: LinuxCNC kinematics frame not ready", {
frameSourceMode: state.rtcpFrame?.sourceMode || state.frameSourceMode || null,
});
}
if (state.machineFileStaging?.status !== "staged" || !state.machineFileStaging?.save?.files?.length) {
return fail("run blocked: LinuxCNC machine files not staged");
}
if (!state.machineFileStaging?.selectedGcodeSourceRel) {
return fail("run blocked: no machine-file G-code opened for task/HAL session");
}
if (requireTaskHalRuntime) {
const taskHalReadiness = state.taskHalRuntimeReadiness || {};
const taskHalReady = state.taskHalRuntime?.loaded === true
&& taskHalReadiness.taskRuntimeReady === true
&& taskHalReadiness.motionRuntimeReady === true
&& taskHalReadiness.halRuntimeReady === true;
if (!taskHalReady) {
return fail("run blocked: task/HAL runtime not ready");
}
}
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (requireTaskHalSession) {
if (!state.taskHalSession?.programPath || !expectedProgramPath) {
return fail("run blocked: no machine-file G-code opened for task/HAL session");
}
if (state.taskHalSession.programPath !== expectedProgramPath) {
return fail("run blocked: task/HAL session program mismatch", {
programPath: state.taskHalSession.programPath,
expectedProgramPath,
});
}
}
return {
apiName: "web-rtcp-5axis-run-preconditions",
ok: true,
operatorMessage: null,
profileId,
machineType: profile.title || null,
iniPath: profile.iniPath,
coordinates: profileCoordinates,
kinematicsModuleId: profileKinematicsModuleId,
selectedGcodeSourceRel: state.machineFileStaging.selectedGcodeSourceRel,
programPath: expectedProgramPath,
sourceMode: "linuxcnc-task-motion-hal-wasm",
semanticBoundary: "linuxcnc_ini_profile_kinematics_task_hal_run_preconditions",
};
}
function expectedTaskHalProgramPathForState(state = {}) {
const sourceRel = state.machineFileStaging?.selectedGcodeSourceRel;
if (!sourceRel || !state.machineFileStaging?.plan || !state.machineFileStaging?.save) return null;
try {
return selectMachineFileProgram(
state.machineFileStaging.plan,
state.machineFileStaging.save,
sourceRel,
).wasmProgramPath || null;
} catch {
return null;
}
}
function normalizeCoordinates(value) {
return String(value || "").replace(/[^A-Za-z]/g, "").toUpperCase();
}
function applyTaskHalStatusPatch(state, status, operatorMessage) {
const ui = status?.ui || {};
const task = status?.task || {};