148 lines
12 KiB
JavaScript
148 lines
12 KiB
JavaScript
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 PROGRAM = "G21 G90\nG0 X0 Y0 Z10 A0 C0\nG1 X10 Y5 Z5 A30 C45 F100\nG1 X20 Y10 Z2 A35 C50 F100\nM2\n";
|
|
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 wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
|
|
const rows = [];
|
|
const record = (group, button, command, source, response, expected = "verified") => {
|
|
const actual = response?.kind === "ERROR" || response?.payload?.status === "error" || response?.payload?.status === "blocked" ? "error" : "verified";
|
|
assert.equal(actual, expected, `${group}/${button}: ${JSON.stringify(response?.payload ?? response)}`);
|
|
rows.push({ group, button, command, source, result: actual, reason: actual === "error" ? response.payload?.message_key ?? response.payload?.reason ?? response.payload?.code ?? null : null });
|
|
return response;
|
|
};
|
|
|
|
async function open(sessionId) {
|
|
const core = await createModule({ wasmBinary });
|
|
const session = new XyzacSession(core, { sessionId });
|
|
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: sessionId };
|
|
let seq = 0;
|
|
const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: `${sessionId}-${seq}`, payload });
|
|
record("session", "HELLO", "HELLO", "axis.py/AXIS startup", send("HELLO"));
|
|
record("session", "SESSION_ATTACH", "SESSION_ATTACH", "axis.py/AXIS startup", send("SESSION_ATTACH"));
|
|
record("machine", "Load case", "LOAD_WORKSPACE", "axis.py:configuration/startup", send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }));
|
|
return { session, send };
|
|
}
|
|
|
|
const machine = await open("xyzac-panel-buttons-machine");
|
|
const off = machine.send("PROGRAM_LOAD", { source: PROGRAM, path: "panel-buttons.ngc" });
|
|
record("program", "Load", "PROGRAM_LOAD", "axis.py:open_file_guts/send_mdi_command", off);
|
|
for (const [button, command, payload, reason] of [
|
|
["Run while OFF", "PROGRAM_RUN", {}, "xyzac_machine_off"],
|
|
["HOME ALL while OFF", "HOME", { all: true }, "xyzac_machine_off"],
|
|
["JOG while OFF", "JOG_START", { axis: "x", direction: 1, speed: 1 }, "xyzac_machine_off"],
|
|
["MDI while OFF", "MDI_EXECUTE", { block: "G0 X1" }, "xyzac_machine_off"],
|
|
["M428 while OFF", "KINEMATICS_SELECT", { type: 1 }, "xyzac_machine_off"],
|
|
]) {
|
|
const response = machine.send(command, payload);
|
|
record("gates", button, command, "sim_task_command_gate / axis.py manual_ok", response, "error");
|
|
assert.equal(response.payload.gate_reason, reason, button);
|
|
}
|
|
record("machine", "Power on", "POWER", "axis.py:commands.power / emcTaskSetState", machine.send("POWER", { active: true }));
|
|
record("machine", "HOME ALL", "HOME", "axis.py:home_all_joints", machine.send("HOME", { all: true }));
|
|
record("machine", "Reset", "RESET", "axis.py:commands.reset", machine.send("RESET"));
|
|
for (const [button, type] of [["Identity", 0], ["TCP: XYZAC", 1], ["User K", 2], ["TCP: XYZAC", 1]]) {
|
|
const response = machine.send("KINEMATICS_SELECT", { type });
|
|
record("switchkins", button, "KINEMATICS_SELECT", "switchkins_postgui.hal / remap 428/429/430", response);
|
|
assert.equal(response.payload.kinematics.active_type, type);
|
|
}
|
|
record("view", "Clear plot", "VISUAL_CLEAR", "axis.py:clear_live_plot / glcanon.py", machine.send("VISUAL_CLEAR"));
|
|
record("override", "Feed 50%", "SET_OVERRIDE", "axis.py:set_feedrate -> c.feedrate", machine.send("SET_OVERRIDE", { feed: 0.5 }));
|
|
record("override", "Jog Speed 50%", "SET_JOG_SCALE", "axis.py:update_jog_slider_vel -> LinuxCNC TP jog scale", machine.send("SET_JOG_SCALE", { percent: 50 }));
|
|
assert.equal(machine.send("SNAPSHOT_REQUEST").payload.panel_snapshot.motion.jog_scale, 0.5);
|
|
record("override", "Max Velocity 50%", "SET_MAX_VELOCITY", "axis.py:update_maxvel_slider_vel -> tpSetVlimit", machine.send("SET_MAX_VELOCITY", { percent: 50 }));
|
|
assert.equal(machine.send("SNAPSHOT_REQUEST").payload.panel_snapshot.motion.velocity_scale, 0.5);
|
|
const invalidOverride = machine.send("SET_OVERRIDE", { feed: 2.01 });
|
|
record("override", "Feed 201% rejected", "SET_OVERRIDE", "axis.py:set_feedrate range", invalidOverride, "error");
|
|
assert.equal(invalidOverride.payload.message_key, "motion_override_range");
|
|
|
|
record("program", "Run", "PROGRAM_RUN", "axis.py:task_run -> c.auto(AUTO_RUN)", machine.send("PROGRAM_RUN"));
|
|
const step = machine.send("SIM_STEP", { cycles: 3 });
|
|
record("program", "Servo/task step", "SIM_STEP", "axis.py:LivePlotter/update + LinuxCNC TP", step);
|
|
record("program", "Pause", "PROGRAM_PAUSE", "axis.py:task_pause -> c.auto(AUTO_PAUSE)", machine.send("PROGRAM_PAUSE"));
|
|
const pausedSnapshot = machine.send("SNAPSHOT_REQUEST");
|
|
record("program", "Paused snapshot", "SNAPSHOT_REQUEST", "axis.py:stat.poll task_paused", pausedSnapshot);
|
|
assert.equal(pausedSnapshot.payload.task.state, "PAUSED");
|
|
record("program", "Resume", "PROGRAM_RESUME", "axis.py:task_resume -> c.auto(AUTO_RESUME)", machine.send("PROGRAM_RESUME"));
|
|
record("program", "Abort/Stop", "PROGRAM_ABORT", "axis.py:task_stop -> c.abort", machine.send("PROGRAM_ABORT"));
|
|
assert.equal(machine.session.wasmMotion.read().state.queue_depth, 0);
|
|
record("program", "Restart", "PROGRAM_RESTART", "axis.py:reload_file/set_first_line", machine.send("PROGRAM_RESTART"));
|
|
record("program", "Step", "PROGRAM_STEP", "axis.py:task_step -> c.auto(AUTO_STEP)", machine.send("PROGRAM_STEP"));
|
|
record("program", "Seek", "PROGRAM_SEEK", "axis.py:set_first_line/task_run_line", machine.send("PROGRAM_SEEK", { line: 3 }));
|
|
record("program", "Resume from line", "PROGRAM_RESUME", "axis.py:task_resume", machine.send("PROGRAM_RESUME"));
|
|
record("program", "Abort after seek", "PROGRAM_ABORT", "axis.py:task_stop", machine.send("PROGRAM_ABORT"));
|
|
|
|
const mdi = await open("xyzac-panel-buttons-mdi");
|
|
record("machine", "Power on", "POWER", "axis.py:commands.power", mdi.send("POWER", { active: true }));
|
|
record("machine", "HOME ALL", "HOME", "axis.py:home_all_joints", mdi.send("HOME", { all: true }));
|
|
record("mdi", "Go", "MDI_EXECUTE", "axis.py:send_mdi_command", mdi.send("MDI_EXECUTE", { block: "G21 G90 G1 X10 Y5 Z5 A30 C45 F100" }));
|
|
record("mdi", "Invalid G-code", "MDI_EXECUTE", "axis.py:send_mdi_command/error channel", mdi.send("MDI_EXECUTE", { block: "G999" }), "error");
|
|
record("mdi", "Load program", "PROGRAM_LOAD", "axis.py:open_file_guts", mdi.send("PROGRAM_LOAD", { source: PROGRAM, path: "mdi-panel.ngc" }));
|
|
record("mdi", "Run", "PROGRAM_RUN", "axis.py:task_run", mdi.send("PROGRAM_RUN"));
|
|
record("mdi", "MDI while running", "MDI_EXECUTE", "axis.py:manual_ok", mdi.send("MDI_EXECUTE", { block: "G0 X1" }), "error");
|
|
assert.equal(mdi.send("MDI_EXECUTE", { block: "G0 X1" }).payload.message_key, "mdi_while_program_active");
|
|
record("mdi", "Stop", "PROGRAM_ABORT", "axis.py:task_stop", mdi.send("PROGRAM_ABORT"));
|
|
|
|
for (const axis of ["x", "y", "z", "a", "c"]) {
|
|
const jog = await open(`xyzac-panel-buttons-jog-${axis}`);
|
|
record("machine", "Power on", "POWER", "axis.py:commands.power", jog.send("POWER", { active: true }));
|
|
record("machine", "HOME ALL", "HOME", "axis.py:home_all_joints", jog.send("HOME", { all: true }));
|
|
const start = jog.session.wasmMotion.read().state.position[axis] ?? 0;
|
|
record("jog", `JOG ${axis.toUpperCase()} +`, "JOG_START", "axis.py:jog_plus/jog_on", jog.send("JOG_START", { axis, direction: 1, speed: 1, increment: axis === "a" || axis === "c" ? 2 : 1 }));
|
|
record("jog", `JOG ${axis.toUpperCase()} sample`, "SIM_STEP", "axis.py:LivePlotter/update", jog.send("SIM_STEP", { cycles: 1 }));
|
|
const end = jog.session.wasmMotion.read().state.position[axis] ?? 0;
|
|
assert.notEqual(end, start, axis);
|
|
record("jog", `JOG ${axis.toUpperCase()} stop`, "JOG_STOP", "axis.py:jog_stop -> JOG_STOP", jog.send("JOG_STOP"));
|
|
}
|
|
|
|
const io = await open("xyzac-panel-buttons-io");
|
|
record("machine", "Power on", "POWER", "axis.py:commands.power", io.send("POWER", { active: true }));
|
|
record("machine", "HOME ALL", "HOME", "axis.py:home_all_joints", io.send("HOME", { all: true }));
|
|
record("spindle", "CW", "SPINDLE_COMMAND", "axis.py:spindle_forward_toggle/c.spindle", io.send("SPINDLE_COMMAND", { spindle: 0, rpm: 600, enabled: true }));
|
|
assert.equal(io.send("SPINDLE_COMMAND", { spindle: 0, delta: 100 }).payload.device_io.state.spindles[0].command_rpm, 700);
|
|
rows.push({ group: "spindle", button: "Speed +", command: "SPINDLE_COMMAND(delta)", source: "axis.py:spindle_increase -> EMCMOT_SPINDLE_INCREASE", result: "verified", reason: null });
|
|
assert.equal(io.send("SPINDLE_COMMAND", { spindle: 0, delta: -100 }).payload.device_io.state.spindles[0].command_rpm, 600);
|
|
rows.push({ group: "spindle", button: "Speed -", command: "SPINDLE_COMMAND(delta)", source: "axis.py:spindle_decrease -> EMCMOT_SPINDLE_DECREASE", result: "verified", reason: null });
|
|
record("spindle", "Stop", "SPINDLE_COMMAND", "axis.py:spindle -> SPINDLE_OFF", io.send("SPINDLE_COMMAND", { spindle: 0, rpm: 0, enabled: false }));
|
|
record("spindle", "Override 50%", "SET_SPINDLE_OVERRIDE", "axis.py:set_spindlerate -> EMC_TRAJ_SET_SPINDLE_SCALE", io.send("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: 0.5 }));
|
|
assert.equal(io.send("SNAPSHOT_REQUEST").payload.panel_snapshot.io.state.spindles[0].override, 0.5);
|
|
record("spindle", "Brake engage", "SPINDLE_BRAKE", "axis.py:brake -> EMC_SPINDLE_BRAKE_ENGAGE", io.send("SPINDLE_BRAKE", { spindle: 0, engaged: true }));
|
|
record("spindle", "Brake release", "SPINDLE_BRAKE", "axis.py:brake -> EMC_SPINDLE_BRAKE_RELEASE", io.send("SPINDLE_BRAKE", { spindle: 0, engaged: false }));
|
|
record("coolant", "Mist/Flood on", "COOLANT_COMMAND", "axis.py:mist_toggle/flood_toggle", io.send("COOLANT_COMMAND", { mist: true, flood: true }));
|
|
record("tool", "Prepare T1", "TOOL_PREPARE", "axis.py:change_tool / iocontrol", io.send("TOOL_PREPARE", { tool: 1, pocket: 1 }));
|
|
record("tool", "Prepared ack", "TOOL_PREPARED", "axis.py:tool-prepared HAL", io.send("TOOL_PREPARED", { prepared: true }));
|
|
record("tool", "Change", "TOOL_CHANGE", "axis.py:change_tool -> c.mdi(M6)", io.send("TOOL_CHANGE"));
|
|
record("tool", "Changed ack", "TOOL_CHANGED", "axis.py:tool-changed HAL", io.send("TOOL_CHANGED", { changed: true }));
|
|
record("probe", "Arm", "PROBE_CONFIG", "axis.py:probe/canonical probe", io.send("PROBE_CONFIG", { probe_type: 0 }));
|
|
record("probe", "Disarm", "PROBE_CONFIG", "axis.py:probe disarm", io.send("PROBE_CONFIG", { action: "disarm" }));
|
|
assert.equal(io.session.xyzacIo.canonical().probe.state, 0);
|
|
record("machine", "ESTOP", "ESTOP", "axis.py:General_Halt/emcTaskAbort", io.send("ESTOP", { active: true }));
|
|
record("machine", "Release E-stop", "ESTOP", "axis.py:ESTOP reset", io.send("ESTOP", { active: false }));
|
|
record("machine", "Power after ESTOP", "POWER", "axis.py:commands.power", io.send("POWER", { active: true }));
|
|
|
|
const unsupported = [
|
|
{ button: "Override Limits", reason: "selected XYZAC WASM configuration owns limits; no LinuxCNC override_limits command ABI" },
|
|
{ button: "Block delete / Optional pause", reason: "UI toggle is not forwarded to sim_program interpreter state" },
|
|
{ button: "Tool Touch Off", reason: "UI emits fixed G43 and does not expose LinuxCNC G10 prompt/value flow" },
|
|
{ button: "Edit tool data / Quit", reason: "desktop process boundary; browser intentionally host-excluded" },
|
|
];
|
|
for (const item of unsupported) rows.push({ group: "scope", button: item.button, command: null, source: "axis.py source mapped", result: "blocked_by_abi", reason: item.reason });
|
|
|
|
const counts = Object.fromEntries(["verified", "error", "blocked_by_abi"].map((status) => [status, rows.filter((row) => row.result === status).length]));
|
|
assert.ok(counts.verified > 0 && counts.error > 0 && counts.blocked_by_abi > 0);
|
|
console.log(JSON.stringify({ schema: "xyzac-panel-button-parity.v1", status: "verified", case: CASE, source: "linuxcnc-wasm-worker", counts, rows, selected_case_release_status: "STOP_RELEASE" }));
|