结论:完成 LinuxCNC kinematics WASM ABI 覆盖,并将 web-rtcp-5axis-sim-plan 的 RTCP frame/boundary adapter 接到 xyzac-trt kinematics SDK;Node、build、browser smoke 验证通过。
231 lines
8.8 KiB
JavaScript
231 lines
8.8 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, resolve } from "node:path";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
createLinuxCncKinematicsSdk,
|
|
linuxCncKinematicsWasmFile,
|
|
supportedLinuxCncKinematicsModules,
|
|
} from "../../../runtime/sdk/src/index.js";
|
|
|
|
const EXPECTED_MODULES = [
|
|
"trivkins",
|
|
"5axiskins",
|
|
"xyzac-trt",
|
|
"xyzbc-trt",
|
|
"corexy",
|
|
"rotate",
|
|
"rose",
|
|
"max",
|
|
"lineardelta",
|
|
"rotarydelta",
|
|
"scorbot",
|
|
"tripod",
|
|
"scara",
|
|
"puma",
|
|
"genser",
|
|
"genhex",
|
|
"pentakins",
|
|
];
|
|
|
|
const EXPECTED_META = {
|
|
trivkins: { type: 1, switchable: 0 },
|
|
"5axiskins": { type: 4, switchable: 1 },
|
|
"xyzac-trt": { type: 4, switchable: 1 },
|
|
"xyzbc-trt": { type: 4, switchable: 1 },
|
|
corexy: { type: 4, switchable: 0 },
|
|
rotate: { type: 4, switchable: 0 },
|
|
rose: { type: 4, switchable: 0 },
|
|
max: { type: 4, switchable: 0 },
|
|
lineardelta: { type: 4, switchable: 0 },
|
|
rotarydelta: { type: 4, switchable: 0 },
|
|
scorbot: { type: 4, switchable: 0 },
|
|
tripod: { type: 4, switchable: 0 },
|
|
scara: { type: 4, switchable: 1 },
|
|
puma: { type: 4, switchable: 1 },
|
|
genser: { type: 4, switchable: 1 },
|
|
genhex: { type: 4, switchable: 1 },
|
|
pentakins: { type: 4, switchable: 0 },
|
|
};
|
|
|
|
function near(actual, expected, tolerance = 1e-7) {
|
|
return Math.abs(actual - expected) < tolerance;
|
|
}
|
|
|
|
function assertNear(actual, expected, label, tolerance = 1e-7) {
|
|
assert.equal(near(actual, expected, tolerance), true, `${label}: ${actual} != ${expected}`);
|
|
}
|
|
|
|
function assertArrayNear(actual, expected, label, tolerance = 1e-7) {
|
|
assert.equal(actual.length >= expected.length, true, `${label}: missing values`);
|
|
expected.forEach((value, index) => assertNear(actual[index], value, `${label}[${index}]`, tolerance));
|
|
}
|
|
|
|
function assertPoseNear(actual, expected, label, tolerance = 1e-7) {
|
|
for (const key of ["x", "y", "z", "a", "b", "c", "u", "v", "w"]) {
|
|
assertNear(actual[key] ?? 0, expected[key] ?? 0, `${label}.${key}`, tolerance);
|
|
}
|
|
}
|
|
|
|
function assertProbeBasics(moduleId, probe) {
|
|
assert.equal(probe.includes("kinematics_init=0"), true, `${moduleId}: init probe`);
|
|
assert.equal(
|
|
probe.includes(`kinematics_type=${EXPECTED_META[moduleId].type}`),
|
|
true,
|
|
`${moduleId}: type probe`,
|
|
);
|
|
assert.equal(
|
|
probe.includes(`kinematics_switchable=${EXPECTED_META[moduleId].switchable}`),
|
|
true,
|
|
`${moduleId}: switchable probe`,
|
|
);
|
|
}
|
|
|
|
function assertSwitchIdentity(kins, moduleId, joints, expectedPose) {
|
|
assert.equal(kins.switchKinematics(1), 0, `${moduleId}: switch identity`);
|
|
const identity = kins.forward(joints);
|
|
assert.equal(identity.rc, 0, `${moduleId}: identity forward`);
|
|
assertPoseNear(identity.pose, expectedPose, `${moduleId}: identity pose`);
|
|
}
|
|
|
|
function assertJointRoundtrip(kins, moduleId, joints, jointCount = joints.length, tolerance = 1e-7) {
|
|
if (kins.switchable()) {
|
|
assert.equal(kins.switchKinematics(0), 0, `${moduleId}: switch primary`);
|
|
}
|
|
const forward = kins.forward(joints);
|
|
assert.equal(forward.rc, 0, `${moduleId}: forward`);
|
|
const inverse = kins.inverse(forward.pose, jointCount, { seedJoints: joints });
|
|
assert.equal(inverse.rc, 0, `${moduleId}: inverse`);
|
|
assertArrayNear(inverse.joints, joints.slice(0, jointCount), `${moduleId}: joints`, tolerance);
|
|
}
|
|
|
|
function assertPoseRoundtrip(kins, moduleId, pose, jointCount, options = {}) {
|
|
if (kins.switchable()) {
|
|
assert.equal(kins.switchKinematics(0), 0, `${moduleId}: switch primary`);
|
|
}
|
|
const inverse = kins.inverse(pose, jointCount, options.inverseOptions ?? {});
|
|
assert.equal(inverse.rc, 0, `${moduleId}: inverse`);
|
|
if (options.warmupForward) {
|
|
kins.forward(inverse.joints, { seedPose: pose });
|
|
}
|
|
const forward = kins.forward(inverse.joints, { seedPose: pose });
|
|
assert.equal(forward.rc, 0, `${moduleId}: forward`);
|
|
assertPoseNear(forward.pose, pose, `${moduleId}: pose`, options.tolerance ?? 1e-7);
|
|
return { inverse, forward };
|
|
}
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const rootDir = resolve(__dirname, "../../..");
|
|
|
|
assert.deepEqual(supportedLinuxCncKinematicsModules(), EXPECTED_MODULES);
|
|
|
|
for (const moduleId of supportedLinuxCncKinematicsModules()) {
|
|
const wasmFile = linuxCncKinematicsWasmFile(moduleId);
|
|
const wasmPath = resolve(rootDir, "build/wasm/kinematics", wasmFile);
|
|
const kins = await createLinuxCncKinematicsSdk({
|
|
moduleId,
|
|
moduleOptions: {
|
|
wasmBinary: readFileSync(wasmPath),
|
|
print() {},
|
|
printErr(message) {
|
|
console.error(`[${moduleId}] ${message}`);
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.equal(kins.apiName, "linuxcnc-kinematics-wasm-sdk");
|
|
assert.equal(kins.wasmFile, wasmFile);
|
|
for (const functionName of ["lckins_forward", "lckins_inverse", "lckins_run_probe"]) {
|
|
assert.equal(kins.hasWasmFunction(functionName), true, `${moduleId}: ${functionName}`);
|
|
}
|
|
assert.equal(kins.type(), EXPECTED_META[moduleId].type, `${moduleId}: type`);
|
|
assert.equal(kins.switchable(), EXPECTED_META[moduleId].switchable, `${moduleId}: switchable`);
|
|
assertProbeBasics(moduleId, kins.runProbe());
|
|
|
|
switch (moduleId) {
|
|
case "trivkins":
|
|
assertJointRoundtrip(kins, moduleId, [1, 2, 3, 4, 5, 6, 7, 8, 9], 9);
|
|
break;
|
|
case "5axiskins":
|
|
assertJointRoundtrip(kins, moduleId, [10, 20, 30, 45, 30, 5], 6);
|
|
assertSwitchIdentity(kins, moduleId, [10, 20, 30, 45, 30, 5], { x: 10, y: 20, z: 30, b: 45, c: 30, w: 5 });
|
|
break;
|
|
case "xyzac-trt":
|
|
assertJointRoundtrip(kins, moduleId, [10, 20, 30, 25, 40], 5);
|
|
assertSwitchIdentity(kins, moduleId, [10, 20, 30, 25, 40], { x: 10, y: 20, z: 30, a: 25, c: 40 });
|
|
break;
|
|
case "xyzbc-trt":
|
|
assertJointRoundtrip(kins, moduleId, [10, 20, 30, 35, 40], 5);
|
|
assertSwitchIdentity(kins, moduleId, [10, 20, 30, 35, 40], { x: 10, y: 20, z: 30, b: 35, c: 40 });
|
|
break;
|
|
case "corexy":
|
|
assertJointRoundtrip(kins, moduleId, [12, 4, 3, 4, 5, 6, 7, 8, 9], 9);
|
|
break;
|
|
case "rotate":
|
|
assertJointRoundtrip(kins, moduleId, [10, 20, 30, 4, 5, 30, 7, 8, 9], 9);
|
|
break;
|
|
case "rose":
|
|
assertJointRoundtrip(kins, moduleId, [12, 5, 30], 3);
|
|
break;
|
|
case "max":
|
|
assertJointRoundtrip(kins, moduleId, [10, 20, 30, 4, 0, 25, 0, 0, 3.5], 9);
|
|
break;
|
|
case "lineardelta":
|
|
assertPoseRoundtrip(kins, moduleId, { x: 10, y: 20, z: -30, a: 1, b: 2, c: 3, u: 4, v: 5, w: 6 }, 9);
|
|
break;
|
|
case "rotarydelta":
|
|
assertPoseRoundtrip(kins, moduleId, { x: 0, y: 0, z: -12, a: 1, b: 2, c: 3, u: 4, v: 5, w: 6 }, 9);
|
|
break;
|
|
case "scorbot": {
|
|
const forward = kins.forward([25, 20, 10, 4, 5]);
|
|
assert.equal(forward.rc, 0, `${moduleId}: forward`);
|
|
const inverse = kins.inverse(forward.pose, 5);
|
|
assert.equal(inverse.rc, 0, `${moduleId}: inverse`);
|
|
const roundtrip = kins.forward(inverse.joints);
|
|
assert.equal(roundtrip.rc, 0, `${moduleId}: roundtrip forward`);
|
|
assertPoseNear(roundtrip.pose, forward.pose, `${moduleId}: pose`);
|
|
break;
|
|
}
|
|
case "tripod":
|
|
assertPoseRoundtrip(kins, moduleId, { x: 0.25, y: 0.25, z: 0.5 }, 3);
|
|
break;
|
|
case "scara":
|
|
assertJointRoundtrip(kins, moduleId, [20, 110, 30, 15, 4, 5], 6);
|
|
assertSwitchIdentity(kins, moduleId, [20, 110, 30, 15, 4, 5], { x: 20, y: 110, z: 30, a: 15, b: 4, c: 5 });
|
|
break;
|
|
case "puma": {
|
|
if (kins.switchable()) {
|
|
assert.equal(kins.switchKinematics(0), 0, `${moduleId}: switch primary`);
|
|
}
|
|
const forward = kins.forward([20, -30, 40, 15, 35, -25]);
|
|
assert.equal(forward.rc, 0, `${moduleId}: forward`);
|
|
const inverse = kins.inverse(forward.pose, 6);
|
|
assert.equal(inverse.rc, 0, `${moduleId}: inverse`);
|
|
const roundtrip = kins.forward(inverse.joints);
|
|
assert.equal(roundtrip.rc, 0, `${moduleId}: roundtrip forward`);
|
|
assertPoseNear(roundtrip.pose, forward.pose, `${moduleId}: pose`);
|
|
assertSwitchIdentity(kins, moduleId, [20, -30, 40, 15, 35, -25], { x: 20, y: -30, z: 40, a: 15, b: 35, c: -25 });
|
|
break;
|
|
}
|
|
case "genser":
|
|
assertJointRoundtrip(kins, moduleId, [10, -15, 20, 5, -10, 12, 7, 8, 9], 9);
|
|
assertSwitchIdentity(kins, moduleId, [10, -15, 20, 5, -10, 12, 7, 8, 9], { x: 10, y: -15, z: 20, a: 5, b: -10, c: 12, u: 7, v: 8, w: 9 });
|
|
break;
|
|
case "genhex":
|
|
assertPoseRoundtrip(kins, moduleId, { x: 1.25, y: -2.5, z: 3.75, a: 1, b: -2, c: 3 }, 6, { warmupForward: true });
|
|
assert.equal(kins.switchKinematics(1), 0, `${moduleId}: switch alternate`);
|
|
break;
|
|
case "pentakins": {
|
|
const { inverse } = assertPoseRoundtrip(kins, moduleId, { x: 12.5, y: -8.25, z: 25, a: 2, b: -1.5 }, 5, { tolerance: 1e-6 });
|
|
assert.equal(inverse.joints.every((value) => value > 0), true, `${moduleId}: positive struts`);
|
|
break;
|
|
}
|
|
default:
|
|
throw new Error(`missing kinematics verification for ${moduleId}`);
|
|
}
|
|
}
|
|
|
|
console.log("kinematics_wasm_node_smoke=ok");
|