import assert from "node:assert/strict"; import { readFile, mkdir, writeFile } from "node:fs/promises"; import createModule from "../../build-wasm/linuxcnc-sim-core.js"; import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs"; import { readTrtConfiguration } from "../workspace/trt-configuration.mjs"; import { MACHINE_DEFINITIONS } from "../workspace/machine-definitions.mjs"; import { SimSession, CONTEXT_ID } from "./sim-session.mjs"; import { fnv1a64 } from "./replay.mjs"; 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)); function edit(bundle, values) { let section = ""; const content = bundle.files[bundle.entrypoint].split("\n").map((line) => { const heading = /^\s*\[([^\]]+)\]/.exec(line); if (heading) section = heading[1]; const assignment = /^\s*([A-Z_]+)\s*=/.exec(line); const value = values[`${section}.${assignment?.[1]}`]; return value === undefined ? line : `${assignment[1]} = ${value}`; }).join("\n"); return { ...bundle, files: { ...bundle.files, [bundle.entrypoint]: content } }; } const rows = []; for (const machine of MACHINE_DEFINITIONS) { const original = await loadSimulationCase({ path: machine.entrypoint, fetchImpl }); const modified = edit(original, { "AXIS_X.MAX_VELOCITY": 4, "AXIS_X.MAX_ACCELERATION": 8, "AXIS_X.MAX_LIMIT": 15, "JOINT_0.MAX_LIMIT": 17, "TRAJ.MAX_LINEAR_VELOCITY": 13, "EMCMOT.SERVO_PERIOD": 2000000, "TASK.CYCLE_TIME": .02, "DISPLAY.MAX_FEED_OVERRIDE": 1.3 }); const config = toWorkerConfiguration(modified); const zeroTolerance = { ...modified, files: { ...modified.files, [modified.entrypoint]: `${modified.files[modified.entrypoint]}\n[WEB_RUNTIME]\nSPINDLE_TOLERANCE = 0\n` } }; assert.equal(readTrtConfiguration(zeroTolerance).kinematics.io.spindle_tolerance, 0); const blendingCase = { ...modified, files: { ...modified.files, [modified.entrypoint]: `${modified.files[modified.entrypoint]}\n[TRAJ]\nARC_BLEND_ENABLE = 1\nARC_BLEND_OPTIMIZATION_DEPTH = 25\nARC_BLEND_GAP_CYCLES = 3\nARC_BLEND_RAMP_FREQ = 50\nARC_BLEND_TANGENT_KINK_RATIO = 0.2\n` } }; const configuredBlend = readTrtConfiguration(blendingCase).kinematics.blend; assert.equal(configuredBlend.optimization_depth, 25); assert.equal(readTrtConfiguration(edit(original, { "TRAJ.MAX_LINEAR_ACCELERATION": 8 })).kinematics.max_acceleration, 8); for (const declaration of ["SPINDLE_COUNT = 2.5", "TOOL_CAPACITY = 1002", "ENCODER_COUNT = 9"]) { const invalid = { ...modified, files: { ...modified.files, [modified.entrypoint]: `${modified.files[modified.entrypoint]}\n[WEB_RUNTIME]\n${declaration}\n` } }; assert.throws(() => readTrtConfiguration(invalid), /trt_configuration_io_capacity/); } assert.deepEqual(config.home_limits.x, [-200, 17]); assert.deepEqual(config.kinematics.axis_limits.x, [-200, 15]); assert.equal(config.kinematics.axis_velocity_limits.x, 4); for (const values of [{ "AXIS_X.MAX_VELOCITY": "NaN" }, { "AXIS_X.MAX_ACCELERATION": 0 }, { "JOINT_0.MAX_LIMIT": -300 }, { "KINS.JOINTS": 4 }, { "TASK.CYCLE_TIME": .0015 }, { "DISPLAY.MAX_FEED_OVERRIDE": .5 }]) assert.throws(() => readTrtConfiguration(edit(original, values)), /trt_configuration/); const core = await createModule({ wasmBinary }); const session = new SimSession(core, { sessionId: `configuration-${machine.id}` }); let seq = 0; const send = (kind, payload = {}, expected = "ok") => { const result = session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, seq: ++seq, operation_id: `cfg-${seq}`, payload }); if (expected === "error") assert.equal(result.kind, "ERROR"); 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(original) }); // Existing adapter values are untrusted duplicates: the files must win. config.home_limits.x = [-1, 1]; config.kinematics.max_linear_velocity = 99; send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: config }); assert.equal(session.wasmMotion.velocityLimit, 13); assert.equal(session.wasmMotion.cycleTime, .002); assert.equal(session.wasmHomeLimit.period, .002); assert.equal(session.xyzacTaskCycleTicks, 10); assert.equal(readTrtConfiguration(modified).kinematics.io.cycle_time, .02); assert.equal(session.wasmMotion.jogVelocity("x"), 4); assert.deepEqual(session.xyzacKinematics.snapshot().limits.x, [-200, 17]); 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.ok(session.wasmHomeLimit.read().state.homed); send("SET_OVERRIDE", { feed: 1.31 }, "error"); send("SET_OVERRIDE", { feed: 1.3 }); send("SET_OVERRIDE", { feed: 1 }); send("JOG_START", { axis: "x", direction: 1, increment: 16 }, "error"); send("MDI_EXECUTE", { block: "G21 G90 G1 X10 F1200" }); let maxVelocity = 0, maxAcceleration = 0, previousVelocity = 0; const trace = []; for (let i = 0; i < 2000 && session.wasmMotion.read().state.queue_depth > 0; i++) { send("SIM_STEP", { cycles: 1 }); const state = session.wasmMotion.read().state; maxVelocity = Math.max(maxVelocity, state.velocity); maxAcceleration = Math.max(maxAcceleration, Math.abs(state.velocity - previousVelocity) / .02); previousVelocity = state.velocity; trace.push({ tick: session.tick, x: state.position.x, velocity: state.velocity }); } assert.equal(session.wasmMotion.read().state.queue_depth, 0); assert.ok(maxVelocity > 3.9 && maxVelocity <= 4.000001, `${machine.id} velocity ${maxVelocity}`); assert.ok(maxAcceleration <= 8.000001, `${machine.id} acceleration ${maxAcceleration}`); assert.ok(Math.abs(session.wasmMotion.read().state.position.x - 10) < 1e-8); rows.push({ machine: machine.id, max_velocity: maxVelocity, max_acceleration: maxAcceleration, period: session.wasmMotion.cycleTime, joint_limit: session.xyzacKinematics.snapshot().limits.x, trace }); const checkpoint = session.createCheckpoint(); send("POWER", { active: false }); const newSpeed = toWorkerConfiguration(edit(modified, { "AXIS_X.MAX_VELOCITY": 5 })); send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: newSpeed }); const restored = session.restoreCheckpoint(checkpoint); assert.equal(restored.status, "blocked"); assert.equal(restored.blocked[0].reason, "trt_recovery_configuration_mismatch"); assert.equal(session.wasmMotion.jogVelocity("x"), 5); } const report = { report_id: "trt-configuration-v1", status: "verified", rows }; await mkdir(new URL("coverage/configuration/", root), { recursive: true }); await writeFile(new URL("coverage/configuration/trt-runtime.report.json", root), JSON.stringify({ ...report, state_hash: fnv1a64(report) }, null, 2) + "\n"); console.log(JSON.stringify({ status: report.status, report_id: report.report_id, rows: rows.map(({ trace, ...row }) => row) }));