124 lines
8.2 KiB
JavaScript
124 lines
8.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
import * as THREE from "three";
|
|
import createModule from "../../../build-wasm/linuxcnc-sim-core.js";
|
|
import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs";
|
|
import { writeTrtGeometry } from "../../workspace/trt-geometry.mjs";
|
|
import { MACHINE_DEFINITIONS } from "../../workspace/machine-definitions.mjs";
|
|
import { SimSession, CONTEXT_ID } from "../../worker/sim-session.mjs";
|
|
import { fnv1a64 } from "../../worker/replay.mjs";
|
|
import { buildXyzacMachine, buildXyzbcMachine } from "./xyzac-machine-model.ts";
|
|
|
|
const root = new URL("../../../", import.meta.url);
|
|
const fetchImpl = async (url) => {
|
|
try { const source = await readFile(new URL(String(url).replace(/^\//, ""), root), "utf8"); return { ok: true, text: async () => source }; }
|
|
catch { return { ok: false }; }
|
|
};
|
|
const wasmBinary = await readFile(new URL("build-wasm/linuxcnc-sim-core.wasm", root));
|
|
const close = (actual, expected, message) => assert.ok(Math.abs(actual - expected) < 1e-7, `${message}: ${actual} != ${expected}`);
|
|
const rows = [];
|
|
for (const machine of MACHINE_DEFINITIONS) {
|
|
const raw = await loadSimulationCase({ path: machine.entrypoint, fetchImpl });
|
|
const bundle = writeTrtGeometry(raw, { x_rot_point: 9, y_rot_point: -7, z_rot_point: 13 });
|
|
const core = await createModule({ wasmBinary });
|
|
const session = new SimSession(core, { sessionId: `rtcp-jog-${machine.id}` }); let seq = 0;
|
|
const send = (kind, payload = {}, expectedKind = null) => {
|
|
const result = session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, seq: ++seq, operation_id: `jog-${seq}`, payload });
|
|
if (expectedKind) assert.equal(result.kind, expectedKind);
|
|
else assert.notEqual(result.kind, "ERROR", `${kind}: ${JSON.stringify(result.payload)}`);
|
|
return result;
|
|
};
|
|
send("HELLO"); send("SESSION_ATTACH");
|
|
send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
|
send("POWER", { active: true }); send("HOME", { all: true });
|
|
for (let i = 0; i < 500 && !session.wasmHomeLimit.read().state.homed; i++) send("SIM_STEP", { cycles: 1 });
|
|
assert.equal(session.wasmHomeLimit.read().state.homed, true);
|
|
const drain = () => {
|
|
for (let i = 0; i < 5000 && session.wasmMotion.read().state.queue_depth > 0; i++) send("SIM_STEP", { cycles: 1 });
|
|
assert.equal(session.wasmMotion.read().state.queue_depth, 0);
|
|
};
|
|
send("SET_MODE", { mode: "MDI" });
|
|
send("MDI_EXECUTE", { block: "G21 G90 G1 X12 Y8 Z6 F600" }); drain();
|
|
send("MDI_EXECUTE", { block: "M428" });
|
|
send("SET_MODE", { mode: "MANUAL" });
|
|
const model = machine.coordinates === "XYZAC" ? buildXyzacMachine() : buildXyzbcMachine();
|
|
const start = session.wasmMotion.read().state;
|
|
const target = { ...start.position };
|
|
const samples = [];
|
|
const check = (phase, snapshot = null) => {
|
|
const state = session.wasmMotion.read().state;
|
|
assert.equal(session.xyzacKinematics.activeMode, 1, phase);
|
|
assert.equal(state.active_tag, 1, `${phase}: RTCP motion mode must remain active`);
|
|
for (const axis of ["x", "y", "z"]) close(state.position[axis], target[axis], `${phase}: programmed TCP ${axis}`);
|
|
const joints = state.joint_commanded;
|
|
const offsets = session.xyzacKinematics.snapshot().offsets;
|
|
model.update({ joints, offsets }); model.machine.updateMatrixWorld(true);
|
|
const point = model.workOrigin.localToWorld(new THREE.Vector3(target.x * .01, target.z * .01, target.y * .01));
|
|
const tip = model.toolTip.getWorldPosition(new THREE.Vector3());
|
|
const error = tip.distanceTo(point);
|
|
assert.ok(error < 1e-7, `${phase}: machine model TCP error ${error}`);
|
|
if (snapshot?.visual?.joints) snapshot.visual.joints.forEach((v, i) => close(v, joints[i], `${phase}: visual joint ${i}`));
|
|
if (snapshot?.visual?.motion_trace) for (const sample of snapshot.visual.motion_trace) {
|
|
model.update({ joints: sample.joints, offsets }); model.machine.updateMatrixWorld(true);
|
|
const samplePoint = model.workOrigin.localToWorld(new THREE.Vector3(target.x * .01, target.z * .01, target.y * .01));
|
|
close(model.toolTip.getWorldPosition(new THREE.Vector3()).distanceTo(samplePoint), 0, `${phase}: live motion trace TCP`);
|
|
}
|
|
samples.push({ phase, tick: session.tick, pose: state.position, joints, error });
|
|
return state;
|
|
};
|
|
check("M428");
|
|
for (const axis of [machine.axes[3], "c", machine.axes[3], "c"]) {
|
|
const before = session.wasmMotion.read().state;
|
|
send("JOG_START", { axis, direction: 1, continuous: true, increment: null });
|
|
for (let i = 0; i < 15; i++) {
|
|
const result = send("SIM_STEP", { cycles: 1 });
|
|
check(`${axis}-continuous-${i}`, result.payload.panel_snapshot);
|
|
}
|
|
const moving = session.wasmMotion.read().state;
|
|
assert.ok(moving.position[axis] > before.position[axis]);
|
|
assert.ok(moving.joint_commanded.slice(0, 3).some((v, i) => Math.abs(v - before.joint_commanded[i]) > 1e-5), `${axis}: XYZ compensation missing`);
|
|
const stopped = send("JOG_STOP");
|
|
const after = check(`${axis}-released`, stopped.payload.panel_snapshot);
|
|
after.joint_commanded.forEach((v, i) => close(v, moving.joint_commanded[i], `${axis}: no release jump`));
|
|
send("SIM_STEP", { cycles: 1 }); check(`${axis}-idle`);
|
|
const incrementalStart = session.wasmMotion.read().state.position[axis];
|
|
send("JOG_START", { axis, direction: -1, increment: 1 }); drain();
|
|
close(check(`${axis}-increment`).position[axis], incrementalStart - 1, `${axis}: incremental endpoint`);
|
|
}
|
|
const beforeInvalid = session.wasmMotion.read().state;
|
|
send("JOG_START", { axis: machine.axes[3], direction: 1, increment: 100000 }, "ERROR");
|
|
send("JOG_START", { axis: "c", direction: 1, increment: "invalid" }, "ERROR");
|
|
assert.deepEqual(check("invalid-jog-retains-rtcp").joint_commanded, beforeInvalid.joint_commanded);
|
|
send("JOG_STOP"); check("repeated-stop");
|
|
const beforeAbort = session.wasmMotion.read().state.joint_commanded;
|
|
send("PROGRAM_ABORT");
|
|
assert.deepEqual(check("abort-retains-rtcp").joint_commanded, beforeAbort);
|
|
const poweredOff = send("POWER", { active: false });
|
|
assert.deepEqual(check("power-off-retains-rtcp", poweredOff.payload.panel_snapshot).joint_commanded, beforeAbort);
|
|
send("POWER", { active: true });
|
|
send("KINEMATICS_SELECT", { type: 0 });
|
|
const identityBefore = session.wasmMotion.read().state.joint_commanded;
|
|
send("JOG_START", { axis: machine.axes[3], direction: 1, increment: 2 }); drain();
|
|
const identityAfter = session.wasmMotion.read().state.joint_commanded;
|
|
identityAfter.slice(0, 3).forEach((v, i) => close(v, identityBefore[i], "M429 XYZ uncompensated"));
|
|
send("JOG_STOP"); assert.equal(session.wasmMotion.read().state.active_tag, 0);
|
|
// A completed program line and a later jog may share the native TP ID.
|
|
const reusedId = 400000;
|
|
const collisionStart = session.wasmMotion.read().state.position;
|
|
assert.equal(session.wasmMotion.enqueue({ end: { ...collisionStart, x: collisionStart.x + .1 }, source_line: reusedId, kinematics_mode: 0, motion_type: 1, velocity: 10 }).status, "verified");
|
|
drain(); send("KINEMATICS_SELECT", { type: 1 });
|
|
assert.equal(session.wasmMotion.jog(5, 1, 2, reusedId).status, "verified");
|
|
for (let i = 0; i < 5000 && session.wasmMotion.read().state.queue_depth > 0; i++) {
|
|
send("SIM_STEP", { cycles: 1 });
|
|
assert.equal(session.wasmMotion.read().state.active_tag, 1, "jog must replace the completed line's mode mapping");
|
|
}
|
|
assert.equal(session.wasmMotion.read().state.queue_depth, 0);
|
|
send("JOG_STOP"); assert.equal(session.wasmMotion.read().state.active_tag, 1);
|
|
model.dispose();
|
|
rows.push({ machine: machine.id, samples, identity_before: identityBefore, identity_after: identityAfter, reused_segment_id: "verified" });
|
|
}
|
|
const report = { report_id: "trt-rtcp-jog-v1", status: "verified", rows };
|
|
const dir = new URL("coverage/trt-parametric/", root); await mkdir(dir, { recursive: true });
|
|
await writeFile(new URL("rtcp-jog.report.json", dir), JSON.stringify({ ...report, state_hash: fnv1a64(report) }, null, 2) + "\n");
|
|
console.log(JSON.stringify({ report_id: report.report_id, status: report.status, rows: rows.map((r) => ({ machine: r.machine, samples: r.samples.length, max_tcp_error: Math.max(...r.samples.map((s) => s.error)) })) }));
|