结论:已完成浏览器侧真实加载验证,M428/M429/M430 的配置入口进一步从 LinuxCNC INI/HALFILE/REMAP 来源生成,native 与 source-link 验证通过。
508 lines
18 KiB
JavaScript
508 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const { pathToFileURL } = require("url");
|
|
|
|
const wasmDir = path.join(__dirname, "web", "public");
|
|
const cjsLoader = path.join(os.tmpdir(), `cnc_sim_node_loader_${process.pid}.cjs`);
|
|
fs.copyFileSync(path.join(wasmDir, "cnc_sim.js"), cjsLoader);
|
|
const createCncSimModule = require(cjsLoader);
|
|
|
|
const EVENT_TYPE = {
|
|
2: "comment",
|
|
9: "rapid",
|
|
10: "linear-feed",
|
|
13: "program-end",
|
|
14: "rtcp-pivot",
|
|
15: "kinematics-switch",
|
|
};
|
|
|
|
const EVENT_OFFSETS = {
|
|
type: 4,
|
|
line: 8,
|
|
tool: 16,
|
|
feed: 24,
|
|
dwell: 40,
|
|
end: 120,
|
|
reserved: 268,
|
|
};
|
|
|
|
function readEvent(module, ptr) {
|
|
const view = new DataView(module.HEAPU8.buffer, ptr, 272);
|
|
return {
|
|
type: EVENT_TYPE[view.getInt32(EVENT_OFFSETS.type, true)] || "other",
|
|
line: view.getInt32(EVENT_OFFSETS.line, true),
|
|
tool: view.getInt32(EVENT_OFFSETS.tool, true),
|
|
feed: view.getFloat64(EVENT_OFFSETS.feed, true),
|
|
dwellSeconds: view.getFloat64(EVENT_OFFSETS.dwell, true),
|
|
endX: view.getFloat64(EVENT_OFFSETS.end + 0, true),
|
|
endY: view.getFloat64(EVENT_OFFSETS.end + 8, true),
|
|
endZ: view.getFloat64(EVENT_OFFSETS.end + 16, true),
|
|
endA: view.getFloat64(EVENT_OFFSETS.end + 24, true),
|
|
endB: view.getFloat64(EVENT_OFFSETS.end + 32, true),
|
|
endC: view.getFloat64(EVENT_OFFSETS.end + 40, true),
|
|
reserved: view.getInt32(EVENT_OFFSETS.reserved, true),
|
|
};
|
|
}
|
|
|
|
function writeString(module, value) {
|
|
const bytes = module.lengthBytesUTF8(value) + 1;
|
|
const ptr = module._malloc(bytes);
|
|
module.stringToUTF8(value, ptr, bytes);
|
|
return { ptr, len: bytes - 1 };
|
|
}
|
|
|
|
function expectKinematicsSwitch(events, line, kinstype, label) {
|
|
if (!events.some((event) => event.type === "kinematics-switch" &&
|
|
event.line === line &&
|
|
event.reserved === kinstype)) {
|
|
throw new Error(`expected LinuxCNC WASM ${label} kinematics switch ${kinstype} on line ${line}`);
|
|
}
|
|
}
|
|
|
|
function near(actual, expected) {
|
|
return Math.abs(actual - expected) < 1e-6;
|
|
}
|
|
|
|
(async () => {
|
|
const module = await createCncSimModule({
|
|
locateFile: (file) => path.join(wasmDir, file),
|
|
print: () => {},
|
|
printErr: () => {},
|
|
});
|
|
|
|
const create = module.cwrap("cnc_sim_create", "number", []);
|
|
const destroy = module.cwrap("cnc_sim_destroy", null, ["number"]);
|
|
const reset = module.cwrap("cnc_sim_reset", null, ["number"]);
|
|
const setDialect = module.cwrap("cnc_sim_set_dialect", "number", ["number", "number"]);
|
|
const setCallback = module.cwrap("cnc_sim_set_event_callback", "number", ["number", "number", "number"]);
|
|
const loadConfig = module.cwrap("cnc_sim_load_config_json", "number", ["number", "number", "number"]);
|
|
const parseProgram = module.cwrap("cnc_sim_parse_program", "number", ["number", "number", "number"]);
|
|
const lastError = module.cwrap("cnc_sim_last_error", "number", ["number"]);
|
|
|
|
const handle = create();
|
|
if (!handle) {
|
|
throw new Error("cnc_sim_create returned null");
|
|
}
|
|
|
|
let callbackPtr = 0;
|
|
try {
|
|
reset(handle);
|
|
if (setDialect(handle, 0) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
|
|
const config = writeString(module, JSON.stringify({ backend: "linuxcnc-rs274" }));
|
|
try {
|
|
if (loadConfig(handle, config.ptr, config.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
} finally {
|
|
module._free(config.ptr);
|
|
}
|
|
|
|
const events = [];
|
|
callbackPtr = module.addFunction((eventPtr) => {
|
|
events.push(readEvent(module, eventPtr));
|
|
return 0;
|
|
}, "iii");
|
|
if (setCallback(handle, callbackPtr, 0) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
|
|
const program = writeString(module, "G21 G90\nG0 X0\nG1 X5 F100\nM30\n");
|
|
try {
|
|
if (parseProgram(handle, program.ptr, program.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
} finally {
|
|
module._free(program.ptr);
|
|
}
|
|
|
|
if (!events.some((event) => event.type === "linear-feed" && event.endX === 5)) {
|
|
throw new Error("expected LinuxCNC WASM linear-feed event ending at X5");
|
|
}
|
|
if (!events.some((event) => event.type === "program-end")) {
|
|
throw new Error("expected LinuxCNC WASM program-end event");
|
|
}
|
|
|
|
events.length = 0;
|
|
const switchProgram = writeString(module, "M428\nM429\nM430\n");
|
|
try {
|
|
if (parseProgram(handle, switchProgram.ptr, switchProgram.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
} finally {
|
|
module._free(switchProgram.ptr);
|
|
}
|
|
|
|
for (const kinstype of [1, 0, 2]) {
|
|
if (!events.some((event) => event.type === "kinematics-switch" && event.reserved === kinstype)) {
|
|
throw new Error(`expected LinuxCNC WASM kinematics switch ${kinstype}`);
|
|
}
|
|
if (!events.some((event) => event.type === "comment" &&
|
|
event.reserved === 68 &&
|
|
event.tool === 3 &&
|
|
event.feed === kinstype)) {
|
|
throw new Error(`expected LinuxCNC WASM M68 E3 Q${kinstype} remap side effect`);
|
|
}
|
|
}
|
|
if (!events.some((event) => event.type === "comment" &&
|
|
event.reserved === 66 &&
|
|
event.tool === 0 &&
|
|
event.feed === 0)) {
|
|
throw new Error("expected LinuxCNC WASM M66 E0 L0 remap side effect");
|
|
}
|
|
|
|
const switchkinsCases = [
|
|
{
|
|
label: "scara",
|
|
config: { backend: "linuxcnc-rs274", switchkins: "scara" },
|
|
program: "M428\nM429\nM430\n",
|
|
expected: [
|
|
[1, 0],
|
|
[2, 1],
|
|
[3, 2],
|
|
],
|
|
},
|
|
{
|
|
label: "table-dual-rotary",
|
|
config: { backend: "linuxcnc-rs274", switchkins: "table-dual-rotary" },
|
|
program: "M428\nM429\n",
|
|
expected: [
|
|
[1, 1],
|
|
[2, 0],
|
|
],
|
|
},
|
|
{
|
|
label: "sim-xyzab-tdr-kins",
|
|
config: { backend: "linuxcnc-rs274", switchkins: "sim-xyzab-tdr-kins" },
|
|
program: "M428\nM429\n",
|
|
expected: [
|
|
[1, 1],
|
|
[2, 0],
|
|
],
|
|
},
|
|
{
|
|
label: "puma",
|
|
config: { backend: "linuxcnc-rs274", switchkins: "puma" },
|
|
program: "M428\nM429\nM430\n",
|
|
expected: [
|
|
[1, 0],
|
|
[2, 1],
|
|
[3, 2],
|
|
],
|
|
},
|
|
{
|
|
label: "melfa-sim",
|
|
config: { backend: "linuxcnc-rs274", switchkins: "melfa-sim" },
|
|
program: "M428\nM429\nM430\n",
|
|
expected: [
|
|
[1, 0],
|
|
[2, 1],
|
|
[3, 2],
|
|
],
|
|
},
|
|
{
|
|
label: "hexapod-sim",
|
|
config: { backend: "linuxcnc-rs274", remap: "hexapod-sim" },
|
|
program: "M428\nM429\nM430\n",
|
|
expected: [
|
|
[1, 0],
|
|
[2, 1],
|
|
[3, 2],
|
|
],
|
|
},
|
|
{
|
|
label: "xyzbca-trsrn",
|
|
config: { backend: "linuxcnc-rs274", switchkins: "xyzbca-trsrn" },
|
|
program: "M428\nM429\nM430\n",
|
|
expected: [
|
|
[1, 0],
|
|
[2, 1],
|
|
[3, 2],
|
|
],
|
|
},
|
|
];
|
|
|
|
for (const testCase of switchkinsCases) {
|
|
reset(handle);
|
|
events.length = 0;
|
|
const switchkinsConfig = writeString(module, JSON.stringify(testCase.config));
|
|
try {
|
|
if (loadConfig(handle, switchkinsConfig.ptr, switchkinsConfig.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
} finally {
|
|
module._free(switchkinsConfig.ptr);
|
|
}
|
|
|
|
const switchkinsProgram = writeString(module, testCase.program);
|
|
try {
|
|
if (parseProgram(handle, switchkinsProgram.ptr, switchkinsProgram.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
} finally {
|
|
module._free(switchkinsProgram.ptr);
|
|
}
|
|
|
|
for (const [line, kinstype] of testCase.expected) {
|
|
expectKinematicsSwitch(events, line, kinstype, testCase.label);
|
|
}
|
|
}
|
|
|
|
reset(handle);
|
|
events.length = 0;
|
|
const fiveaxisConfig = writeString(
|
|
module,
|
|
JSON.stringify({
|
|
backend: "linuxcnc-rs274",
|
|
rtcp: { enabled: true, toolLength: 250 },
|
|
kinematics: "5axiskins",
|
|
pivotLength: 250,
|
|
}),
|
|
);
|
|
try {
|
|
if (loadConfig(handle, fiveaxisConfig.ptr, fiveaxisConfig.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
} finally {
|
|
module._free(fiveaxisConfig.ptr);
|
|
}
|
|
|
|
const fiveaxisProgram = writeString(module, "M428\nG0 X260 Y20 Z280 B90 C0\n");
|
|
try {
|
|
if (parseProgram(handle, fiveaxisProgram.ptr, fiveaxisProgram.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
} finally {
|
|
module._free(fiveaxisProgram.ptr);
|
|
}
|
|
|
|
expectKinematicsSwitch(events, 1, 0, "5axiskins");
|
|
if (!events.some((event) => event.type === "rtcp-pivot" &&
|
|
event.line === 2 &&
|
|
event.reserved === 0 &&
|
|
near(event.dwellSeconds, 250) &&
|
|
near(event.endX, 10) &&
|
|
near(event.endY, 20) &&
|
|
near(event.endZ, 30) &&
|
|
near(event.endB, 90) &&
|
|
near(event.endC, 0))) {
|
|
throw new Error("expected LinuxCNC WASM 5axiskins RTCP pivot from LinuxCNC 5axiskins inverse");
|
|
}
|
|
|
|
reset(handle);
|
|
events.length = 0;
|
|
const userkConfig = writeString(
|
|
module,
|
|
JSON.stringify({
|
|
backend: "linuxcnc-rs274",
|
|
rtcp: { enabled: true, toolLength: 250 },
|
|
}),
|
|
);
|
|
try {
|
|
if (loadConfig(handle, userkConfig.ptr, userkConfig.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
} finally {
|
|
module._free(userkConfig.ptr);
|
|
}
|
|
|
|
const userkProgram = writeString(module, "M430\nG0 X260 Y20 Z280 B90 C45\n");
|
|
try {
|
|
if (parseProgram(handle, userkProgram.ptr, userkProgram.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(handle)));
|
|
}
|
|
} finally {
|
|
module._free(userkProgram.ptr);
|
|
}
|
|
|
|
expectKinematicsSwitch(events, 1, 2, "userk");
|
|
if (!events.some((event) => event.type === "rtcp-pivot" &&
|
|
event.line === 2 &&
|
|
event.reserved === 2 &&
|
|
near(event.dwellSeconds, 250) &&
|
|
near(event.endX, 260) &&
|
|
near(event.endY, 20) &&
|
|
near(event.endZ, 280) &&
|
|
near(event.endB, 90) &&
|
|
near(event.endC, 45))) {
|
|
throw new Error("expected LinuxCNC WASM userk RTCP pivot from LinuxCNC userk identity inverse");
|
|
}
|
|
|
|
events.length = 0;
|
|
const configuredTrtHandle = create();
|
|
if (!configuredTrtHandle) {
|
|
throw new Error("cnc_sim_create returned null for configured xyzbc-trt case");
|
|
}
|
|
const configuredTrtConfig = writeString(
|
|
module,
|
|
JSON.stringify({
|
|
backend: "linuxcnc-rs274",
|
|
rtcp: { enabled: false, toolLength: 11 },
|
|
xyzbcTrt: {
|
|
xRotPoint: 3,
|
|
yRotPoint: -4,
|
|
zRotPoint: 5,
|
|
xOffset: 2,
|
|
zOffset: 7,
|
|
conventionalDirections: true,
|
|
},
|
|
}),
|
|
);
|
|
try {
|
|
reset(configuredTrtHandle);
|
|
if (setDialect(configuredTrtHandle, 0) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(configuredTrtHandle)));
|
|
}
|
|
if (setCallback(configuredTrtHandle, callbackPtr, 0) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(configuredTrtHandle)));
|
|
}
|
|
if (loadConfig(configuredTrtHandle, configuredTrtConfig.ptr, configuredTrtConfig.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(configuredTrtHandle)));
|
|
}
|
|
} finally {
|
|
module._free(configuredTrtConfig.ptr);
|
|
}
|
|
|
|
const configuredTrtProgram = writeString(module, "M428\nG43.4\nG0 X30 Y40 Z50 B20 C-30\n");
|
|
try {
|
|
if (parseProgram(configuredTrtHandle, configuredTrtProgram.ptr, configuredTrtProgram.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(configuredTrtHandle)));
|
|
}
|
|
} finally {
|
|
module._free(configuredTrtProgram.ptr);
|
|
destroy(configuredTrtHandle);
|
|
}
|
|
|
|
expectKinematicsSwitch(events, 1, 1, "configured xyzbc-trt");
|
|
if (!events.some((event) => event.type === "rtcp-pivot" &&
|
|
event.line === 3 &&
|
|
event.reserved === 1 &&
|
|
near(event.dwellSeconds, 11) &&
|
|
near(event.endX, -4.814629) &&
|
|
near(event.endY, 47.605118) &&
|
|
near(event.endZ, 48.160567) &&
|
|
near(event.endB, 20) &&
|
|
near(event.endC, -30))) {
|
|
throw new Error("expected LinuxCNC WASM configured xyzbc-trt RTCP pivot from LinuxCNC trtfuncs inverse");
|
|
}
|
|
|
|
events.length = 0;
|
|
const configuredXyzacHandle = create();
|
|
if (!configuredXyzacHandle) {
|
|
throw new Error("cnc_sim_create returned null for configured xyzac-trt case");
|
|
}
|
|
const configuredXyzacConfig = writeString(
|
|
module,
|
|
JSON.stringify({
|
|
backend: "linuxcnc-rs274",
|
|
rtcp: { enabled: false, toolLength: 7 },
|
|
switchkins: "xyzac-trt",
|
|
xyzbcTrt: {
|
|
yOffset: 20,
|
|
zOffset: 10,
|
|
},
|
|
}),
|
|
);
|
|
try {
|
|
reset(configuredXyzacHandle);
|
|
if (setDialect(configuredXyzacHandle, 0) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle)));
|
|
}
|
|
if (setCallback(configuredXyzacHandle, callbackPtr, 0) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle)));
|
|
}
|
|
if (loadConfig(configuredXyzacHandle, configuredXyzacConfig.ptr, configuredXyzacConfig.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle)));
|
|
}
|
|
} finally {
|
|
module._free(configuredXyzacConfig.ptr);
|
|
}
|
|
|
|
const configuredXyzacProgram = writeString(module, "M428\nG43.4\nG0 X12 Y-8 Z42 A35 C-25\n");
|
|
try {
|
|
if (parseProgram(configuredXyzacHandle, configuredXyzacProgram.ptr, configuredXyzacProgram.len) !== 0) {
|
|
throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle)));
|
|
}
|
|
} finally {
|
|
module._free(configuredXyzacProgram.ptr);
|
|
destroy(configuredXyzacHandle);
|
|
}
|
|
|
|
expectKinematicsSwitch(events, 1, 1, "configured xyzac-trt");
|
|
if (!events.some((event) => event.type === "rtcp-pivot" &&
|
|
event.line === 3 &&
|
|
event.reserved === 1 &&
|
|
near(event.dwellSeconds, 7) &&
|
|
near(event.endX, 7.494747) &&
|
|
near(event.endY, -20.815946) &&
|
|
near(event.endZ, 18.939732) &&
|
|
near(event.endA, 35) &&
|
|
near(event.endB, 0) &&
|
|
near(event.endC, -25))) {
|
|
throw new Error("expected LinuxCNC WASM configured xyzac-trt RTCP pivot from LinuxCNC xyzab_tdr inverse");
|
|
}
|
|
|
|
globalThis.createCncSimModule = createCncSimModule;
|
|
const wasmCoreUrl = pathToFileURL(path.join(__dirname, "web", "src", "wasm-core.js")).href;
|
|
const { createWasmSimulator } = await import(wasmCoreUrl);
|
|
const webSimulator = await createWasmSimulator({
|
|
locateFile: (file) => path.join(wasmDir, file),
|
|
print: () => {},
|
|
printErr: () => {},
|
|
});
|
|
try {
|
|
const defaultBackendEvents = webSimulator.parse("G21 G90\nG1 X3 F30\nM30\n", "linuxcnc");
|
|
if (!defaultBackendEvents.some((event) => event.type === "linear-feed" &&
|
|
event.line === 2 &&
|
|
near(event.end.x, 3))) {
|
|
throw new Error("expected web wasm-core default backend to use LinuxCNC RS274");
|
|
}
|
|
|
|
const webEvents = webSimulator.parse(
|
|
"M428\nG43.4\nG0 X12 Y-8 Z42 A35 C-25\n",
|
|
"linuxcnc",
|
|
{
|
|
backend: "linuxcnc-rs274",
|
|
rtcp: { enabled: false, toolLength: 7 },
|
|
switchkins: "xyzac-trt",
|
|
xyzbcTrt: {
|
|
yOffset: 20,
|
|
zOffset: 10,
|
|
},
|
|
},
|
|
);
|
|
if (!webEvents.some((event) => event.type === "rtcp-pivot" &&
|
|
event.line === 3 &&
|
|
event.reserved === 1 &&
|
|
near(event.dwellSeconds, 7) &&
|
|
near(event.end.x, 7.494747) &&
|
|
near(event.end.y, -20.815946) &&
|
|
near(event.end.z, 18.939732) &&
|
|
near(event.end.a, 35) &&
|
|
near(event.end.b, 0) &&
|
|
near(event.end.c, -25))) {
|
|
throw new Error("expected web wasm-core options to pass LinuxCNC xyzac-trt RTCP config into WASM");
|
|
}
|
|
} finally {
|
|
webSimulator.dispose();
|
|
delete globalThis.createCncSimModule;
|
|
}
|
|
} finally {
|
|
if (callbackPtr) {
|
|
module.removeFunction(callbackPtr);
|
|
}
|
|
destroy(handle);
|
|
}
|
|
|
|
console.log("web wasm node smoke passed");
|
|
})().catch((error) => {
|
|
console.error(error && error.stack ? error.stack : error);
|
|
process.exit(1);
|
|
}).finally(() => {
|
|
fs.rmSync(cjsLoader, { force: true });
|
|
});
|