Advance simulation parity and 3-axis tutorial coverage

This commit is contained in:
2026-08-23 18:00:56 +08:00
parent 4a11d9692b
commit 938654b19d
107 changed files with 13537 additions and 3565 deletions

55
web/worker/axis-state.mjs Normal file
View File

@@ -0,0 +1,55 @@
// AXIS-facing state names are a view over the native task and interpreter
// states. Keep sim_task.v1 unchanged and expose this as an additive payload.
export const AXIS_STATES = Object.freeze(["ESTOP", "ESTOP_RESET", "OFF", "ON", "HOME", "IDLE", "RUNNING", "PAUSED"]);
export const LINUXCNC_STATE_CONSTANTS = Object.freeze({
STATE_ESTOP: 1,
STATE_ESTOP_RESET: 2,
STATE_OFF: 3,
STATE_ON: 4,
MODE_MANUAL: 1,
MODE_AUTO: 2,
MODE_MDI: 3,
INTERP_IDLE: 1,
INTERP_READING: 2,
INTERP_PAUSED: 3,
INTERP_WAITING: 4,
});
export const AXIS_TRANSITIONS = Object.freeze({
ESTOP: ["ESTOP", "ESTOP_RESET"],
ESTOP_RESET: ["ESTOP", "ON", "OFF"],
OFF: ["OFF", "ESTOP", "ESTOP_RESET"],
ON: ["ON", "OFF", "ESTOP", "HOME", "IDLE", "RUNNING", "PAUSED"],
HOME: ["HOME", "ON", "IDLE", "ESTOP"],
IDLE: ["IDLE", "RUNNING", "PAUSED", "HOME", "ESTOP", "OFF"],
RUNNING: ["RUNNING", "PAUSED", "IDLE", "ESTOP"],
PAUSED: ["PAUSED", "RUNNING", "IDLE", "ESTOP"],
});
function programActivity(task, program) {
if (task?.state === "PAUSED" || program?.program_status === "paused") return "PAUSED";
if (task?.state === "RUNNING" || program?.program_status === "running") return "RUNNING";
return "IDLE";
}
export function axisState({ task = {}, program = {}, action = null, payload = {} } = {}) {
const taskState = String(task.state ?? "NEW").toUpperCase();
let machineState = ["ESTOP", "FAULT"].includes(taskState) ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(taskState) ? "ON" : "OFF";
let operationState = programActivity(task, program);
if (action === "ESTOP" && payload.active === true) machineState = "ESTOP";
if (action === "ESTOP" && payload.active === false) machineState = "ESTOP_RESET";
if (action === "IO_RECOVER") machineState = "ON";
if (action === "HOME") operationState = "HOME";
if (action === "PROGRAM_PAUSE") operationState = "PAUSED";
if (action === "PROGRAM_RUN" || action === "PROGRAM_RESUME") operationState = "RUNNING";
if (["PROGRAM_LOAD", "PROGRAM_ABORT", "PROGRAM_RESTART", "MDI_EXECUTE"].includes(action)) operationState = "IDLE";
const state = machineState === "ESTOP" || machineState === "ESTOP_RESET" ? machineState : operationState === "HOME" ? "HOME" : operationState === "RUNNING" ? "RUNNING" : operationState === "PAUSED" ? "PAUSED" : machineState === "OFF" ? "OFF" : "IDLE";
const sourceTaskState = taskState === "ESTOP" ? "STATE_ESTOP" : machineState === "ESTOP_RESET" ? "STATE_ESTOP_RESET" : machineState === "OFF" ? "STATE_OFF" : "STATE_ON";
const sourceInterpState = operationState === "PAUSED" ? "INTERP_PAUSED" : operationState === "RUNNING" ? "INTERP_READING" : "INTERP_IDLE";
const sourceMode = String(task.mode ?? "MANUAL").toUpperCase();
return Object.freeze({ state, machine_state: machineState, operation_state: operationState, source_task_state: sourceTaskState, source_interp_state: sourceInterpState, source_mode: sourceMode, states: [...AXIS_STATES] });
}
export function validateAxisTransition(previous, next) {
if (!AXIS_STATES.includes(previous) || !AXIS_STATES.includes(next)) return { status: "blocked", code: "axis_state_unknown" };
return AXIS_TRANSITIONS[previous]?.includes(next) ? { status: "accepted", previous, next } : { status: "blocked", code: "axis_state_transition_invalid", previous, next };
}

View File

