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/nc_files/3dtest.ngc", "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"); assert.ok(bundle.files["linuxcnc/configs/nc_files/3dtest.ngc"].includes("start xy circle")); 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));