Files
linuxcncWebWASM/web/react/src/test_trt_rtcp_jog.mjs

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)) })) }));