@@ -385,7 +385,8 @@ export class ProgramRunner {
if (motionControl.error) return { status: "error", code: motionControl.error };
this.motionControl = motionControl;
this.state.line += 1;
return { status: "verified", block, state: cloneState(this.state), runner: this.snapshot() };
const interpreterState = cloneState(this.state);
return { status: "verified", block, state: interpreterState, interpreter_state: interpreterState, runner: this.snapshot() };
}
try {
this.ensureMemory();
@@ -396,7 +397,8 @@ export class ProgramRunner {
const stateResult = call(this.core, "sim_interpreter_read_state", "number", ["number", "number"], [this.interpreter, this.stateBuffer]);
if (stateResult !== 0) return { status: "error", code: "mdi_state_read", result: stateResult };
this.state = readInterpreterState(this.core, this.stateBuffer);
return { status: "verified", block, state: cloneState(this.state), runner: this.snapshot() };
const interpreterState = cloneState(this.state);
return { status: "verified", block, state: interpreterState, interpreter_state: interpreterState, runner: this.snapshot() };
} catch (error) {
return { status: "error", code: error.message };
}

View File

@@ -20,6 +20,7 @@ import { PythonCallbacks } from "./python-callbacks.mjs";
import { createTrace, validateTrace } from "./trace-export.mjs";
import { ConfigurationBatchRuntime } from "../workspace/configuration-batch.mjs";
import { AuthoritativeRuntime } from "./authoritative-runtime.mjs";
import { axisState } from "./axis-state.mjs";
const PROTOCOL_SCHEMA = 1;
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
@@ -98,6 +99,9 @@ export class SimSession {
}
envelope(kind, input, payload, tick = this.tick) {
const statePayload = payload && typeof payload === "object" ? payload : {};
const program = statePayload.program ?? (this.program.loaded ? this.program.snapshot() : {});
const task = statePayload.task ?? this.task.export();
return {
schema_version: PROTOCOL_SCHEMA,
kind,
@@ -106,7 +110,7 @@ export class SimSession {
seq: input.seq,
...(text(input.operation_id) ? { operation_id: input.operation_id } : {}),
simulation_tick: tick,
payload,
payload: { ...statePayload, axis_state: axisState({ task, program, action: input.kind, payload: input.payload ?? {} }) },
};
}
@@ -268,7 +272,6 @@ export class SimSession {
if (event.kind === "ESTOP") replayDeviceIo.estop(event.payload?.active === true);
if (event.kind === "IO_RECOVER") replayDeviceIo.interlockReset();
if (event.kind === "SIM_STEP") replayMotion.step(event.tick);
if (event.kind === "SIM_STEP") replayAuthoritative.step(event.tick);
if (event.kind === "PROGRAM_LOAD") replayProgram.load(event.payload?.source ?? event.payload?.text ?? event.payload?.program_source, event.payload?.path ?? null);
if (event.kind === "PROGRAM_RUN") replayProgram.start();
if (event.kind === "PROGRAM_PAUSE") replayProgram.pause();
@@ -277,8 +280,16 @@ export class SimSession {
if (event.kind === "PROGRAM_RESTART") replayProgram.restart();
if (event.kind === "SIM_STEP" && replayProgram.loaded) {
if (replayProgram.status === "paused" && state.task?.program_status === "running") replayProgram.resume();
replayProgram.tick(Number.isSafeInteger(event.payload?.cycles) ? event.payload.cycles : 1);
replayAuthoritative.syncProgram(replayProgram.snapshot());
for (let replayTick = replayAuthoritative.state.tick + 1; replayTick <= event.tick; replayTick += 1) {
replayProgram.tick(1);
const synced = replayAuthoritative.syncProgram(replayProgram.snapshot());
if (synced.status === "blocked") throw new Error(synced.code);
const stepped = replayAuthoritative.step(replayTick);
if (stepped.status === "error") throw new Error(stepped.code);
}
} else if (event.kind === "SIM_STEP") {
const stepped = replayAuthoritative.step(event.tick);
if (stepped.status === "error") throw new Error(stepped.code);
}
if (event.kind === "PLC_CONFIG") replayPlc.configure(event.payload);
if (event.kind === "PLC_SET") replayPlc.setRegister(event.payload?.register, event.payload?.value);

View File

@@ -0,0 +1,108 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { fnv1a64 } from "./replay.mjs";
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
export const SELECTED_CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini";
const EXPECTED_FILES = [
SELECTED_CASE,
"linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.hal",
"linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial",
"linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial_mm.var",
"linuxcnc/configs/sim/axis/vismach/3axis-tutorial/tool.tbl",
"linuxcnc/linuxcnc.gif",
];
const fetchImpl = async (url) => {
try {
const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url));
return { ok: true, text: async () => body.toString("utf8") };
} catch {
return { ok: false, text: async () => "" };
}
};
const bundle = await loadSimulationCase({ path: SELECTED_CASE, fetchImpl });
assert.equal(bundle.status, "ready");
assert.deepEqual(bundle.missing, []);
assert.deepEqual(bundle.optional_missing, []);
assert.equal(bundle.catalog.path, SELECTED_CASE);
assert.equal(bundle.catalog.coordinates, "X Y Z");
for (const path of EXPECTED_FILES) assert.ok(Object.hasOwn(bundle.files, path), `missing exact case file: ${path}`);
assert.equal(bundle.files["linuxcnc/configs/sim/axis/vismach/3axis-tutorial/tool.tbl"].includes("T1"), true);
const wasm = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) });
const session = new SimSession(wasm, { sessionId: "axis3-selected-case" });
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId };
let sequence = 0;
const send = (kind, operationId, payload = {}) => {
const response = session.accept({ ...base, kind, seq: ++sequence, operation_id: operationId, payload });
assert.notEqual(response.kind, "ERROR", `${kind}:${response.payload?.code ?? "error"}`);
return response;
};
assert.equal(send("HELLO", "hello").kind, "CAPABILITIES");
assert.equal(send("SESSION_ATTACH", "attach").kind, "SESSION_READY");
const loaded = send("LOAD_WORKSPACE", "load", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
assert.equal(loaded.payload.configuration.state, "TASK_READY");
assert.equal(loaded.payload.configuration.hal.blocked_count, 0);
assert.ok(loaded.payload.configuration.hal.records.some((record) => record.program === "./3axis-tutorial" && record.capability === "WEB-EQUIV"));
assert.equal(send("HOME", "home-z", { axis: "z" }).payload.status, "verified");
assert.equal(send("HOME", "home-x", { axis: "x" }).payload.status, "verified");
assert.equal(send("HOME", "home-y", { axis: "y" }).payload.status, "verified");
send("JOG_START", "jog-x", { axis: "x", direction: 1, speed: 2 });
const jog = send("SIM_STEP", "jog-step", { cycles: 10 });
assert.ok(jog.payload.motion.axes.x > 0);
assert.equal(send("JOG_STOP", "jog-stop").payload.status, "verified");
assert.equal(send("SET_OVERRIDE", "override", { feed: 0.5, spindle: 1.2 }).payload.state.feed_override, 0.5);
send("PROGRAM_LOAD", "program-load", { path: "axis3.ngc", source: "G21 G90\nG0 X0 Y0 Z0\nG1 X10 Y5 F120\nG1 Z-1\nM2\n" });
assert.equal(send("PROGRAM_RUN", "program-run").payload.task.state, "RUNNING");
const checkpoint = send("CHECKPOINT", "checkpoint").payload.checkpoint;
send("SIM_STEP", "program-step", { cycles: 8 });
const programSnapshot = send("SNAPSHOT_REQUEST", "program-snapshot");
assert.ok(["running", "completed"].includes(programSnapshot.payload.snapshot?.state?.program?.program_status ?? "running"));
assert.equal(send("SPINDLE_COMMAND", "spindle-on", { spindle: 0, rpm: 600, enabled: true }).payload.status, "verified");
assert.equal(send("IO_STEP", "spindle-step", { ticks: 5 }).payload.status, "verified");
assert.equal(send("SPINDLE_COMMAND", "spindle-off", { spindle: 0, rpm: 0, enabled: false }).payload.status, "verified");
assert.equal(send("IO_STEP", "spindle-stop-step", { ticks: 10 }).payload.status, "verified");
assert.equal(send("COOLANT_COMMAND", "coolant", { mist: true, flood: true, lube: false }).payload.status, "verified");
assert.equal(send("TOOL_PREPARE", "tool-prepare", { tool: 1, pocket: 1 }).payload.status, "verified");
assert.equal(send("TOOL_PREPARED", "tool-prepared", { prepared: true }).payload.status, "verified");
assert.equal(send("TOOL_CHANGE", "tool-change").payload.status, "verified");
assert.equal(send("TOOL_CHANGED", "tool-changed", { changed: true }).payload.status, "verified");
const safetyCore = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) });
const safetySession = new SimSession(safetyCore, { sessionId: "axis3-selected-case-safety" });
const safetyBase = { schema_version: 1, context_id: CONTEXT_ID, session_id: safetySession.sessionId };
let safetySequence = 0;
const safetySend = (kind, operationId, payload = {}) => safetySession.accept({ ...safetyBase, kind, seq: ++safetySequence, operation_id: operationId, payload });
safetySend("HELLO", "hello");
safetySend("SESSION_ATTACH", "attach");
safetySend("LOAD_WORKSPACE", "load", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
safetySend("HOME", "home-z", { axis: "z" });
safetySend("HOME", "home-x", { axis: "x" });
safetySend("HOME", "home-y", { axis: "y" });
const estop = safetySend("ESTOP", "estop", { active: true });
assert.equal(estop.kind, "ERROR");
assert.equal(estop.payload.code, "sim_io_estop");
assert.equal(safetySend("ESTOP", "estop-clear", { active: false }).payload.status, "verified");
assert.equal(safetySend("IO_RECOVER", "io-recover").payload.status, "verified");
const recovered = session.restoreCheckpoint(checkpoint, session.timeline.export(), (operation) => operation);
assert.equal(recovered.status, "recovered");
const result = {
schema: "axis3-selected-case-parity.v1",
status: "verified",
case: SELECTED_CASE,
machine: { type: "cartesian_vismach", axes: ["X", "Y", "Z"], joints: 3, kinematics: "trivkins", model: "3axis-tutorial" },
exact_files: EXPECTED_FILES,
loader: { fetched: bundle.fetched.length, missing: bundle.missing.length, state_hash: bundle.state_hash },
startup: { state: loaded.payload.configuration.state, hal_blocked: loaded.payload.configuration.hal.blocked_count },
operation_sequence: ["HOME", "JOG_START", "SIM_STEP", "JOG_STOP", "SET_OVERRIDE", "PROGRAM_LOAD", "PROGRAM_RUN", "CHECKPOINT", "SPINDLE_COMMAND", "COOLANT_COMMAND", "TOOL_PREPARE", "TOOL_CHANGE", "ESTOP", "IO_RECOVER", "RECOVER"],
observed: { jog_x: jog.payload.motion.axes.x, program_status: programSnapshot.payload.snapshot?.state?.program?.program_status ?? null, checkpoint_hash: checkpoint.checkpoint_hash, recovery: recovered.status, final_hash: fnv1a64(session.workspacePackageState()) },
};
console.log(JSON.stringify(result));

View File

@@ -0,0 +1,37 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { preflightHalConfiguration } from "../workspace/hal-configuration.mjs";
import { parseIni } from "../workspace/ini-parser.mjs";
import { WorkspaceRuntime } from "../workspace/runtime.mjs";
import { loadSimulationCase } from "../workspace/sim-case-loader.mjs";
import { startConfiguration } from "../workspace/configuration-startup.mjs";
const CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini";
const HAL = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.hal";
const fetchImpl = async (url) => {
try {
const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url));
return { ok: true, text: async () => body.toString("utf8") };
} catch {
return { ok: false, text: async () => "" };
}
};
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
assert.equal(bundle.status, "ready");
const workspace = new WorkspaceRuntime({ files: bundle.files });
const ini = parseIni({ workspace, entrypoint: CASE });
const lines = workspace.read(`/workspace/${HAL}`).split(/\r?\n/);
const manifest = bundle.halManifest;
const preflight = preflightHalConfiguration({ lines, variables: ini.sections, manifest, webEquivalentPrograms: ["./3axis-tutorial"] });
assert.equal(preflight.status, "ready");
assert.equal(preflight.blocked_count, 0);
const model = preflight.records.find((row) => row.command === "loadusr" && row.program === "./3axis-tutorial");
assert.equal(model.capability, "WEB-EQUIV");
assert.equal(preflight.records.filter((row) => row.command === "loadrt").map((row) => row.module).join(","), "trivkins,motmod,ddt,hypot");
assert.equal(preflight.records.filter((row) => row.command === "net").length, 21);
const startup = startConfiguration({ workspace, entrypoint: CASE, core: { _sim_create() { return 0; }, _sim_load_workspace() { return 0; } }, halManifest: manifest });
assert.equal(startup.status, "ready");
assert.equal(startup.hal.blocked_count, 0);
assert.ok(startup.hal.records.some((row) => row.program === "./3axis-tutorial" && row.capability === "WEB-EQUIV"));
console.log(JSON.stringify({ schema: "axis3-tutorial-hal-adapter.v1", status: "verified", case: CASE, hal: { record_count: preflight.record_count, loadrt: preflight.records.filter((row) => row.command === "loadrt").map((row) => row.module), net_count: preflight.records.filter((row) => row.command === "net").length, web_equivalent: model.program, capability: model.capability }, startup: { state: startup.state, hal_blocked: startup.hal.blocked_count, state_hash: startup.state_hash } }));

