Document source UI gates, explicit native live-plot clearing, and remaining parity boundaries. Fix pause/resume transport continuity, repeated stepping, coordinate previews, and spindle override state. Optimize large program snapshots and rendering, and cover the control flows with native Tcl, WASM, and browser checks. Validation: 22 native tests and source/browser checks pass. Worker/App regression: 113 of 123 pass; 10 pre-existing failures remain documented with no new failures.
93 lines
6.4 KiB
JavaScript
93 lines
6.4 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 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 }));
|