import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js"; import { getFiveAxisProfile } from "../../app/src/profiles/index.js"; import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js"; import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js"; import { wrapTaskHalSdk } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js"; import { createSimulationStore } from "../../app/src/state/store.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const rootDir = resolve(__dirname, "../../.."); const wasmPath = resolve(rootDir, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm"); const sdk = await createLinuxCncTaskHalSdk({ wasmBinary: readFileSync(wasmPath), print() {}, printErr(message) { console.error(message); }, }); const runtime = wrapTaskHalSdk(sdk); const profile = getFiveAxisProfile("xyzac-trt"); const iniText = readFileSync( resolve(rootDir, "wasm-port/vendor/linuxcnc", profile.iniPath), "utf8", ); const iniConfig = parseLinuxCncIni(iniText, { path: profile.iniPath, profileId: profile.id, }); const store = createSimulationStore(); store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig }); store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime: await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }), }); store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime }); assert.equal(store.getState().taskHalRuntimeReadiness.taskRuntimeReady, true); assert.equal(store.getState().taskHalRuntimeReadiness.motionRuntimeReady, true); assert.equal(store.getState().taskHalRuntimeReadiness.halRuntimeReady, true); await store.stageMachineFiles(); store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc", }); await waitForState(store, (state) => ( state.machineFileStaging.selectedGcodeSourceRel?.endsWith("xyzac_switchkins_test_1.ngc") && state.taskHalSession?.programPath?.endsWith("xyzac_switchkins_test_1.ngc") )); store.dispatch({ type: "TOGGLE_POWER" }); await waitForTaskHal(store); store.dispatch({ type: "HOME" }); store.dispatch({ type: "SET_MODE", mode: "auto" }); await waitForTaskHal(store); store.dispatch({ type: "RUN" }); await waitForTaskHal(store); let state = store.getState(); assert.equal(state.taskHalStatus.summary.taskRuntimeReady, true); assert.equal(state.taskHalStatus.summary.halSyncReady, true); assert.equal(state.taskHalStatus.ui.activeLine >= 1, true); assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm"); assert.equal(state.fullExecutionBoundary.nativeTaskReady, true); assert.equal(state.fullExecutionBoundary.nativeHalSyncReady, true); await store.initializeTaskHalSession({ openProgram: true }); if (!store.getState().machine.powerOn) { store.dispatch({ type: "TOGGLE_POWER" }); await waitForTaskHal(store); } store.dispatch({ type: "HOME" }); store.dispatch({ type: "SET_MODE", mode: "mdi" }); await waitForTaskHal(store); store.dispatch({ type: "RUN_MDI", command: "M428" }); await waitForTaskHal(store); state = store.getState(); assert.equal(state.taskHalStatus.ui.switchkinsType, 1); assert.equal(state.kinsType, "tcp-xyzac"); assert.equal(state.rtcpState, "on"); store.dispatch({ type: "SET_MODE", mode: "manual" }); await waitForTaskHal(store); store.dispatch({ type: "HOME" }); state = store.getState(); const homePose = { ...state.axisPose }; assert.equal(homePose.x, 43); assert.equal(homePose.y, -32.15); assert.equal(homePose.z, -11.306); store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 0.5 }); await waitForTaskHal(store); state = store.getState(); assert.equal(state.taskHalStatus.motionStatus.motion.teleopMode, 1); assert.equal(state.programRuntimeFeedback.sourceMode, "linuxcnc-task-motion-hal-wasm"); assertNear(state.axisPose.x, homePose.x + 0.5, "task/HAL JOG X+ should keep HOME work-pose continuity"); assertNear(state.axisPose.y, homePose.y, "task/HAL JOG X+ should not reset Y"); assertNear(state.axisPose.z, homePose.z, "task/HAL JOG X+ should not reset Z"); store.dispatch({ type: "JOG", axis: "y", direction: -1, increment: 0.5 }); await waitForTaskHal(store); state = store.getState(); assertNear(state.axisPose.x, homePose.x + 0.5, "task/HAL JOG Y- should not reset X"); assertNear(state.axisPose.y, homePose.y - 0.5, "task/HAL JOG Y- should keep HOME work-pose continuity"); assertNear(state.axisPose.z, homePose.z, "task/HAL JOG Y- should not reset Z"); store.dispatch({ type: "SET_MODE", mode: "auto" }); await waitForTaskHal(store); store.dispatch({ type: "PAUSE" }); await waitForTaskHal(store); assert.equal(store.getState().machine.interpState, "paused"); store.dispatch({ type: "RESUME" }); await waitForTaskHal(store); assert.equal(store.getState().machine.interpState, "reading"); store.dispatch({ type: "STEP" }); await waitForTaskHal(store); state = store.getState(); assert.equal(state.machine.interpState, "paused"); assert.equal(state.machine.taskPaused, true); assert.equal(state.taskHalStatus.task.singleStepping, true); store.dispatch({ type: "STOP" }); await waitForTaskHal(store); state = store.getState(); assert.equal(state.runState, "stopped"); assert.equal(state.machine.interpState, "idle"); assert.equal(state.taskHalStatus.motionStatus.motion.aborted, true); console.log("linuxcnc_task_hal_runtime_smoke=ok"); console.log("task_hal_machine_file_smoke=ok"); console.log("switchkins_remap_hal_sync_smoke=ok"); console.log("browser_task_hal_worker_smoke=ok"); async function waitForTaskHal(store) { for (let attempt = 0; attempt < 30; attempt += 1) { if (!store.getState().taskHalExecutionPending) { await new Promise((resolve) => setTimeout(resolve, 0)); if (!store.getState().taskHalExecutionPending) return store.getState(); } await new Promise((resolve) => setTimeout(resolve, 0)); } throw new Error("task/HAL store command did not settle"); } async function waitForState(store, predicate) { for (let attempt = 0; attempt < 60; attempt += 1) { const state = store.getState(); if (predicate(state)) return state; await new Promise((resolve) => setTimeout(resolve, 0)); } throw new Error("store state condition did not settle"); } function assertNear(actual, expected, message) { assert.equal(Math.abs(Number(actual) - Number(expected)) < 1e-9, true, `${message}: ${actual} !== ${expected}`); }