#!/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 }); });