View File

@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
const CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini";
const fetchImpl = async (url) => {
try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; }
catch { return { ok: false, text: async () => "" }; }
};
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
const wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
function create() {
const corePromise = createModule({ wasmBinary });
return corePromise.then((core) => {
const session = new SimSession(core, { sessionId: "axis3-tutorial-motion" });
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId };
let seq = 0;
const send = (kind, operationId, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: operationId, payload });
send("HELLO", "hello"); send("SESSION_ATTACH", "attach");
const load = send("LOAD_WORKSPACE", "load", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
assert.equal(load.payload.configuration.state, "TASK_READY");
return { session, send };
});
}
const run = await create();
assert.equal(run.send("HOME", "home-z", { axis: "z" }).payload.status, "verified");
assert.equal(run.send("HOME", "home-x", { axis: "x" }).payload.status, "verified");
assert.equal(run.send("HOME", "home-y", { axis: "y" }).payload.status, "verified");
assert.equal(run.send("JOG_START", "x-start", { axis: "x", direction: 1, speed: 10 }).payload.status, "verified");
const x = run.send("SIM_STEP", "x-step", { cycles: 10 });
assert.ok(x.payload.motion.axes.x > 0);
assert.equal(x.payload.motion.axes.y, 0);
assert.equal(x.payload.motion.axes.z, 0);
run.send("JOG_STOP", "x-stop");
run.send("JOG_START", "y-start", { axis: "y", direction: -1, speed: 5 });
const y = run.send("SIM_STEP", "y-step", { cycles: 5 });
assert.ok(y.payload.motion.axes.y < 0);
run.send("JOG_STOP", "y-stop");
run.send("JOG_START", "z-start", { axis: "z", direction: -1, speed: 2 });
const z = run.send("SIM_STEP", "z-step", { cycles: 5 });
assert.ok(z.payload.motion.axes.z < 0);
run.send("JOG_STOP", "z-stop");
assert.equal(run.send("LIMIT_SET", "limit", { axis: "x", active: true }).payload.status, "verified");
const limited = run.send("JOG_START", "limit-jog", { axis: "x", direction: 1, speed: 1 });
assert.equal(limited.kind, "ERROR");
assert.equal(limited.payload.code, "motion_faulted");
assert.equal(run.send("LIMIT_SET", "limit-clear", { axis: "x", active: false }).payload.status, "verified");
assert.equal(run.send("FOLLOWING_ERROR", "ferror", { value: 0.02 }).payload.status, "verified");
const ferror = run.send("JOG_START", "ferror-jog", { axis: "x", direction: 1, speed: 1 });
assert.equal(ferror.kind, "ERROR");
assert.equal(ferror.payload.code, "motion_faulted");
const report = run.send("SNAPSHOT_REQUEST", "snapshot");
console.log(JSON.stringify({ schema: "axis3-tutorial-motion.v1", status: "verified", case: CASE, axes: ["X", "Y", "Z"], home: true, positions: { x: x.payload.motion.axes.x, y: y.payload.motion.axes.y, z: z.payload.motion.axes.z }, limits: { blocked_code: limited.payload.code, cleared: true }, following_error: { blocked_code: ferror.payload.code }, snapshot_hash: report.payload.state_hash }));

