import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js"; import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js"; import { createMachineFileStagingPlan, selectMachineFileProgram, stageProfileMachineFiles, } from "../../app/src/runtime/linuxcnc-machine-file-staging.js"; import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js"; import { buildTaskHalProgramMotionPlan, buildTaskHalSessionFromMachineFiles, normalizeTaskHalStatus, wrapTaskHalSdk, } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js"; import { getFiveAxisProfile } from "../../app/src/profiles/index.js"; import { createSimulationStore } from "../../app/src/state/store.js"; import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js"; import { buildVismachModelState, XYZBC_TRT_VISMACH_PINS } from "../../app/src/runtime/vismach-model-state.js"; import { AXIS_BUTTON_PARITY } from "../../app/src/ui/axis-shell.js"; import { applyToolCommandSequence, createToolRuntimeState, } from "../../app/src/runtime/tool-db-simulation.js"; import { buildAxisExecutionTraceFromProgram } from "../../app/src/runtime/axis-preview-path.js"; const profile = getFiveAxisProfile(); assert.equal(profile.id, "xyzbc-trt"); assert.equal(profile.machineName, "sim-xyzbc-trt-kins (switchkins)"); assert.equal(profile.traj.coordinates, "XYZBC"); assert.deepEqual(profile.coordinates, ["X", "Y", "Z", "B", "C"]); assert.equal(profile.kinematics, "xyzbc-trt-kins"); assert.equal(profile.kinematicsModuleId, "xyzbc-trt"); assert.equal(profile.machineFileStaging.defaultProgramFilename, "xyzbc_switchkins.ngc"); assert.equal(profile.panelSchema.id, "xyzbc-trt-switchkins-pyvcp"); assert.ok(profile.halPins.includes("xyzbc-trt-kins.x-offset")); assert.ok(profile.halPins.includes("motion.switchkins-type")); assert.deepEqual(profile.hal.halcmd.feedbackNets.map((net) => net.signal), [ "table-x", "saddle-y", "spindle-z", "tilt-b", "rotate-c", ]); assert.deepEqual(profile.hal.halcmd.offsetNets.map((net) => net.target), [ "xyzbc-trt-kins.tool-offset", "xyzbc-trt-gui.tool-offset", "xyzbc-trt-gui.x-offset", "xyzbc-trt-gui.z-offset", ]); assert.deepEqual(profile.samplePrograms, [ "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc", "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc", ]); assert.ok(profile.remaps.some((remap) => remap.code === "M428" && remap.switchkinsType === 1)); assert.ok(profile.remaps.some((remap) => remap.code === "M429" && remap.switchkinsType === 0)); assert.ok(profile.remaps.some((remap) => remap.code === "M430" && remap.switchkinsType === 2)); const iniText = await readFile( new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", import.meta.url), "utf8", ); const ini = parseLinuxCncIni(iniText, { path: profile.iniPath, profileId: profile.id, }); assert.equal(ini.validation.ready, true); assert.equal(ini.machineName, "sim-xyzbc-trt-kins (switchkins)"); assert.equal(ini.kinematics.name, "xyzbc-trt-kins"); assert.equal(ini.kinematicsModuleId, "xyzbc-trt"); assert.equal(ini.traj.coordinates, "XYZBC"); assert.equal(ini.rs274ngc.halPinVars, true); assert.equal(ini.rs274ngc.parameterFile, "xyzbc.var"); assert.equal(ini.emcio.toolTable, "xyzbc-trt.tbl"); assert.equal(ini.jointConfig[3].axis, "B"); assert.equal(ini.jointConfig[4].axis, "C"); assert.deepEqual(ini.halui.mdiCommands, ["M429", "M428", "M430"]); const storage = createMemorySessionStorage(); const staged = await stageProfileMachineFiles(profile, { storage }); assert.equal(staged.plan.profileId, "xyzbc-trt"); assert.equal(staged.plan.wasmDir, "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt"); assert.equal(staged.save.status, "saved"); assert.equal(staged.save.storageMode, "memory"); assert.ok(staged.save.opfsRoot.endsWith("/xyzbc-trt")); assert.ok(staged.save.files.every((file) => file.opfsPath.startsWith("web-rtcp-5axis-xyzbc-trt-sim-plan/machines/xyzbc-trt/"))); assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.ini") && file.kind === "ini")); assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.xml") && file.kind === "pyvcp")); assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.tbl") && file.kind === "toolTable")); assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc") && file.kind === "remap")); assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/429remap.ngc") && file.kind === "remap")); assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/430remap.ngc") && file.kind === "remap")); assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/xyzbc_switchkins_sub.ngc") && file.kind === "remap")); assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/centering.ngc") && file.kind === "remap")); assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/helix_bc.ngc") && file.kind === "remap")); assert.ok(staged.save.gcodeSources.some((source) => source.filename === "xyzbc_switchkins.ngc")); assert.ok(staged.save.gcodeSources.some((source) => source.filename === "boat-xyzbc.ngc")); assert.equal(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc.var") && file.kind === "parameters"), true); assert.equal(staged.save.files.find((file) => file.sourceRel.endsWith("xyzbc-trt.tbl")).text.includes("T2 P2 Z10 D8"), true); const selectedPlan = selectMachineFileProgram( staged.plan, staged.save, "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc", ); assert.equal(selectedPlan.selectedProgramFilename, "xyzbc_switchkins.ngc"); assert.ok(selectedPlan.wasmProgramPath.endsWith("/demos/xyzbc_switchkins.ngc")); const selectedProgramFile = staged.save.files.find((file) => file.sourceRel === selectedPlan.selectedProgramSourceRel); const semanticExecutionPath = buildAxisExecutionTraceFromProgram({ filename: selectedPlan.selectedProgramFilename, sourceRel: selectedPlan.selectedProgramSourceRel, content: selectedProgramFile.text, }); assert.equal(semanticExecutionPath.status, "ok"); assert.equal(semanticExecutionPath.samplePeriodMs, 50); assert.equal(semanticExecutionPath.sampleCount, 1300); assert.equal(semanticExecutionPath.segmentCount, 29); assert.equal(semanticExecutionPath.lineExecutionTrace.length, 64); assert.equal(semanticExecutionPath.axisValuesByLine.length, 29); assert.equal(semanticExecutionPath.gcodeExecutionProcess.status, "ok"); assert.equal(semanticExecutionPath.gcodeExecutionProcess.executionStepCount, 128); assert.equal(semanticExecutionPath.gcodeExecutionProcess.sourceLineCoverage.length, 65); assert.equal(semanticExecutionPath.gcodeExecutionProcess.summary.motionStepCount, 29); assert.equal(semanticExecutionPath.gcodeExecutionProcess.summary.parameterAssignmentStepCount, 41); assert.deepEqual(semanticExecutionPath.samples.find((sample) => sample.motionType === "arc").machineState.cutting, { active: true, cuttingSpeedMmPerMin: 1000, }); assert.equal(semanticExecutionPath.samples[0].machineState.spindle.speedRpm, 0); assert.equal(semanticExecutionPath.samples[0].machineState.coolant.flood, false); assert.equal(semanticExecutionPath.samples[0].machineState.toolChange.activeTool, 2); assert.equal(semanticExecutionPath.gcodeExecutionProcess.executionSteps.some((step) => ( step.sourceFile === "helix_bc.ngc" && step.line === 17 && step.result.operation === "feed-helix" && step.result.motion?.endJoint?.b === 20 && step.result.motion?.endJoint?.c === 45 && step.result.motion?.endJoint?.z === 5 && step.result.machineStateAfter?.cutting?.active === true && step.result.machineStateAfter?.feed?.actualMmPerMin === 1000 )), true); assert.equal(semanticExecutionPath.axisValuesByLine.some((entry) => ( entry.sourceFile === "helix_bc.ngc" && entry.line === 17 && entry.operation === "feed-helix" && entry.joint.b === 20 && entry.joint.c === 45 && entry.joint.z === 5 && entry.machineState.cutting.cuttingSpeedMmPerMin === 1000 )), true); const interpreter = await createLinuxCncInterpreterRuntime(); const execution = interpreter.runMachineFileProgram({ plan: selectedPlan, files: staged.save.files, executionMode: "fiveAxisRemap", }); assert.equal(execution.summary.machineFileExecutionReady, true); assert.equal(execution.plannerTiming.plannerRuntimeReady, true); assert.equal(execution.plannerTiming.samples.length > 0, true); const taskHalWasm = await readFile( new URL("../../../wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm", import.meta.url), ); const taskHal = wrapTaskHalSdk(await createLinuxCncTaskHalSdk({ wasmBinary: taskHalWasm, print() {}, printErr() {}, })); const taskHalSession = buildTaskHalSessionFromMachineFiles({ profile, plan: selectedPlan, save: staged.save, selectedProgramRel: selectedPlan.selectedProgramSourceRel, }); taskHal.initSession(taskHalSession); taskHal.stageFiles(taskHalSession.files); taskHal.openProgram(taskHalSession.programPath); taskHal.loadProgramMotionPlan(buildTaskHalProgramMotionPlan({ programPath: taskHalSession.programPath, motion: execution.motion, timing: execution.plannerTiming, linearUnits: execution.plannerTiming.linearUnits || "mm", programLines: staged.save.files .find((file) => (file.wasmPath || file.path) === taskHalSession.programPath) ?.text.split(/\r?\n/) || [], })); taskHal.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" }); taskHal.sendCommand({ type: "EMC_JOINT_HOME", joint: -1 }); taskHal.runCycles({ taskPeriodNs: 10000000, servoPeriodNs: 1000000, taskCycles: 1 }); taskHal.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" }); taskHal.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 }); taskHal.runCycles({ taskPeriodNs: 20000000, servoPeriodNs: 1000000, taskCycles: 3 }); const taskHalStatus = taskHal.readStatus(); assert.equal(taskHalStatus.summary.taskRuntimeReady, true); assert.equal(taskHalStatus.summary.halSyncReady, true); assert.equal(taskHalStatus.ui.activeLine >= 1, true); assert.equal(taskHalStatus.statusSource, "StandaloneEmcStatus"); assert.equal(taskHalStatus.schemaVersion, 1); assert.equal(taskHalStatus.emcStatus.source, "StandaloneEmcStatus"); assert.equal("task" in taskHalStatus, false); assert.equal("motionStatus" in taskHalStatus, false); assert.equal(taskHalStatus.ui.activeLine, taskHalStatus.emcStatus.task.currentLine); assert.equal(taskHalStatus.ui.motionProgramLine, taskHalStatus.emcStatus.motion.programLine); assert.equal(taskHalStatus.ui.motionQueueDepth, taskHalStatus.emcStatus.motion.traj.queue); assert.equal(taskHalStatus.ui.axisPose.x, taskHalStatus.emcStatus.motion.axisByName.x); assert.equal(Number.isFinite(taskHalStatus.ui.axisPose.x), true); assert.equal(Number.isFinite(taskHalStatus.ui.axisPose.y), true); assert.equal(Number.isFinite(taskHalStatus.ui.axisPose.z), true); const emcOnlyTaskHalStatus = normalizeTaskHalStatus({ statusSource: "StandaloneEmcStatus", schemaVersion: 1, emcStatus: { source: "StandaloneEmcStatus", task: { status: "EXEC", state: "ON", mode: "AUTO", interpState: "READING", execState: "EXEC", cycle: 17, programOpen: true, planId: 9, motionPlanLoaded: true, allHomed: true, currentLine: 12, readLine: 12, motionLine: 12, callLevel: 1, }, motion: { valid: true, status: "EXEC", programLine: 12, motionId: 44, enabled: true, commandQueueDepth: 2, activeDepth: 1, queueFull: false, joint0: { motorPosCmd: 10, motorPosFb: 9.5, }, traj: { enabled: true, inpos: false, queue: 2, activeQueue: 1, queueFull: false, id: 44, paused: false, singleStepping: false, actualPosition: { x: 10, y: 20, z: 30, b: 40, c: 50, }, currentVel: 1.25, }, axisByName: { x: 10, y: 20, z: 30, b: 40, c: 50, }, }, }, halSnapshot: { ready: true, pins: { "motion.program-line": { value: 12 }, "motion.switchkins-type": { value: 1 }, }, }, }); assert.equal(emcOnlyTaskHalStatus.summary.taskRuntimeReady, true); assert.equal(emcOnlyTaskHalStatus.summary.motionRuntimeReady, true); assert.equal(emcOnlyTaskHalStatus.summary.halSyncReady, true); assert.equal(emcOnlyTaskHalStatus.ui.taskMode, "auto"); assert.equal(emcOnlyTaskHalStatus.ui.interpState, "reading"); assert.equal(emcOnlyTaskHalStatus.ui.taskCycle, 17); assert.equal(emcOnlyTaskHalStatus.ui.activeLine, 12); assert.equal(emcOnlyTaskHalStatus.ui.motionQueueDepth, 2); assert.equal(emcOnlyTaskHalStatus.ui.motionId, 44); assert.equal(emcOnlyTaskHalStatus.ui.currentVelocity, 75); assert.deepEqual(emcOnlyTaskHalStatus.ui.axisPose, { x: 10, y: 20, z: 30, a: 0, b: 40, c: 50, }); assert.equal("task" in emcOnlyTaskHalStatus, false); assert.equal("motionStatus" in emcOnlyTaskHalStatus, false); assert.equal(emcOnlyTaskHalStatus.emcStatus.motion.joint0.motorPosFb, 9.5); assert.throws( () => normalizeTaskHalStatus({ task: { state: "ON", mode: "AUTO" }, motionStatus: { motion: { programLine: 1 }, axis: { x: 1 } }, }), /removed legacy fields/, ); const store = createSimulationStore(); const state = store.getState(); assert.equal(state.machineProfile, "xyzbc-trt"); assert.equal(state.profile.id, "xyzbc-trt"); assert.equal(state.sessionName, "xyzbc-trt-web-session"); assert.equal(state.kinematicsRuntimeReadiness, null); const storeStage = await store.stageMachineFiles({ storage: createMemorySessionStorage() }); assert.equal(store.getState().toolRuntimeState.ready, true); assert.equal(store.getState().toolRuntimeState.currentTool.toolNumber, 0); store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc", }); assert.equal(store.getState().toolRuntimeState.activeToolNumber, 0); assert.equal(store.getState().programAxisPreviewPath.source, "web-axis-source-execution-expanded-ngcgui-subroutines"); assert.equal(store.getState().programAxisPreviewPath.semanticBoundary, "linuxcnc_xyzbc_switchkins_ngc_execution_expanded_by_source_subroutines"); assert.equal(store.getState().programAxisPreviewPath.gcodeExecutionProcess.executionStepCount, 128); assert.equal(store.getState().programAxisPreviewPath.gcodeExecutionProcess.summary.motionStepCount, 29); assert.equal(store.getState().programAxisPreviewPath.gcodeExecutionProcess.summary.parameterAssignmentStepCount, 41); assert.equal(store.getState().programAxisPreviewPath.samples[0].tool.id, 2); assert.equal(store.getState().programAxisPreviewPath.samples[0].tool.diameter, 8); assert.equal(store.getState().programAxisPreviewPath.samples[0].sourceFile, "xyzbc_switchkins_sub.ngc"); assert.equal(store.getState().programAxisPreviewPath.samples[0].statement.includes("g53 g0"), true); store.dispatch({ type: "RUN_READY" }); assert.equal(store.getState().machine.powerOn, false); assert.equal(store.getState().machine.allHomed, false); assert.equal(store.getState().machine.mode, "manual"); assert.equal(store.getState().operatorMessage, "RUN setup ready: reset ESTOP, power on, Home All, then Run"); await store.dispatch({ type: "RUN_FROM_OPERATOR" }); assert.equal(store.getState().runState, "idle"); assert.equal(store.getState().operatorMessage, "run blocked: machine must be on"); store.dispatch({ type: "TOGGLE_POWER" }); await store.dispatch({ type: "RUN_FROM_OPERATOR" }); assert.equal(store.getState().runState, "idle"); assert.equal(store.getState().operatorMessage, "run blocked: home machine first"); store.dispatch({ type: "HOME" }); await store.dispatch({ type: "RUN_FROM_OPERATOR" }); const liveRunState = store.getState(); assert.equal(liveRunState.programUiExecution.source, "programAxisPreviewPath.samples"); assert.equal(liveRunState.programUiExecution.samplePeriodMs, 50); assert.equal(liveRunState.programUiExecution.executedSampleCount, liveRunState.programExecutionSampleIndex + 1); assert.equal(liveRunState.programUiExecution.sourceFile, liveRunState.programRuntimeFeedback.sourceFile); assert.equal(liveRunState.programUiExecution.line, liveRunState.programRuntimeFeedback.line); assert.equal(liveRunState.programUiExecution.gcodeStepIndex === null, false); assert.equal(liveRunState.programUiExecution.semanticBoundary, "ui_updates_from_real_expanded_linuxcnc_gcode_sample_stream"); assert.deepEqual(liveRunState.toolAxisVector, { x: liveRunState.programUiExecution.toolAxis.i, y: liveRunState.programUiExecution.toolAxis.j, z: liveRunState.programUiExecution.toolAxis.k, }); assert.deepEqual(liveRunState.programRuntimeFeedback.tcp, liveRunState.programUiExecution.tcp); assert.notDeepEqual(liveRunState.programRuntimeFeedback.tcp, liveRunState.programRuntimeFeedback.axisPose); assert.equal(liveRunState.linuxCncProcessMonitor.path.uiExecution.sourceFile, liveRunState.programUiExecution.sourceFile); assert.equal(liveRunState.linuxCncProcessMonitor.path.activeGcode, liveRunState.programUiExecution.statement); assert.equal(store.getState().linuxCncProcessMonitor.toolChange.activeToolNumber, 2); assert.equal(store.getState().linuxCncProcessMonitor.toolChange.diameter, 8); assert.equal(store.getState().linuxCncProcessMonitor.sourceReferences.task, "linuxcnc/src/emc/task/emctaskmain.cc"); const toolOffsetDb = applyToolCommandSequence(store.getState().toolDbSimulation, [ { code: "T", toolNumber: 2 }, { code: "M6" }, { code: "G43", h: 2 }, ]); const toolRuntimeState = createToolRuntimeState(toolOffsetDb, { fallbackToolLength: state.toolPreview.length, }); assert.equal(toolRuntimeState.ready, true); assert.equal(toolRuntimeState.activeOffsetApplied, true); assert.equal(toolRuntimeState.activeToolNumber, 2); assert.equal(toolRuntimeState.activePocket, 2); assert.equal(toolRuntimeState.currentTool.offset.z, 10); assert.equal(toolRuntimeState.currentTool.diameter, 8); assert.equal(toolRuntimeState.kinematics.toolOffsetZ, 10); assert.equal(toolRuntimeState.kinematics.halPin, "motion.tooloffset.z"); assert.deepEqual(toolRuntimeState.kinematics.downstreamPins, [ "xyzbc-trt-kins.tool-offset", "xyzbc-trt-gui.tool-offset", ]); assert.equal(toolRuntimeState.pathTool.id, 2); assert.equal(toolRuntimeState.pathTool.pocket, 2); assert.equal(toolRuntimeState.pathTool.length, 10); assert.equal(toolRuntimeState.pathTool.diameter, 8); assert.equal(toolRuntimeState.vismach.toolOffset, 10); const vismachState = buildVismachModelState({ ...state, axisPose: { x: 12.5, y: -6.25, z: 3.75, b: 35, c: -90 }, toolPreview: { ...state.toolPreview, length: 84.019 }, toolRuntimeState, }); assert.deepEqual(Object.keys(vismachState.pins), XYZBC_TRT_VISMACH_PINS); assert.equal(vismachState.pins["table-x"], 12.5); assert.equal(vismachState.pins["saddle-y"], -6.25); assert.equal(vismachState.pins["spindle-z"], 3.75); assert.equal(vismachState.pins["tilt-b"], 35); assert.equal(vismachState.pins["rotate-c"], -90); assert.equal(vismachState.pins["x-offset"], -20); assert.equal(vismachState.pins["z-offset"], -15); assert.equal(vismachState.pins["tool-offset"], 10); assert.deepEqual(vismachState.transforms.table.sourcePins, ["table-x"]); assert.deepEqual(vismachState.transforms.saddle.sourcePins, ["saddle-y"]); assert.deepEqual(vismachState.transforms.spindle.sourcePins, ["spindle-z"]); assert.deepEqual(vismachState.transforms.tilt.sourcePins, ["tilt-b"]); assert.deepEqual(vismachState.transforms.rotary.sourcePins, ["rotate-c"]); assert.deepEqual(vismachState.transforms.tool.sourcePins, ["tool-offset", "x-offset", "z-offset"]); assert.equal(vismachState.transforms.tool.translate.x, -20); assert.equal(vismachState.transforms.tool.translate.z, -25); assert.equal(vismachState.halNets.length, 9); assert.equal(vismachState.semanticBoundary, "web_threejs_vismach_equivalent_driven_by_xyzbc_trt_hal_pins"); const pausedVismachState = buildVismachModelState({ ...state, runState: "paused", machine: { ...state.machine, interpState: "paused", taskPaused: true }, axisPose: { x: 12.5, y: -6.25, z: 3.75, b: 35, c: -90 }, programRuntimeFeedback: { axisPose: { x: 99, y: 88, z: 77, b: 66, c: 55 }, }, }); assert.equal(pausedVismachState.pins["table-x"], 12.5); assert.equal(pausedVismachState.pins["saddle-y"], -6.25); assert.equal(pausedVismachState.pins["spindle-z"], 3.75); assert.equal(pausedVismachState.pins["tilt-b"], 35); assert.equal(pausedVismachState.pins["rotate-c"], -90); const stagedState = store.getState(); assert.equal(storeStage.save.profileId, "xyzbc-trt"); assert.equal(stagedState.machineFileStaging.profileId, "xyzbc-trt"); assert.ok(stagedState.machineProject.projectRoot.startsWith("web-rtcp-5axis-xyzbc-trt-sim-plan/machines/xyzbc-trt")); assert.ok(stagedState.machineFileStaging.gcodeSources.some((source) => source.filename === "xyzbc_switchkins.ngc")); assert.equal(AXIS_BUTTON_PARITY.length >= 50, true); for (const action of [ "estop", "power", "run-ready", "run", "pause", "resume", "pause-resume", "step", "stop", "home-all", "jog-plus", "jog-minus", "spindle-reverse", "spindle-stop", "spindle-forward", "kins-identity", "kins-tcp", "kins-userk", "clear-preview", ]) { assert.ok(AXIS_BUTTON_PARITY.some((item) => item.action === action && item.sourceSymbol && item.sourceLines && item.expected), action); } const buttonStore = createSimulationStore(); buttonStore.dispatch({ type: "ESTOP" }); assert.equal(buttonStore.getState().machine.estopActive, true); buttonStore.dispatch({ type: "TOGGLE_POWER" }); assert.equal(buttonStore.getState().machine.taskState, "estop"); assert.equal(buttonStore.getState().machine.powerOn, false); assert.equal(buttonStore.getState().operatorMessage, "power blocked: reset ESTOP first"); buttonStore.dispatch({ type: "TOGGLE_POWER" }); assert.equal(buttonStore.getState().machine.taskState, "estop"); assert.equal(buttonStore.getState().machine.powerOn, false); await buttonStore.dispatch({ type: "RUN_FROM_OPERATOR" }); assert.equal(buttonStore.getState().runState, "estopped"); assert.equal(buttonStore.getState().operatorMessage, "run blocked: machine must be on"); buttonStore.dispatch({ type: "ESTOP" }); buttonStore.dispatch({ type: "RESET" }); assert.equal(buttonStore.getState().machine.taskState, "estop-reset"); await buttonStore.dispatch({ type: "RUN_FROM_OPERATOR" }); assert.equal(buttonStore.getState().runState, "idle"); assert.equal(buttonStore.getState().operatorMessage, "run blocked: machine must be on"); buttonStore.dispatch({ type: "TOGGLE_POWER" }); assert.equal(buttonStore.getState().machine.powerOn, true); await buttonStore.dispatch({ type: "RUN_FROM_OPERATOR" }); assert.equal(buttonStore.getState().runState, "idle"); assert.equal(buttonStore.getState().operatorMessage, "run blocked: home machine first"); buttonStore.dispatch({ type: "HOME" }); assert.equal(buttonStore.getState().machine.allHomed, true); assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.x, 0); assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.y, 0); assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.z, 10); await buttonStore.dispatch({ type: "RUN_FROM_OPERATOR" }); assert.equal(buttonStore.getState().machine.mode, "auto"); assert.equal(["running", "complete"].includes(buttonStore.getState().runState), true); assert.equal(Boolean(buttonStore.getState().programRuntimeFeedback), true); if (buttonStore.getState().runState === "running") { buttonStore.dispatch({ type: "PAUSE_RESUME" }); assert.equal(buttonStore.getState().runState, "paused"); assert.equal(buttonStore.getState().machine.interpState, "paused"); assert.equal(buttonStore.getState().feed.currentVelocity, 0); buttonStore.dispatch({ type: "PAUSE_RESUME" }); assert.equal(buttonStore.getState().runState, "running"); assert.equal(buttonStore.getState().machine.interpState, "reading"); buttonStore.dispatch({ type: "PAUSE_RESUME" }); assert.equal(buttonStore.getState().runState, "paused"); assert.equal(buttonStore.getState().machine.interpState, "paused"); assert.equal(buttonStore.getState().feed.currentVelocity, 0); const beforeStepSample = buttonStore.getState().programExecutionSampleIndex; buttonStore.dispatch({ type: "STEP" }); assert.equal(buttonStore.getState().runState, "stepping"); assert.equal(buttonStore.getState().machine.interpState, "paused"); assert.equal(buttonStore.getState().programExecutionSampleIndex > beforeStepSample, true); buttonStore.dispatch({ type: "RESUME" }); assert.equal(buttonStore.getState().runState, "running"); assert.equal(buttonStore.getState().machine.interpState, "reading"); buttonStore.dispatch({ type: "PAUSE" }); assert.equal(buttonStore.getState().runState, "paused"); assert.equal(buttonStore.getState().machine.interpState, "paused"); assert.equal(buttonStore.getState().feed.currentVelocity, 0); buttonStore.dispatch({ type: "RESUME" }); assert.equal(buttonStore.getState().runState, "running"); assert.equal(buttonStore.getState().machine.interpState, "reading"); } buttonStore.dispatch({ type: "STOP" }); buttonStore.dispatch({ type: "TOGGLE_POWER" }); assert.equal(buttonStore.getState().machine.taskState, "estop-reset"); assert.equal(buttonStore.getState().machine.powerOn, false); assert.equal(buttonStore.getState().machine.allHomed, false); assert.deepEqual(buttonStore.getState().machine.homed, [false, false, false, false, false]); buttonStore.dispatch({ type: "TOGGLE_POWER" }); assert.equal(buttonStore.getState().machine.taskState, "on"); assert.equal(buttonStore.getState().machine.powerOn, true); const idleInterpRunningStore = createSimulationStore({ runState: "running", machine: { powerOn: true, estopActive: false, taskState: "on", mode: "auto", interpState: "idle", interpResumeState: "reading", taskPaused: false, allHomed: true, noForceHoming: false, }, }); idleInterpRunningStore.dispatch({ type: "PAUSE" }); assert.equal(idleInterpRunningStore.getState().runState, "running"); assert.equal(idleInterpRunningStore.getState().machine.interpState, "idle"); assert.equal(idleInterpRunningStore.getState().machine.taskPaused, false); assert.equal(idleInterpRunningStore.getState().operatorMessage, "pause blocked: interpreter is not running"); idleInterpRunningStore.dispatch({ type: "PAUSE_RESUME" }); assert.equal(idleInterpRunningStore.getState().runState, "running"); assert.equal(idleInterpRunningStore.getState().machine.interpState, "idle"); assert.equal(idleInterpRunningStore.getState().operatorMessage, "pause ignored: interpreter is idle"); const staleManualRunningStore = createSimulationStore({ runState: "running", machine: { powerOn: true, estopActive: false, taskState: "on", mode: "manual", interpState: "reading", interpResumeState: "reading", taskPaused: false, allHomed: true, noForceHoming: false, }, }); staleManualRunningStore.dispatch({ type: "PAUSE_RESUME" }); assert.equal(staleManualRunningStore.getState().runState, "running"); assert.equal(staleManualRunningStore.getState().machine.mode, "manual"); assert.equal(staleManualRunningStore.getState().machine.interpState, "reading"); assert.equal(staleManualRunningStore.getState().machine.taskPaused, false); assert.equal(staleManualRunningStore.getState().operatorMessage, "pause blocked: task mode must be auto or MDI"); assert.equal(staleManualRunningStore.getState().taskHalPauseLock, null); const lockedPauseState = staleManualRunningStore.getState(); staleManualRunningStore.dispatch({ type: "TASK_HAL_STATUS_APPLIED", operatorMessage: "simulated stale running task/HAL status", status: { statusSource: "StandaloneEmcStatus", schemaVersion: 1, emcStatus: { source: "StandaloneEmcStatus", task: { state: "ON", mode: "AUTO", interpState: "READING", execState: "EXEC", cycle: 123, taskCycle: 123, programOpen: true, openedLineCount: 999, openedSourceLineCount: 999, nextProgramLine: Number(lockedPauseState.activeLine || 1) + 5, }, motion: { valid: true, programLine: Number(lockedPauseState.activeLine || 1) + 5, currentVel: 12, requestedVel: 12, inPosition: false, axisByName: { x: 99, y: 88, z: 77, b: 66, c: 55, }, traj: { queue: 1, actualPosition: { x: 99, y: 88, z: 77, b: 66, c: 55, }, }, }, }, ui: { taskState: "on", taskMode: "auto", interpState: "reading", execState: "exec", activeLine: Number(lockedPauseState.activeLine || 1) + 5, motionProgramLine: Number(lockedPauseState.activeLine || 1) + 5, activeLineSource: "emc-status", taskCycle: 123, servoCycle: 25, axisPose: { x: 99, y: 88, z: 77, b: 66, c: 55, }, currentVelocity: 720, }, halSnapshot: { ready: true, pins: { "motion.program-line": { value: Number(lockedPauseState.activeLine || 1) + 5 }, }, }, }, }); assert.equal(staleManualRunningStore.getState().runState, "running"); assert.equal(staleManualRunningStore.getState().machine.mode, "auto"); assert.equal(staleManualRunningStore.getState().machine.interpState, "reading"); assert.equal(staleManualRunningStore.getState().taskHalPauseLock, null); const controlStore = createSimulationStore(); controlStore.dispatch({ type: "ESTOP" }); controlStore.dispatch({ type: "RESET" }); controlStore.dispatch({ type: "TOGGLE_POWER" }); controlStore.dispatch({ type: "HOME" }); controlStore.dispatch({ type: "SET_MODE", mode: "manual" }); controlStore.dispatch({ type: "SET_MODE", mode: "mdi" }); controlStore.dispatch({ type: "RUN_MDI", command: "M428" }); assert.equal(controlStore.getState().kinsType, "tcp-xyzbc"); assert.equal(controlStore.getState().linuxCncProcessMonitor.axes.rtcpState, "on"); controlStore.dispatch({ type: "RUN_MDI", command: "M429" }); assert.equal(controlStore.getState().kinsType, "identity"); controlStore.dispatch({ type: "RUN_MDI", command: "M430" }); assert.equal(controlStore.getState().kinsType, "userk"); controlStore.dispatch({ type: "SET_MODE", mode: "manual" }); controlStore.dispatch({ type: "SET_ACTIVE_JOINT", joint: 4 }); const cBeforeJog = controlStore.getState().axisPose.c; controlStore.dispatch({ type: "JOG", axis: "c", direction: 1, increment: 1 }); assert.equal(controlStore.getState().axisPose.c, cBeforeJog + 1); controlStore.dispatch({ type: "RUN_MDI", command: "G10 L20 P0 C0", manualTouchOff: true }); assert.equal(controlStore.getState().mdiHistory[0], "G10 L20 P0 C0"); assert.equal(controlStore.getState().machine.mode, "manual"); assert.equal(controlStore.getState().runState, "idle"); assert.equal(controlStore.getState().operatorMessage, "manual touch off G10 L20 P0 C0"); controlStore.dispatch({ type: "RUN_MDI", command: "G43", manualTouchOff: true }); assert.equal(controlStore.getState().mdiHistory[0], "G43"); assert.equal(controlStore.getState().machine.mode, "manual"); assert.equal(controlStore.getState().runState, "idle"); const feedBefore = controlStore.getState().feed.feedOverride; controlStore.dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: 10 }); assert.equal(controlStore.getState().feed.feedOverride, feedBefore + 10); assert.equal(controlStore.getState().linuxCncProcessMonitor.feed.feedOverridePercent, feedBefore + 10); controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "reverse" }); assert.equal(controlStore.getState().spindle.direction, "reverse"); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.enabled, true); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 1); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 0); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 1); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 1600); controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" }); assert.equal(controlStore.getState().spindle.direction, "stop"); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.enabled, false); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 0); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 0); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 0); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 0); controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" }); assert.equal(controlStore.getState().spindle.direction, "forward"); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.enabled, true); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.actualRpm, 1600); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 1); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 1); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 0); assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 1600); controlStore.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" }); assert.equal(controlStore.getState().coolant.flood, true); assert.equal(controlStore.getState().linuxCncProcessMonitor.coolant.flood, true); controlStore.dispatch({ type: "SET_BLOCK_DELETE", enabled: true }); assert.equal(controlStore.getState().gmoccapyGui.optionalBlocks, true); controlStore.dispatch({ type: "SET_OPTIONAL_STOP", enabled: true }); assert.equal(controlStore.getState().gmoccapyGui.optionalStop, true); controlStore.dispatch({ type: "SET_IGNORE_LIMITS", enabled: true }); assert.equal(controlStore.getState().gmoccapyGui.ignoreLimits, true); controlStore.dispatch({ type: "SET_VIEW", view: "x" }); assert.equal(controlStore.getState().preview.selectedView, "x"); controlStore.dispatch({ type: "CLEAR_PREVIEW" }); assert.equal(controlStore.getState().preview.pathPoints, 0); const stagingPlan = await createMachineFileStagingPlan({ profile }); assert.equal(stagingPlan.summary.remapFileCount >= 3, true); assert.equal(stagingPlan.files.some((file) => file.sourceRel.endsWith("xyzbc.var")), true); console.log("xyzbc_trt_web_app_smoke=ok");