Align AXIS Web control lifecycle with LinuxCNC

This commit is contained in:
2026-09-07 13:47:10 +08:00
parent 35adac6471
commit 0a679b5eeb
34 changed files with 2598 additions and 177 deletions

View File

@@ -0,0 +1,86 @@
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 { fnv1a64 } from "./replay.mjs";
import { CONTEXT_ID, SimSession } from "./sim-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 programs = [
{ name: "program-a.ngc", source: "G21\nG90\nG0 X0 Y0 Z10 A0 C0\nG1 X10 Y0 Z10 A0 C0 F600\nM2\n", expected: { x: 10, y: 0, z: 10 } },
{ name: "program-b.ngc", source: "G21\nG91\nG1 X5 F600\nG1 Y4 F600\nM2\n", delta: { x: 5, y: 4, z: 0 } },
{ name: "program-c.ngc", source: "G21\nG91\nG1 X-3 Y-2 Z2 F600\nM2\n", delta: { x: -3, y: -2, z: 2 } },
];
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 core = await createModule({ wasmBinary });
const session = new SimSession(core, { sessionId: "xyzac-multi-program-load" });
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId };
let seq = 0;
const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: `${kind.toLowerCase()}-${seq}`, payload });
const close = (actual, expected, label) => assert.ok(Math.abs(Number(actual) - Number(expected)) <= 1e-7, `${label}: ${actual} != ${expected}`);
const runToCompletion = () => {
const started = send("PROGRAM_RUN");
assert.equal(started.kind, "COMMAND_RESULT", JSON.stringify(started));
let response = started;
for (let index = 0; index < 2000; index += 1) {
response = send("SIM_STEP", { cycles: 20, response: "motion_delta" });
assert.equal(response.kind, "COMMAND_RESULT", JSON.stringify(response));
if (response.payload.program?.program_status === "completed" && Number(response.payload.wasm_motion?.state?.queue_depth ?? -1) === 0) return response;
}
throw new Error(`program_completion_timeout:${JSON.stringify(response.payload?.program)}`);
};
send("HELLO");
send("SESSION_ATTACH");
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.configuration.state, "TASK_READY");
assert.equal(send("POWER", { active: true }).payload.status, "verified");
assert.equal(send("HOME", { all: true }).payload.status, "verified");
const evidence = [];
let expectedPose = { x: 0, y: 0, z: 0 };
for (const program of programs) {
const before = session.wasmMotion.read().state.position;
close(before.x, expectedPose.x, `${program.name}:before.x`);
close(before.y, expectedPose.y, `${program.name}:before.y`);
close(before.z, expectedPose.z, `${program.name}:before.z`);
const loaded = send("PROGRAM_LOAD", { source: program.source, path: `browser/${program.name}` });
assert.equal(loaded.kind, "COMMAND_RESULT", JSON.stringify(loaded));
assert.equal(loaded.payload.program.source_hash, fnv1a64(program.source));
assert.equal(loaded.payload.program.current_line, 0);
const firstMotion = loaded.payload.preview.segments[0];
close(firstMotion.start.x, before.x, `${program.name}:preview.start.x`);
close(firstMotion.start.y, before.y, `${program.name}:preview.start.y`);
close(firstMotion.start.z, before.z, `${program.name}:preview.start.z`);
const completed = runToCompletion();
expectedPose = program.expected ?? {
x: before.x + program.delta.x,
y: before.y + program.delta.y,
z: before.z + program.delta.z,
};
close(completed.payload.wasm_motion.state.position.x, expectedPose.x, `${program.name}:final.x`);
close(completed.payload.wasm_motion.state.position.y, expectedPose.y, `${program.name}:final.y`);
close(completed.payload.wasm_motion.state.position.z, expectedPose.z, `${program.name}:final.z`);
evidence.push({ name: program.name, source_hash: loaded.payload.program.source_hash, preview_segments: loaded.payload.preview.segments.length, synchronized_start: { x: firstMotion.start.x, y: firstMotion.start.y, z: firstMotion.start.z }, final: expectedPose });
}
const beforeRoundTrip = session.wasmMotion.read().state.position;
const roundTrip = send("PROGRAM_LOAD", { source: programs[0].source, path: `browser/${programs[0].name}` });
assert.equal(roundTrip.payload.program.source_hash, evidence[0].source_hash);
assert.notEqual(roundTrip.payload.program.source_hash, evidence[1].source_hash);
assert.notEqual(roundTrip.payload.program.source_hash, evidence[2].source_hash);
close(roundTrip.payload.preview.segments[0].start.x, beforeRoundTrip.x, "roundtrip:start.x");
close(roundTrip.payload.preview.segments[0].start.y, beforeRoundTrip.y, "roundtrip:start.y");
close(roundTrip.payload.preview.segments[0].start.z, beforeRoundTrip.z, "roundtrip:start.z");
console.log(JSON.stringify({ schema: "xyzac-multi-program-load.v1", status: "verified", source: "axis.py:open_file_guts/task_plan_synch", programs: evidence, round_trip_hash: roundTrip.payload.program.source_hash }));