import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { createHash } from "node:crypto"; 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 path = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"; const bundle = await loadSimulationCase({ path, fetchImpl: async (url) => { try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url), "utf8"); return { ok: true, text: async () => body }; } catch { return { ok: false, text: async () => "" }; } } }); const binary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)); for (const [file, hash] of [["homing.c", "ee73d38f2feb8d5523b9a733cb17d635d3904667aaa219f8a02d7fbffee1ba39"], ["simple_tp.c", "5a0026f4d5bdd9d1bc55290c6ea9c399abd664633b69413c307f7353953d2603"]]) { assert.equal(createHash("sha256").update(await readFile(new URL(`../../linuxcnc/src/emc/motion/${file}`, import.meta.url))).digest("hex"), hash, `source drift: ${file}`); } async function setup(id, finalHome = 0) { const core = await createModule({ wasmBinary: binary }); const session = new XyzacSession(core, { sessionId: id }); let seq = 0; const send = (kind, payload = {}) => session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: id, seq: ++seq, operation_id: `${id}-${seq}`, kind, payload }); const ok = (kind, payload) => { const r = send(kind, payload); assert.notEqual(r.kind, "ERROR", `${kind}: ${r.payload.message_key}`); return r.payload; }; ok("HELLO"); ok("SESSION_ATTACH"); const configuration = toWorkerConfiguration(bundle); if (finalHome) configuration.files[path] = configuration.files[path].replace(/HOME =\s+0/g, `HOME = ${finalHome}`); ok("LOAD_WORKSPACE", { root_path: "/workspace", configuration }); ok("PROGRAM_LOAD", { source: "G21 G90\nG1 X10 F600\nM2\n", path: "native-home.ngc" }); ok("POWER", { active: true }); const snapshot = () => ok("SNAPSHOT_REQUEST").panel_snapshot; const drainHome = () => { let ticks = 0; while (session.wasmHomeLimit.active && ticks++ < 1000) ok("SIM_STEP", { cycles: 1, response: "motion_delta" }); assert.equal(session.wasmHomeLimit.active, false); return snapshot(); }; const jog = (axis, increment) => { ok("JOG_START", { axis, direction: 1, increment }); let ticks = 0; while (session.wasmMotion.read().state.queue_depth > 0 && ticks++ < 5000) ok("SIM_STEP", { cycles: 1, response: "motion_delta" }); assert.equal(session.wasmMotion.read().state.queue_depth, 0); }; return { session, send, ok, snapshot, drainHome, jog }; } const s = await setup("native-homing"); assert.equal(s.snapshot().source_status.traj.mode, "FREE"); assert.equal(s.send("HOME", { axis: "x" }).payload.message_key, "home_joint_requires_joint_mode"); for (const joint of [0, 1, 2, 3, 4]) { const ack = s.ok("HOME", { joint }); assert.equal(ack.homing.phase, "accepted"); assert.equal(ack.panel_snapshot.source_status.homing_active, true); assert.equal(ack.panel_snapshot.source_status.task.interpState, "INTERP_IDLE"); assert.equal(ack.panel_snapshot.axis_state.operation_state, "HOME"); assert.equal(ack.panel_snapshot.motion.homed_axes[joint], false); const done = s.drainHome(); assert.equal(done.motion.homed_axes[joint], true); assert.equal(done.source_status.traj.mode, joint === 4 ? "TELEOP" : "FREE"); assert.ok(done.source_status.homing_history.some((e) => e.joint[joint].home_state === 21 && e.joint[joint].homing)); } for (const [joint, axis] of ["x", "y", "z", "a", "c"].entries()) { s.jog(axis, 3); const before = s.snapshot(); s.ok("HOME", { joint }); const after = s.drainHome(); after.source_status.joint.forEach((j, index) => { assert.ok(Math.abs(j.motor_position - before.source_status.joint[index].motor_position) < 1e-8, "no-search home does not move a motor"); assert.ok(Math.abs(j.position - (index === joint ? 0 : before.source_status.joint[index].position)) < 1e-8); }); assert.equal(after.visual.live_path_epoch, before.visual.live_path_epoch); } s.jog("x", 4); s.jog("y", 2); const beforeAll = s.snapshot(); s.ok("HOME", { all: true }); for (const [kind, payload] of [["PROGRAM_RUN", {}], ["MDI_EXECUTE", { block: "G0 X1" }], ["SET_MODE", { mode: "AUTO" }]]) assert.equal(s.send(kind, payload).kind, "ERROR"); const afterAll = s.drainHome(); assert.deepEqual(afterAll.source_status.joint.map((j) => j.position), [0, 0, 0, 0, 0]); assert.deepEqual(afterAll.source_status.joint.map((j) => j.motor_position), beforeAll.source_status.joint.map((j) => j.motor_position)); const nativeCheckpoint = s.session.wasmHomeLimit.exportState(); s.session.wasmHomeLimit.reset(); s.session.wasmHomeLimit.restoreState(nativeCheckpoint); assert.deepEqual(s.session.wasmHomeLimit.read().state.joint, afterAll.source_status.joint); s.ok("PROGRAM_RUN"); assert.equal(s.snapshot().source_status.task.mode, "AUTO"); assert.equal(s.snapshot().source_status.traj.mode, "COORD"); s.ok("PROGRAM_PAUSE"); assert.equal(s.snapshot().source_status.task.interpState, "INTERP_PAUSED"); assert.equal(s.send("SET_MODE", { mode: "MANUAL" }).kind, "ERROR"); s.ok("PROGRAM_ABORT"); s.ok("SET_MODE", { mode: "MANUAL" }); assert.equal(s.snapshot().source_status.traj.mode, "TELEOP"); s.ok("HOME", { joint: 0 }); s.ok("ESTOP", { active: true }); s.ok("SIM_STEP", { cycles: 1 }); assert.equal(s.snapshot().source_status.homing_active, false); assert.equal(s.snapshot().source_status.joint[0].homed, false); s.ok("ESTOP", { active: false }); s.ok("POWER", { active: true }); s.ok("HOME", { joint: 0 }); s.drainHome(); // A configured final move uses the original joint free_tp, not coordinated TP. const moving = await setup("native-final-move", 5); moving.ok("HOME", { joint: 0 }); const mid = moving.ok("SIM_STEP", { cycles: 1 }).panel_snapshot; assert.equal(mid.source_status.joint[0].home_state, 21); assert.ok(mid.source_status.joint[0].position > 0 && mid.source_status.joint[0].position < 5); assert.equal(mid.wasm_motion.state.queue_depth, 0); assert.equal(mid.source_status.joint[0].homed, false); const done = moving.drainHome(); assert.ok(Math.abs(done.source_status.joint[0].position - 5) < 1e-8); assert.ok(Math.abs(done.source_status.joint[0].motor_position - 5) < 1e-8); moving.ok("HOME", { joint: 1 }); moving.ok("SIM_STEP", { cycles: 1 }); moving.ok("PROGRAM_ABORT"); moving.drainHome(); assert.equal(moving.snapshot().source_status.joint[1].homed, false); assert.equal(moving.snapshot().source_status.homing_active, false); console.log(JSON.stringify({ status: "verified", source: "homing.c + simple_tp.c", checks: ["five-joint-home", "no-search-coordinate-rebase", "motor-position-preserved", "native-final-move", "mode-interlocks", "abort-and-recovery", "servo-transition-history"] }));