Preserve RTCP compensation across rotary jog stops
This commit is contained in:
@@ -136,7 +136,7 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
// workpiece path together with the physical table.
|
||||
const pathGroup = new THREE.Group(); pathGroup.name = "xyzac-toolpath"; assembly.joints.rotate.add(pathGroup);
|
||||
const liveGroup = new THREE.Group(); liveGroup.name = "xyzac-live-toolpath"; assembly.joints.rotate.add(liveGroup);
|
||||
const pathMaterials = [0xdce7e8, 0xff9a55, 0x5c9d9a].map((color) => new THREE.LineBasicMaterial({ color }));
|
||||
const pathMaterials = [0xffff00, 0xff9a55, 0x5c9d9a].map((color) => new THREE.LineBasicMaterial({ color }));
|
||||
const clearLines = (group: THREE.Group) => { group.children.forEach((child) => (child as THREE.Line).geometry.dispose()); group.clear(); };
|
||||
const limits = new THREE.Box3Helper(new THREE.Box3(), 0xdd7566); scene.add(limits);
|
||||
const offsets = new THREE.AxesHelper(.8); scene.add(offsets);
|
||||
|
||||
123
web/react/src/test_trt_rtcp_jog.mjs
Normal file
123
web/react/src/test_trt_rtcp_jog.mjs
Normal file
@@ -0,0 +1,123 @@
|
||||
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)) })) }));
|
||||
Reference in New Issue
Block a user