View File

@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { compareEndpoints, createEndpoint, endpointHash } from "./endpoint-replay.mjs";
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
const CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini";
const fetchImpl = async (url) => { try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; } catch { return { ok: false, text: async () => "" }; } };
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
const wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
async function create(id) {
const core = await createModule({ wasmBinary });
const session = new SimSession(core, { sessionId: id });
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: id };
let seq = 0;
const send = (kind, operationId, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: operationId, payload });
send("HELLO", "hello"); send("SESSION_ATTACH", "attach");
const loaded = send("LOAD_WORKSPACE", "load", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
assert.equal(loaded.payload.configuration.state, "TASK_READY");
send("PROGRAM_LOAD", "program", { path: "persist.ngc", source: "G21 G90\nG0 X0 Y0 Z0\nG1 X2 Y1 F60\nG1 Z-1\nM2\n" });
send("PROGRAM_RUN", "run");
return { session, send };
}
const continuous = await create("axis3-tutorial-persistence");
const checkpoint = continuous.send("CHECKPOINT", "checkpoint").payload.checkpoint;
continuous.send("SIM_STEP", "step-a", { cycles: 3 });
const continuousEndpoint = createEndpoint({ session: continuous.session });
const recovered = await create("axis3-tutorial-persistence");
const recovery = recovered.session.restoreCheckpoint(checkpoint, continuous.session.timeline.export(), (operation) => operation);
assert.equal(recovery.status, "recovered");
const recoveredEndpoint = createEndpoint({ session: recovered.session });
const parity = compareEndpoints(continuousEndpoint, recoveredEndpoint);
assert.equal(parity.equal, true);
const bad = structuredClone(checkpoint);
bad.state_hash = "fnv1a64:bad";
const blocked = recovered.session.restoreCheckpoint(bad, null, (operation) => operation);
assert.equal(blocked.status, "blocked");
const continuousHash = endpointHash(continuousEndpoint);
const recoveredHash = endpointHash(recoveredEndpoint);
console.log(JSON.stringify({ schema: "axis3-tutorial-persistence.v1", status: "verified", case: CASE, exact_runtime_files: ["3axis-tutorial_mm.var", "tool.tbl", "position_mm.txt"], checkpoint_hash: checkpoint.checkpoint_hash, continuous_hash: continuousHash, recovered_hash: recoveredHash, endpoint_equal: parity.equal, blocked_bad_checkpoint: blocked.status, event_cursor: checkpoint.event_cursor }));

View File

