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 { XyzacSession, CONTEXT_ID } from "./xyzac-session.mjs"; const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.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)) }); function setup(id, { homeAll = true } = {}) { const session = new XyzacSession(core, { sessionId: id }); const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: id }; let seq = 0; const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload }); send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); if (homeAll) assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true); return { session, send }; } const partialHome = setup("xyzac-manual-partial-home", { homeAll: false }); for (const [index, axis] of ["x", "y", "z", "a", "c"].entries()) { const response = partialHome.send("HOME", `home-${axis}`, { joint: index }); assert.equal(response.payload.wasm_home_limit.state.homed_mask, (1 << (index + 1)) - 1); assert.equal(response.payload.wasm_home_limit.state.homed, index === 4); } const jogCase = setup("xyzac-manual-jog"); const home = jogCase.send("HOME", "home-all"); assert.equal(home.payload.motion_authority, "wasm"); assert.equal(home.payload.wasm_home_limit.state.homed, true); assert.equal(home.payload.wasm_home_limit.state.joint_count, 5); const axes = ["x", "y", "z", "a", "c"]; const jogRows = []; for (const axis of axes) { const axisCase = axis === "x" ? jogCase : setup(`xyzac-manual-jog-${axis}`); if (axis !== "x") assert.equal(axisCase.send("HOME", `home-${axis}`).payload.wasm_home_limit.state.homed, true); const start = axisCase.session.wasmMotion.read().state.position[axis] ?? 0; const started = axisCase.send("JOG_START", `jog-${axis}`, { axis, direction: 1, speed: 1, increment: axis === "a" || axis === "c" ? 2 : 1 }); assert.equal(started.kind, "COMMAND_RESULT", axis); assert.equal(started.payload.motion_authority, "wasm"); const stepped = axisCase.send("SIM_STEP", `jog-step-${axis}`, { cycles: 1 }); assert.equal(stepped.payload.motion_authority, "wasm"); const end = axisCase.session.wasmMotion.read().state.position[axis] ?? axisCase.session.wasmMotion.read().state.position.x; assert.notEqual(end, start, axis); assert.equal(axisCase.send("JOG_STOP", `jog-stop-${axis}`).payload.motion_authority, "wasm"); jogRows.push({ axis, start, end }); } const outOfRange = jogCase.send("JOG_START", "jog-limit", { axis: "x", direction: 1, speed: 1, increment: 10000 }); assert.equal(outOfRange.kind, "ERROR"); assert.equal(outOfRange.payload.message_key, "motion_limit_range"); const nonFinite = jogCase.send("JOG_START", "jog-nonfinite", { axis: "a", direction: 1, increment: "not-a-number" }); assert.equal(nonFinite.kind, "ERROR"); assert.equal(nonFinite.payload.message_key, "motion_jog_nonfinite"); const ferror = jogCase.send("FOLLOWING_ERROR", "ferror", { value: 1 }); assert.equal(ferror.kind, "ERROR"); assert.equal(ferror.payload.code, "motion_following_error"); const mdiCase = setup("xyzac-manual-mdi"); // The selected INI limits Z to [-120, 120]; use an in-range completion case. const mdi = mdiCase.send("MDI_EXECUTE", "mdi-xyzac", { block: "G21 G90 G1 X10 Y5 Z18 A30 C45 F600" }); assert.equal(mdi.kind, "COMMAND_RESULT"); assert.equal(mdi.payload.motion_authority, "wasm"); assert.equal(mdi.payload.program.source, "linuxcnc-wasm-sim_program"); assert.equal(mdi.payload.panel_snapshot.kinematics.active_type, 0); const invalidMdi = mdiCase.send("MDI_EXECUTE", "mdi-invalid", { block: "G999" }); assert.equal(invalidMdi.kind, "ERROR"); assert.equal(invalidMdi.payload.gate_reason, "mdi_while_program_active"); for (let tick = 0; tick < 20000 && mdiCase.session.wasmMotion.read().state.queue_depth > 0; tick += 32) { const sample = mdiCase.send("SIM_STEP", `mdi-drain-${tick}`, { cycles: 32, response: "motion_delta" }); assert.equal(sample.kind, "COMMAND_RESULT", sample.payload.message_key); } assert.equal(mdiCase.session.wasmMotion.read().state.queue_depth, 0); const loaded = mdiCase.send("PROGRAM_LOAD", "mdi-program", { source: "G21 G90\nG1 X20 A10 C20 F100\nM2\n", path: "mdi-run.ngc" }); assert.equal(loaded.kind, "COMMAND_RESULT", JSON.stringify({ reason: loaded.payload.gate_reason ?? loaded.payload.message_key, code: loaded.payload.code, preview: loaded.payload.preview, program: loaded.payload.program?.program_status })); mdiCase.send("PROGRAM_RUN", "mdi-run"); const mdiWhileRunning = mdiCase.send("MDI_EXECUTE", "mdi-running", { block: "G0 X1" }); assert.equal(mdiWhileRunning.kind, "ERROR"); assert.equal(mdiWhileRunning.payload.message_key, "mdi_while_program_active"); mdiCase.send("PROGRAM_ABORT", "mdi-abort"); const estop = mdiCase.send("ESTOP", "mdi-estop", { active: true }); assert.equal(estop.kind, "COMMAND_RESULT"); assert.equal(estop.payload.task.state, "ESTOP"); assert.equal(estop.payload.device_io.source, "wasm_io"); mdiCase.send("ESTOP", "mdi-estop-reset", { active: false }); const recovered = mdiCase.send("POWER", "mdi-power-after-estop", { active: true }); assert.equal(recovered.payload.motion_authority, "wasm"); console.log(JSON.stringify({ schema: "xyzac-manual-control-rtcp-06.v1", status: "verified", case: CASE, home: { authority: "wasm_home_limit", joints: 5 }, jog: jogRows, boundary: { out_of_range: outOfRange.payload.code, nonfinite_speed: nonFinite.payload.code, following_error: ferror.payload.code }, mdi: "verified", mdi_running_gate: mdiWhileRunning.payload.message_key, estop_recovery: "verified", state_hash: recovered.payload.panel_snapshot.state_hash }));