Advance simulation parity and 3-axis tutorial coverage
This commit is contained in:
55
web/worker/axis-state.mjs
Normal file
55
web/worker/axis-state.mjs
Normal 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 };
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
108
web/worker/test_axis3_selected_case.mjs
Normal file
108
web/worker/test_axis3_selected_case.mjs
Normal 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));
|
||||
37
web/worker/test_axis3_tutorial_hal_adapter.mjs
Normal file
37
web/worker/test_axis3_tutorial_hal_adapter.mjs
Normal 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 } }));
|
||||
58
web/worker/test_axis3_tutorial_motion.mjs
Normal file
58
web/worker/test_axis3_tutorial_motion.mjs
Normal 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 }));
|
||||
46
web/worker/test_axis3_tutorial_persistence.mjs
Normal file
46
web/worker/test_axis3_tutorial_persistence.mjs
Normal 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 }));
|
||||
51
web/worker/test_axis3_tutorial_program.mjs
Normal file
51
web/worker/test_axis3_tutorial_program.mjs
Normal 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 }));
|
||||
45
web/worker/test_axis3_tutorial_sequence.mjs
Normal file
45
web/worker/test_axis3_tutorial_sequence.mjs
Normal 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 }));
|
||||
32
web/worker/test_axis3_tutorial_startup.mjs
Normal file
32
web/worker/test_axis3_tutorial_startup.mjs
Normal 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 }));
|
||||
57
web/worker/test_axis3_tutorial_tool_io.mjs
Normal file
57
web/worker/test_axis3_tutorial_tool_io.mjs
Normal 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 }));
|
||||
20
web/worker/test_axis_state.mjs
Normal file
20
web/worker/test_axis_state.mjs
Normal 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"] }));
|
||||
@@ -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"],
|
||||
}));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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: {} });
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
Reference in New Issue
Block a user