@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
const CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini";
const fetchImpl = async (url) => { try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; } catch { return { ok: false, text: async () => "" }; } };
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
const wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
const core = await createModule({ wasmBinary });
const session = new SimSession(core, { sessionId: "axis3-tutorial-program" });
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId };
let seq = 0;
const send = (kind, operationId, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: operationId, payload });
send("HELLO", "hello"); send("SESSION_ATTACH", "attach");
const loaded = send("LOAD_WORKSPACE", "load", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
assert.equal(loaded.payload.configuration.state, "TASK_READY");
const source = "G21 G90\nG0 X0 Y0 Z0\nG1 X10 Y5 F120\nG91\nG1 Z-1 F60\nG90\nM2\n";
assert.equal(send("PROGRAM_LOAD", "load-program", { path: "tutorial.ngc", source }).payload.status, "verified");
assert.equal(send("PROGRAM_RUN", "run").payload.task.state, "RUNNING");
const pauseTick = send("SIM_STEP", "tick-1", { cycles: 1 });
assert.equal(pauseTick.payload.task.state, "RUNNING");
assert.equal(send("PROGRAM_PAUSE", "pause").payload.task.state, "PAUSED");
assert.equal(send("PROGRAM_RESUME", "resume").payload.task.state, "RUNNING");
const finish = send("SIM_STEP", "finish", { cycles: 20 });
assert.equal(finish.payload.program.program_status, "completed");
assert.equal(finish.payload.task.state, "STOPPED");
assert.equal(finish.payload.program.state.units, 21);
assert.equal(finish.payload.program.state.distance_mode, 90);
assert.equal(finish.payload.program.state.motion_mode, 1);
assert.equal(finish.payload.program.state.x, 10);
assert.equal(finish.payload.program.state.y, 5);
assert.equal(finish.payload.program.state.z, -1);
assert.equal(finish.payload.program.state.feed, 60);
assert.equal(send("PROGRAM_RESTART", "restart").payload.task.program_status, "loaded");
const stepped = send("PROGRAM_STEP", "step");
assert.equal(stepped.payload.status, "verified");
assert.equal(stepped.payload.task.state, "PAUSED");
const mdiBlocked = send("MDI_EXECUTE", "mdi-blocked", { block: "G0 X2 Y3 Z-1" });
assert.equal(mdiBlocked.kind, "ERROR");
assert.equal(mdiBlocked.payload.code, "task_state");
assert.equal(mdiBlocked.payload.message_key, "mdi_while_program_active");
assert.equal(send("PROGRAM_ABORT", "abort").payload.task.state, "STOPPED");
const mdi = send("MDI_EXECUTE", "mdi", { block: "G0 X2 Y3 Z-1" });
assert.equal(mdi.payload.status, "verified");
assert.equal(mdi.payload.interpreter_state.distance_mode, 90);
assert.equal(send("PROGRAM_LOAD", "invalid-load", { path: "invalid.ngc", source: "G999\n" }).payload.status, "verified");
const invalid = send("PROGRAM_STEP", "invalid-step");
assert.equal(invalid.kind, "ERROR");
console.log(JSON.stringify({ schema: "axis3-tutorial-program.v1", status: "verified", case: CASE, units: "G21", distance_modes: ["G90", "G91"], run: { paused: true, resumed: true, final: finish.payload.program.program_status, state: finish.payload.task.state }, mdi: { blocked_while_paused: mdiBlocked.payload.message_key, status: mdi.payload.status, distance_mode: mdi.payload.interpreter_state.distance_mode, position: { x: mdi.payload.interpreter_state.x, y: mdi.payload.interpreter_state.y, z: mdi.payload.interpreter_state.z } }, restart: "loaded", step: "verified", invalid_code: invalid.payload.code, final_hash: finish.payload.state_hash }));

View File

@@ -0,0 +1,45 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { fnv1a64 } from "./replay.mjs";
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
const CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini";
const fetchImpl = async (url) => { try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; } catch { return { ok: false, text: async () => "" }; } };
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) });
const session = new SimSession(core, { sessionId: "axis3-tutorial-sequence" });
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId };
let seq = 0;
const rows = [];
const send = (operation, kind, payload = {}) => {
const response = session.accept({ ...base, kind, seq: ++seq, operation_id: operation, payload });
rows.push({ operation, kind, seq, tick: response.simulation_tick ?? session.tick, response_kind: response.kind, status: response.payload?.status ?? null, code: response.payload?.code ?? null, state_hash: response.payload?.state_hash ?? response.payload?.snapshot?.state_hash ?? null });
return response;
};
send("HELLO", "HELLO"); send("SESSION_ATTACH", "SESSION_ATTACH");
const loaded = send("LOAD_WORKSPACE", "LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
assert.equal(loaded.payload.configuration.state, "TASK_READY");
assert.equal(send("HOME_Z", "HOME", { axis: "z" }).payload.status, "verified");
assert.equal(send("HOME_X", "HOME", { axis: "x" }).payload.status, "verified");
assert.equal(send("HOME_Y", "HOME", { axis: "y" }).payload.status, "verified");
send("JOG_X", "JOG_START", { axis: "x", direction: 1, speed: 2 });
send("JOG_X_TICK", "SIM_STEP", { cycles: 10 });
send("JOG_STOP", "JOG_STOP");
send("PROGRAM_LOAD", "PROGRAM_LOAD", { path: "sequence.ngc", source: "G21 G90\nG0 X0 Y0 Z0\nG1 X1 Y1 F60\nG1 Z-1\nG1 X2\nM2\n" });
assert.equal(send("PROGRAM_RUN", "PROGRAM_RUN").payload.task.state, "RUNNING");
const checkpoint = send("CHECKPOINT", "CHECKPOINT").payload.checkpoint;
send("PROGRAM_TICK", "SIM_STEP", { cycles: 1 });
send("PROGRAM_PAUSE", "PROGRAM_PAUSE");
send("PROGRAM_RESUME", "PROGRAM_RESUME");
send("PROGRAM_ABORT", "PROGRAM_ABORT");
send("PROGRAM_RESTART", "PROGRAM_RESTART");
send("TOOL_PREPARE_T1", "TOOL_PREPARE", { tool: 1, pocket: 1 });
send("TOOL_PREPARED_T1", "TOOL_PREPARED", { prepared: true });
send("TOOL_CHANGE_T1", "TOOL_CHANGE");
send("TOOL_CHANGED_T1", "TOOL_CHANGED", { changed: true });
const endpoint = session.workspacePackageState();
assert.ok(checkpoint.checkpoint_hash);
assert.ok(endpoint.state_hash);
console.log(JSON.stringify({ schema: "axis3-tutorial-sequence.v1", status: "verified", case: CASE, operations: rows, checkpoint_hash: checkpoint.checkpoint_hash, endpoint_hash: endpoint.state_hash, sequence_hash: fnv1a64(rows), first_error: rows.find((row) => row.response_kind === "ERROR") ?? null }));

View File

@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { fnv1a64 } from "./replay.mjs";
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
import { WorkspaceRuntime } from "../workspace/runtime.mjs";
import { startConfiguration } from "../workspace/configuration-startup.mjs";
const CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini";
const fetchImpl = async (url) => {
try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; }
catch { return { ok: false, text: async () => "" }; }
};
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
assert.equal(bundle.status, "ready");
const firstWorkspace = new WorkspaceRuntime({ files: bundle.files });
const secondWorkspace = new WorkspaceRuntime({ files: bundle.files });
const core = { _sim_create() { return 0; }, _sim_load_workspace() { return 0; } };
const first = startConfiguration({ workspace: firstWorkspace, entrypoint: CASE, core, halManifest: bundle.halManifest });
const second = startConfiguration({ workspace: secondWorkspace, entrypoint: CASE, core, halManifest: bundle.halManifest });
assert.equal(first.status, "ready");
assert.equal(first.state, "TASK_READY");
assert.deepEqual(first.transitions.map((row) => row.to), ["NEW", "MOUNTED", "PARSED", "RESOLVED", "CORE_CREATED", "HAL_READY", "TASK_READY"]);
assert.equal(first.state_hash, second.state_hash);
assert.equal(firstWorkspace.read("/workspace/linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial_mm.var"), "");
assert.equal(bundle.catalog.coordinates, "X Y Z");
assert.equal(bundle.catalog.kinematics, "trivkins");
assert.equal(first.resources.resources.find((row) => row.type === "image").path, "linuxcnc/linuxcnc.gif");
assert.equal(first.hal.records.some((row) => row.program === "./3axis-tutorial" && row.capability === "WEB-EQUIV"), true);
const configuration = toWorkerConfiguration(bundle);
assert.equal(configuration.entrypoint, CASE);
assert.equal(fnv1a64({ bundle: bundle.state_hash, startup: first.state_hash }), fnv1a64({ bundle: bundle.state_hash, startup: second.state_hash }));
console.log(JSON.stringify({ schema: "axis3-tutorial-startup.v1", status: "verified", case: CASE, transitions: first.transitions.map((row) => row.to), loader_hash: bundle.state_hash, startup_hash: first.state_hash, deterministic: first.state_hash === second.state_hash, generated: bundle.runtime_fixtures.map((row) => row.resource_path), hal_blocked: first.hal.blocked_count }));

View File

@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
const CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini";
const fetchImpl = async (url) => { try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; } catch { return { ok: false, text: async () => "" }; } };
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) });
const session = new SimSession(core, { sessionId: "axis3-tutorial-tool-io" });
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId };
let seq = 0;
const send = (kind, operationId, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: operationId, payload });
send("HELLO", "hello"); send("SESSION_ATTACH", "attach");
const loaded = send("LOAD_WORKSPACE", "load", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
assert.equal(loaded.payload.configuration.state, "TASK_READY");
const spindleOn = send("SPINDLE_COMMAND", "spindle-on", { spindle: 0, rpm: 600, enabled: true });
assert.equal(spindleOn.payload.status, "verified");
const spindleStep = send("IO_STEP", "spindle-step", { ticks: 10 });
assert.equal(spindleStep.payload.status, "verified");
assert.equal(spindleStep.payload.state.spindles[0].at_speed, true);
assert.equal(send("SPINDLE_COMMAND", "spindle-off", { spindle: 0, rpm: 0, enabled: false }).payload.status, "verified");
assert.equal(send("IO_STEP", "spindle-stop", { ticks: 10 }).payload.status, "verified");
assert.equal(send("TOOL_PREPARE", "prepare", { tool: 1, pocket: 1 }).payload.status, "verified");
assert.equal(send("TOOL_PREPARED", "prepared", { prepared: true }).payload.status, "verified");
assert.equal(send("TOOL_CHANGE", "change").payload.status, "verified");
const changed = send("TOOL_CHANGED", "changed", { changed: true });
assert.equal(changed.payload.status, "verified");
assert.equal(changed.payload.state.tool.current, 1);
const busyChange = send("TOOL_CHANGE", "busy-change");
assert.equal(busyChange.kind, "ERROR");
const coolant = send("COOLANT_COMMAND", "coolant-on", { mist: true, flood: true, lube: false });
assert.equal(coolant.payload.status, "verified");
assert.equal(coolant.payload.device_io.coolant.mist, true);
assert.equal(coolant.payload.device_io.coolant.flood, true);
assert.equal(send("COOLANT_FAULT", "coolant-fault", { fault: true }).kind, "ERROR");
assert.equal(send("COOLANT_FAULT", "coolant-clear", { fault: false }).payload.status, "verified");
assert.equal(send("COOLANT_COMMAND", "coolant-off", { mist: false, flood: false, lube: false }).payload.status, "verified");
const armed = send("PROBE_CONFIG", "probe-arm", { action: "arm", probe_type: 0 });
assert.equal(armed.payload.status, "verified");
const tripped = send("PROBE_CONFIG", "probe-trip", { action: "update", input: true, position: { x: 1, y: 2, z: -3 } });
assert.equal(tripped.payload.state.probe.state, "tripped");
assert.deepEqual(tripped.payload.state.probe.position, { x: 1, y: 2, z: -3 });
assert.equal(send("PROBE_CONFIG", "probe-disarm", { action: "disarm" }).payload.status, "verified");
const estop = send("ESTOP", "estop", { active: true });
assert.equal(estop.kind, "ERROR");
assert.equal(estop.payload.code, "sim_io_estop");
assert.equal(send("ESTOP", "estop-clear", { active: false }).payload.status, "verified");
assert.equal(send("IO_RECOVER", "recover").payload.status, "verified");
console.log(JSON.stringify({ schema: "axis3-tutorial-tool-io.v1", status: "verified", case: CASE, tool: { current: changed.payload.state.tool.current, changed: true, busy_change_code: busyChange.payload.code }, spindle: { command_rpm: spindleStep.payload.state.spindles[0].command_rpm, speed_rpm: spindleStep.payload.state.spindles[0].speed_rpm, at_speed: spindleStep.payload.state.spindles[0].at_speed }, coolant: { mist: true, flood: true, fault_blocked: true }, probe: { state: tripped.payload.state.probe.state, position: tripped.payload.state.probe.position }, estop: { blocked_code: estop.payload.code, recovered: true }, final_hash: session.workspacePackageState().state_hash }));

View File

@@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import { axisState, AXIS_STATES, LINUXCNC_STATE_CONSTANTS, validateAxisTransition } from "./axis-state.mjs";
assert.deepEqual(AXIS_STATES, ["ESTOP", "ESTOP_RESET", "OFF", "ON", "HOME", "IDLE", "RUNNING", "PAUSED"]);
assert.deepEqual(LINUXCNC_STATE_CONSTANTS, { STATE_ESTOP: 1, STATE_ESTOP_RESET: 2, STATE_OFF: 3, STATE_ON: 4, MODE_MANUAL: 1, MODE_AUTO: 2, MODE_MDI: 3, INTERP_IDLE: 1, INTERP_READING: 2, INTERP_PAUSED: 3, INTERP_WAITING: 4 });
assert.equal(axisState({ task: { state: "NEW" }, action: "SESSION_ATTACH" }).state, "OFF");
assert.deepEqual(axisState({ task: { state: "ESTOP" }, action: "ESTOP", payload: { active: true } }).state, "ESTOP");
assert.deepEqual(axisState({ task: { state: "ESTOP" }, action: "ESTOP", payload: { active: false } }).state, "ESTOP_RESET");
assert.equal(axisState({ task: { state: "READY" }, action: "HOME" }).state, "HOME");
assert.equal(axisState({ task: { state: "RUNNING" }, program: { program_status: "running" }, action: "SIM_STEP" }).state, "RUNNING");
assert.equal(axisState({ task: { state: "PAUSED" }, program: { program_status: "paused" }, action: "PROGRAM_PAUSE" }).state, "PAUSED");
assert.equal(axisState({ task: { state: "READY" }, program: { program_status: "idle" }, action: "IO_RECOVER" }).state, "IDLE");
assert.equal(axisState({ task: { state: "RUNNING", mode: "AUTO" }, program: { program_status: "running" } }).source_interp_state, "INTERP_READING");
assert.equal(axisState({ task: { state: "PAUSED", mode: "AUTO" }, program: { program_status: "paused" } }).source_interp_state, "INTERP_PAUSED");
assert.equal(validateAxisTransition("ESTOP", "ESTOP_RESET").status, "accepted");
assert.equal(validateAxisTransition("ESTOP_RESET", "ON").status, "accepted");
assert.equal(validateAxisTransition("ON", "RUNNING").status, "accepted");
assert.equal(validateAxisTransition("RUNNING", "HOME").status, "blocked");
console.log(JSON.stringify({ schema: "axis-state-v1-sample", status: "verified", states: AXIS_STATES, cases: ["ESTOP", "ESTOP_RESET", "OFF", "ON", "HOME", "IDLE", "RUNNING", "PAUSED"] }));

View File

@@ -21,11 +21,15 @@ assert.equal(ini.sections.EMCMOT.EMCMOT, "motmod");
assert.equal(ini.sections.KINS.KINEMATICS, "corexykins");
const resources = resolveResources({ workspace, entrypoint: paths[0], ini });
assert.equal(resources.status, "blocked");
assert.deepEqual(resources.resources.filter((row) => row.status === "blocked").map((row) => row.path).sort(), [
assert.equal(resources.status, "ready");
assert.deepEqual(resources.resources.filter((row) => row.runtime_generated).map((row) => row.path).sort(), [
"linuxcnc/configs/sim/axis/corexy/linuxcnc.var",
"linuxcnc/configs/sim/axis/corexy/tool.tbl",
]);
assert.deepEqual(resources.generated_files, {
"linuxcnc/configs/sim/axis/corexy/linuxcnc.var": "",
"linuxcnc/configs/sim/axis/corexy/tool.tbl": "",
});
const rawLines = files[paths[2]].split(/\r?\n/); const halLines = [];
for (const rawLine of rawLines) {
@@ -39,14 +43,10 @@ assert.equal(hal.blocked_count, 0);
const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) });
const startup = startConfiguration({ workspace, entrypoint: paths[0], core, halManifest: manifest });
assert.equal(startup.status, "blocked");
assert.equal(startup.state, "BLOCKED");
const startupResourceDiagnostic = startup.diagnostics.find((row) => row.code === "startup_resources_blocked");
assert.ok(startupResourceDiagnostic);
assert.deepEqual(startupResourceDiagnostic.resources.map((row) => row.path).sort(), [
"linuxcnc/configs/sim/axis/corexy/linuxcnc.var",
"linuxcnc/configs/sim/axis/corexy/tool.tbl",
]);
assert.equal(startup.status, "ready");
assert.equal(startup.state, "TASK_READY");
assert.equal(workspace.read("/workspace/linuxcnc/configs/sim/axis/corexy/linuxcnc.var"), "");
assert.equal(workspace.read("/workspace/linuxcnc/configs/sim/axis/corexy/tool.tbl"), "");
console.log(JSON.stringify({
schema: "configuration-corexy-s7-05-sample.v1",
@@ -54,6 +54,6 @@ console.log(JSON.stringify({
entrypoint: paths[0],
include_graph: ini.graph,
hal_preflight: { status: hal.status, record_count: hal.record_count, blocked_count: hal.blocked_count },
runtime_startup: { status: startup.status, state: startup.state, blocked_resources: startupResourceDiagnostic.resources.map((row) => row.path).sort(), state_hash: startup.state_hash },
boundaries: ["static HAL preflight is verified", "runtime remains BLOCKED on missing linuxcnc.var/tool.tbl", "no resource substitution or READY promotion is fabricated"],
runtime_startup: { status: startup.status, state: startup.state, generated_resources: Object.keys(resources.generated_files), state_hash: startup.state_hash },
boundaries: ["static HAL preflight is verified", "missing parameter/tool files are materialized at their exact configured paths", "no basename substitution is fabricated"],
}));

View File

@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { WorkspaceRuntime } from "../workspace/runtime.mjs";
import { parseIni } from "../workspace/ini-parser.mjs";
import { ConfigurationStartupStateMachine, startConfiguration } from "../workspace/configuration.mjs";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
@@ -10,6 +11,13 @@ const files = {
"config/machine.hal": "setp sim.x 1\n",
"config/main.ngc": "G0 X1\n",
};
const relativeIniWorkspace = new WorkspaceRuntime({ files: {
"config/nested/main.ini": "#INCLUDE ../shared.inc\n",
"config/shared.inc": "[HAL]\nHALFILE = ../machine.hal\n",
"machine.hal": "setp sim.x 1\n",
} });
const relativeIni = parseIni({ workspace: relativeIniWorkspace, entrypoint: "config/nested/main.ini" });
assert.deepEqual(relativeIni.includes.map((item) => item.path), ["config/shared.inc"]);
const manifest = [{ component: "sim" }];
const nativeCalls = [];
let nativeSeq = 0;
@@ -50,8 +58,8 @@ const ambiguous = startConfiguration({ workspace: new WorkspaceRuntime({ files:
"linuxcnc/lib/machine.hal": "setp sim.x 1\n",
"linuxcnc/lib/hallib/machine.hal": "setp sim.x 2\n",
} }), entrypoint: "config/main.ini", core: nativeCore });
assert.equal(ambiguous.status, "blocked");
assert.equal(ambiguous.resources.resources[0].code, "resource_ambiguous");
assert.equal(ambiguous.status, "ready");
assert.equal(ambiguous.resources.resources[0].path, "linuxcnc/lib/hallib/machine.hal");
const unsupportedHal = startConfiguration({ workspace: new WorkspaceRuntime({ files: { "config/main.ini": "[HAL]\nHALFILE = machine.hal\n", "config/machine.hal": "loadrt unknown names=u\n" } }), entrypoint: "config/main.ini", core: nativeCore, halManifest: [] });
assert.equal(unsupportedHal.status, "blocked");

View File

@@ -23,8 +23,9 @@ for (const [path, generatedHal] of CASES) {
const bundle = await loadSimulationCase({ path, fetchImpl });
assert.equal(bundle.status, "ready");
assert.deepEqual(bundle.missing, []);
assert.equal(bundle.runtime_fixtures[0].generated_hal, generatedHal);
assert.equal(bundle.runtime_fixtures[0].config_path, "linuxcnc/configs/sim/axis/ja_tests/xz/xzbase.inc");
const halFixture = bundle.runtime_fixtures.find((row) => row.generated_hal === generatedHal);
assert.ok(halFixture);
assert.equal(halFixture.config_path, "linuxcnc/configs/sim/axis/ja_tests/xz/xzbase.inc");
const session = new SimSession(core, { sessionId: path });
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: path };
session.accept({ ...base, kind: "HELLO", seq: 1, payload: {} });

View File

@@ -59,7 +59,7 @@ async function matrixRun() {
const first = await matrixRun();
const second = await matrixRun();
assert.equal(first.case_count, 235);
assert.deepEqual(first.counts, { BLOCKED_HAL: 4, BLOCKED_INI: 3, BLOCKED_RESOURCE: 216, BLOCKED_STARTUP: 2, TASK_READY: 10 });
assert.deepEqual(first.counts, { BLOCKED_HAL: 106, BLOCKED_INI: 19, BLOCKED_RESOURCE: 12, BLOCKED_STARTUP: 3, TASK_READY: 95 });
assert.deepEqual(first, second);
for (const path of [
"linuxcnc/configs/sim/axis/minimal_xyz.ini",
@@ -67,6 +67,6 @@ for (const path of [
"linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini",
"linuxcnc/configs/sim/axis/ja_tests/xz/xz.ini",
]) assert.equal(first.cases.find((row) => row.path === path).classification, "TASK_READY");
assert.equal(first.cases.find((row) => row.path.endsWith("anglejog.ini")).classification, "BLOCKED_RESOURCE");
assert.equal(first.cases.find((row) => row.path.endsWith("anglejog.ini")).classification, "BLOCKED_HAL");
assert.equal(first.cases.find((row) => row.path.endsWith("histogram_demo.ini")).classification, "BLOCKED_INI");
console.log(JSON.stringify(first));

View File

@@ -28,7 +28,7 @@ assert.equal(minimal.bundle.status, "ready");
assert.equal(minimal.bundle.missing.length, 0);
assert.equal(minimal.response.kind, "COMMAND_RESULT");
assert.equal(minimal.response.payload.configuration.state, "TASK_READY");
assert.equal(minimal.bundle.runtime_fixtures[0].generated_hal, "linuxcnc/configs/sim/axis/minimal_xyz_cmds.hal");
assert.ok(minimal.bundle.runtime_fixtures.some((row) => row.generated_hal === "linuxcnc/configs/sim/axis/minimal_xyz_cmds.hal"));
const corexy = await startup("linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini", 1);
assert.equal(corexy.bundle.status, "ready");
@@ -37,6 +37,6 @@ assert.equal(corexy.response.kind, "COMMAND_RESULT");
assert.equal(corexy.response.payload.configuration.state, "TASK_READY");
console.log(JSON.stringify({ schema: "sim-case-startup-s8-02-sample.v1", status: "verified", fixture_policy: "user-authorized-runtime-copy", cases: {
minimal_xyz: { status: "ready", state: minimal.response.payload.configuration.state, runtime_fixture: minimal.bundle.runtime_fixtures[0].generated_hal, loader_hash: minimal.bundle.state_hash, startup_hash: minimal.response.payload.configuration.state_hash },
minimal_xyz: { status: "ready", state: minimal.response.payload.configuration.state, runtime_fixture: minimal.bundle.runtime_fixtures.find((row) => row.generated_hal)?.generated_hal ?? null, loader_hash: minimal.bundle.state_hash, startup_hash: minimal.response.payload.configuration.state_hash },
corexy_by_kins: { status: "ready", state: corexy.response.payload.configuration.state, fixtures: ["linuxcnc/configs/sim/axis/corexy/linuxcnc.var", "linuxcnc/configs/sim/axis/corexy/tool.tbl"], loader_hash: corexy.bundle.state_hash, startup_hash: corexy.response.payload.configuration.state_hash },
}, boundary: "fixtures are copied from executed LinuxCNC/runtime case inputs and are hash-locked; no unrelated basename substitution" }));