import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { createMemorySessionStorage } from "../app/src/runtime/five-axis-session.js"; import { parseLinuxCncIni } from "../app/src/runtime/linuxcnc-ini-runtime.js"; import { selectMachineFileProgram, stageProfileMachineFiles, } from "../app/src/runtime/linuxcnc-machine-file-staging.js"; import { applyToolCommandSequence, createToolDbSimulation, createToolRuntimeState, extractToolCommandSequenceFromProgram, parseLinuxCncToolTable, } from "../app/src/runtime/tool-db-simulation.js"; import { createSimulationStore } from "../app/src/state/store.js"; import { getFiveAxisProfile } from "../app/src/profiles/index.js"; import { buildVismachModelState } from "../app/src/runtime/vismach-model-state.js"; const SAMPLE_PERIOD_MS = 20; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan"); const outputPath = process.argv[2] || resolve(projectRoot, "working/evidence/web-xyzbc-trt-evidence.json"); const sourceRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc"; const profile = getFiveAxisProfile("xyzbc-trt"); const wasmArtifacts = await inspectWasmArtifacts(); const iniText = await readFile( resolve(repoRoot, "wasm-port/vendor/linuxcnc", profile.iniPath), "utf8", ); const ini = parseLinuxCncIni(iniText, { path: profile.iniPath, profileId: profile.id, }); const storage = createMemorySessionStorage(); const staged = await stageProfileMachineFiles(profile, { storage }); const selectedPlan = selectMachineFileProgram(staged.plan, staged.save, sourceRel); const store = createSimulationStore(); const storeStage = await store.stageMachineFiles({ storage: createMemorySessionStorage() }); store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel }); const state = store.getState(); const paths = await collectPathEvidence({ profile, staged, selectedPlan, wasmArtifacts, }); const toolRuntime = buildToolRuntimeEvidence({ profile, staged, selectedPlan, fallbackToolLength: state.toolPreview?.length, }); const taskHalEquivalence = buildTaskHalEquivalenceEvidence({ state, profile, paths }); const ngcguiExecution = await collectNgcguiExecutionEvidence({ profile, staged, wasmArtifacts }); const semanticFields = buildSemanticFields({ profile, ini, staged, state, toolRuntime, taskHalEquivalence, ngcguiExecution, }); const axisMainUi = buildAxisMainUiEvidence({ state, profile, paths, toolRuntime, semanticFields }); const evidence = { apiName: "xyzbc-trt-web-opfs-wasm-evidence", status: wasmArtifacts.ready ? "ready-for-wasm-runtime" : "blocked", collectedAt: new Date().toISOString(), profile: { id: profile.id, machineName: profile.machineName, iniPath: profile.iniPath, pyvcpXmlPath: profile.pyvcpXmlPath, toolTablePath: profile.toolTablePath, coordinates: profile.coordinates, kinematics: profile.kinematics, kinematicsModuleId: profile.kinematicsModuleId, defaultProgramFilename: profile.machineFileStaging?.defaultProgramFilename, samplePrograms: profile.samplePrograms, switchkinsTypes: profile.kinematicsParameters?.switchkinsTypes, halPins: profile.halPins, }, ini: { ready: ini.validation.ready, machineName: ini.machineName, coordinates: ini.traj.coordinates, kinematicsName: ini.kinematics.name, kinematicsModuleId: ini.kinematicsModuleId, remaps: ini.rs274ngc.remaps, haluiMdiCommands: ini.halui.mdiCommands, toolTable: ini.emcio.toolTable, parameterFile: ini.rs274ngc.parameterFile, jointConfig: ini.jointConfig, display: ini.display, traj: ini.traj, axisLimits: ini.axisLimits, hal: ini.hal, }, opfsStaging: { storageMode: staged.save.storageMode, opfsRoot: staged.save.opfsRoot, fileCount: staged.save.fileCount, summary: staged.save.summary, files: staged.save.files.map((file) => ({ sourceRel: file.sourceRel, opfsPath: file.opfsPath, wasmPath: file.wasmPath, kind: file.kind, bytes: file.bytes, executable: file.executable, })), gcodeSources: staged.save.gcodeSources, selectedProgram: { sourceRel: selectedPlan.selectedProgramSourceRel, filename: selectedPlan.selectedProgramFilename, wasmProgramPath: selectedPlan.wasmProgramPath, }, }, store: { machineProfile: state.machineProfile, sessionName: state.sessionName, machineProjectRoot: state.machineProject?.projectRoot || storeStage.save.opfsRoot, activeProgram: state.activeProgram, programSource: state.programSource, programLineCount: state.programLines.length, selectedGcodeSourceRel: state.machineFileStaging.selectedGcodeSourceRel, }, pathSampling: createPathSampling(), previewPath: paths.previewPath, executionPath: paths.executionPath, toolRuntime, taskHalEquivalence, basicSimEquivalent: taskHalEquivalence.basicSimEquivalent, ngcguiExecution, axisMainUi, ...semanticFields, wasm: wasmArtifacts, coverage: { profileDefaultXyzbc: profile.id === "xyzbc-trt", iniReady: ini.validation.ready, opfsStaged: staged.save.status === "saved" && staged.save.fileCount > 0, pyvcpXmlStaged: staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.xml")), remapsStaged: ["428remap.ngc", "429remap.ngc", "430remap.ngc"].every((name) => ( staged.save.files.some((file) => file.sourceRel.endsWith(`/remap_subs/${name}`)) )), toolTableStaged: staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.tbl")), parameterFileStaged: staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc.var")), defaultProgramStaged: staged.save.gcodeSources.some((source) => source.filename === "xyzbc_switchkins.ngc"), boatProgramStaged: staged.save.gcodeSources.some((source) => source.filename === "boat-xyzbc.ngc"), ngcguiSubroutinesStaged: ["xyzbc_switchkins_sub.ngc", "centering.ngc", "helix_bc.ngc"].every((name) => ( staged.save.files.some((file) => file.sourceRel.endsWith(`/remap_subs/${name}`)) )), postguiHalEquivalent: semanticFields.halNets.some((net) => net.source === "pyvcp.type1-button" && net.target === "halui.mdi-command-01"), kinematicsPinsCovered: semanticFields.kinematicsPins.xOffset === -20 && semanticFields.kinematicsPins.zOffset === -15 && semanticFields.kinematicsPins.conventionalDirections === 0, axisJointLimitsCovered: semanticFields.axisJointLimits.coordinates === "XYZBC" && semanticFields.axisJointLimits.jointCount === 5 && Boolean(semanticFields.axisJointLimits.axisLimits.B) && Boolean(semanticFields.axisJointLimits.axisLimits.C), previewPathAvailable: paths.previewPath.sampleCount > 0, executionPathAvailable: paths.executionPath.sampleCount > 0, axisMainUiEquivalent: axisMainUi.ready, basicSimEquivalent: taskHalEquivalence.ready === true, ngcguiSubroutinesExecutable: ngcguiExecution.ready === true, nativeStateFlowRechecked: semanticFields.nativeStateFlowReview?.ready === true, toolTableToToolOffsetClosed: toolRuntime.ready && toolRuntime.toolTable.toolCount > 0 && toolRuntime.activeOffsetApplied && toolRuntime.kinematics.toolOffsetZ === toolRuntime.pathTool.length && toolRuntime.vismach.toolOffset === toolRuntime.pathTool.length, wasmArtifactsReady: wasmArtifacts.ready, }, blockers: [ ...(wasmArtifacts.ready ? [] : [{ id: "missing-wasm-artifacts", detail: "wasm-port/build/wasm does not contain all required kinematics/core/tp/task-hal artifacts.", required: wasmArtifacts.required, missing: wasmArtifacts.missing, }]), ...(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc.var")) ? [] : [{ id: "missing-parameter-file-staging", detail: "xyzbc.var is referenced by INI but absent from wasm-port/vendor manifest in this workspace.", }]), ...(paths.previewPath.sampleCount > 0 ? [] : [{ id: "web-preview-path-unavailable", detail: paths.previewPath.unavailableReason, }]), ...(paths.executionPath.sampleCount > 0 ? [] : [{ id: "web-execution-path-unavailable", detail: paths.executionPath.unavailableReason, }]), ...(taskHalEquivalence.ready ? [] : [{ id: "web-basic-sim-equivalence-incomplete", detail: taskHalEquivalence.unavailableReason, }]), ...(ngcguiExecution.ready ? [] : [{ id: "web-ngcgui-execution-incomplete", detail: ngcguiExecution.unavailableReason, }]), ], semanticBoundary: "web_opfs_wasm_runtime_readiness_for_linuxcnc_xyzbc_trt", }; await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, JSON.stringify(evidence, null, 2) + "\n", "utf8"); console.log(`web_xyzbc_trt_evidence=${outputPath}`); async function inspectWasmArtifacts() { const required = [ "wasm-port/build/wasm/kinematics/linuxcnc_xyzbc_trt_kinematics.js", "wasm-port/build/wasm/kinematics/linuxcnc_xyzbc_trt_kinematics.wasm", "wasm-port/build/wasm/core/linuxcnc_interp.js", "wasm-port/build/wasm/core/linuxcnc_interp.wasm", "wasm-port/build/wasm/tp/linuxcnc_tp.js", "wasm-port/build/wasm/tp/linuxcnc_tp.wasm", "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.js", "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm", ]; const files = []; const missing = []; for (const rel of required) { const abs = resolve(repoRoot, rel); try { await access(abs); files.push(rel); } catch { missing.push(rel); } } return { required, files, missing, ready: missing.length === 0, emscriptenAvailable: Boolean(await commandExists("emcc")), }; } async function commandExists(command) { const { spawn } = await import("node:child_process"); return new Promise((resolveCommand) => { const child = spawn("bash", ["-lc", `command -v ${command}`], { stdio: "ignore" }); child.on("exit", (code) => resolveCommand(code === 0)); }); } async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifacts }) { if (!wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.js") || !wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.wasm")) { return { previewPath: emptyPath("web-preview", "missing linuxcnc_interp WASM artifacts"), executionPath: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"), }; } try { const { createLinuxCncInterpreterRuntime } = await import("../app/src/runtime/linuxcnc-interpreter-runtime.js"); const runtime = await createLinuxCncInterpreterRuntime(); const execution = runtime.runMachineFileProgram({ plan: selectedPlan, files: staged.save.files, executionMode: "fiveAxisRemap", }); const toolRuntime = buildToolRuntimeEvidence({ profile, staged, selectedPlan, }); const previewPath = pathFromWebMotion(execution, profile, toolRuntime.pathTool); return { previewPath, executionPath: wasmArtifacts.ready ? await pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime }) : emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"), }; } catch (error) { return { previewPath: emptyPath("web-preview", error instanceof Error ? error.message : String(error)), executionPath: emptyPath("web-task-hal", "preview runtime failed before task/HAL execution capture"), }; } } function pathFromWebMotion(execution, profile, pathTool = null) { const plannerSamples = execution.plannerTiming?.samples || []; const motion = execution.motion || []; const motionByIndex = new Map(motion.map((event, index) => [index, event])); const resampled = resamplePlannerSamples(plannerSamples, SAMPLE_PERIOD_MS); const samples = resampled.map((sample, index) => { const event = motionByIndex.get(sample.motionIndex) || {}; const axes = sample.axes || event.axes || {}; return normalizePathSample({ sampleIndex: index, timeMs: sample.timeMs, line: sample.line ?? event.line ?? 0, motionType: motionTypeFromCanonical(sample.type || event.type), activeKinematics: activeKinematics(event), axes, tool: pathTool || firstTool(profile), feed: sample.currentVelocityMmPerMin ?? event.feedRate ?? 0, spindle: 0, }); }); return { source: "web-linuxcnc-interpreter-preview", samplePeriodMs: SAMPLE_PERIOD_MS, status: samples.length > 0 ? "ok" : "blocked", unavailableReason: samples.length > 0 ? null : "interpreter produced no planner samples", sampleCount: samples.length, samples, }; } function resamplePlannerSamples(plannerSamples = [], samplePeriodMs = SAMPLE_PERIOD_MS) { if (!Array.isArray(plannerSamples) || plannerSamples.length === 0) return []; const normalized = plannerSamples .map((sample) => ({ ...sample, timeMs: Math.round(Number(sample.timeSeconds || 0) * 1000), })) .filter((sample) => Number.isFinite(sample.timeMs)) .sort((left, right) => left.timeMs - right.timeMs); if (normalized.length === 0) return []; const firstMs = 0; const lastMs = normalized.at(-1).timeMs; const output = []; let rightIndex = 0; for (let timeMs = firstMs; timeMs <= lastMs; timeMs += samplePeriodMs) { while (rightIndex < normalized.length - 1 && normalized[rightIndex].timeMs < timeMs) { rightIndex += 1; } const right = normalized[rightIndex]; const left = normalized[Math.max(0, rightIndex - 1)] || right; output.push(interpolatePlannerSample(left, right, timeMs)); } return output; } function interpolatePlannerSample(left, right, timeMs) { if (!left || !right || left.timeMs === right.timeMs) { return { ...(right || left), timeMs }; } const ratio = Math.max(0, Math.min(1, (timeMs - left.timeMs) / (right.timeMs - left.timeMs))); const axes = {}; for (const axis of ["x", "y", "z", "a", "b", "c", "u", "v", "w"]) { axes[axis] = lerpNumber(left.axes?.[axis], right.axes?.[axis], ratio); } return { ...right, timeMs, axes, currentVelocityMmPerMin: lerpNumber(left.currentVelocityMmPerMin, right.currentVelocityMmPerMin, ratio), currentVelocity: lerpNumber(left.currentVelocity, right.currentVelocity, ratio), distanceToGo: lerpNumber(left.distanceToGo, right.distanceToGo, ratio), }; } async function pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime = null }) { try { const { createLinuxCncTaskHalSdk } = await import("../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js"); const { buildTaskHalProgramMotionPlan, buildTaskHalSessionFromMachineFiles, wrapTaskHalSdk, } = await import("../app/src/runtime/linuxcnc-task-hal-runtime.js"); const wasmBinary = await readFile(resolve(repoRoot, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm")); const taskHal = wrapTaskHalSdk(await createLinuxCncTaskHalSdk({ wasmBinary, print() {}, printErr() {}, })); const session = buildTaskHalSessionFromMachineFiles({ profile, plan: selectedPlan, save: staged.save, selectedProgramRel: selectedPlan.selectedProgramSourceRel, }); const programFile = staged.save.files.find((file) => ( (file.wasmPath || file.path) === session.programPath )); const programLines = String(programFile?.text || "").split(/\r?\n/); taskHal.initSession(session); taskHal.stageFiles(session.files); taskHal.openProgram(session.programPath); taskHal.loadProgramMotionPlan(buildTaskHalProgramMotionPlan({ programPath: session.programPath, motion: execution.motion, timing: execution.plannerTiming, linearUnits: execution.plannerTiming?.linearUnits || "mm", programLines, })); taskHal.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" }); taskHal.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" }); taskHal.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 }); const totalSeconds = Number(execution.plannerTiming?.totalSeconds || 0); const cycleCount = Math.max( Math.ceil((totalSeconds * 1000) / SAMPLE_PERIOD_MS) + 5, Number(execution.plannerTiming?.samples?.length || 0), 1, ); const samples = []; let previousKey = null; let completed = false; for (let index = 0; index < cycleCount; index += 1) { taskHal.runCycles({ taskPeriodNs: SAMPLE_PERIOD_MS * 1000000, servoPeriodNs: 1000000, taskCycles: 1, }); const status = taskHal.readStatus(); const motion = status.motionStatus?.motion || {}; const task = status.task || {}; const activeLine = status.ui?.activeLine ?? motion.programLine ?? 0; const sample = normalizePathSample({ sampleIndex: samples.length, timeMs: samples.length * SAMPLE_PERIOD_MS, line: activeLine, motionType: motionTypeFromCanonical(currentMotionTypeForLine(execution.motion, activeLine)), activeKinematics: activeKinematics({ switchkinsType: status.ui?.switchkinsType ?? motion.switchkinsType, }), axes: xyzbcAxesFromTaskHalStatus(status), tool: toolRuntime?.pathTool || firstTool(profile), feed: status.ui?.currentVelocity ?? Number(motion.currentVel || motion.currentVelocity || 0) * 60, spindle: spindleFromTaskHalStatus(status), }); const sampleKey = JSON.stringify({ line: sample.line, joint: sample.joint, feed: sample.feed }); samples.push(sample); completed = String(task.execState || "").toUpperCase() === "DONE" && String(task.interpState || "").toUpperCase() === "IDLE" && samples.length > 2 && sampleKey === previousKey; previousKey = sampleKey; if (completed) break; } return { source: "web-linuxcnc-task-hal-execution", samplePeriodMs: SAMPLE_PERIOD_MS, status: samples.length > 0 ? "ok" : "blocked", unavailableReason: samples.length > 0 ? null : "task/HAL execution produced no status samples", sampleCount: samples.length, samples, taskHal: { semanticBoundary: "linuxcnc_task_motion_hal_wasm_simulation_runtime", sessionProgramPath: session.programPath, completed, eventCount: taskHal.readEvents()?.events?.length || 0, }, }; } catch (error) { return emptyPath("web-task-hal", error instanceof Error ? error.message : String(error)); } } async function collectNgcguiExecutionEvidence({ profile, staged, wasmArtifacts }) { const filenames = ["xyzbc_switchkins_sub.ngc", "centering.ngc", "helix_bc.ngc"]; if (!wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.js") || !wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.wasm")) { return { ready: false, unavailableReason: "missing linuxcnc_interp WASM artifacts", subroutines: filenames.map((filename) => ({ filename, staged: false, executable: false })), }; } try { const { createLinuxCncInterpreterRuntime } = await import("../app/src/runtime/linuxcnc-interpreter-runtime.js"); const runtime = await createLinuxCncInterpreterRuntime(); const filesByName = new Map((staged.save.files || []).map((file) => [file.filename || file.sourceRel?.split("/").at(-1), file])); const wrappers = [ { filename: "xyzbc_switchkins_sub.ngc", wrapper: "o call [10] [5] [10] [1000] [3] [0] [20] [45] [20]\nM2\n", }, { filename: "centering.ngc", wrapper: "o call [-2.5] [-2.5] [5] [5] [60] [12] [1000]\nM2\n", }, { filename: "helix_bc.ngc", wrapper: "o call [10] [5] [10] [1000] [3] [0] [20] [45]\nM2\n", }, ]; const subroutines = wrappers.map((item) => { const file = filesByName.get(item.filename); const wrapperFilename = `ngcgui-wrapper-${item.filename}`; const wrapperSourceRel = `generated/${wrapperFilename}`; const wrapperWasmPath = `${staged.plan.wasmDir}/${wrapperFilename}`; const files = [ ...staged.save.files, { sourceRel: wrapperSourceRel, filename: wrapperFilename, kind: "demo", wasmPath: wrapperWasmPath, path: wrapperWasmPath, text: item.wrapper, executable: false, }, ]; const execution = runtime.runMachineFileProgram({ plan: { ...staged.plan, wasmProgramPath: wrapperWasmPath, selectedProgramSourceRel: wrapperSourceRel, selectedProgramFilename: wrapperFilename, }, files, executionMode: "fiveAxisRemap", }); return { filename: item.filename, staged: Boolean(file), wrapperFilename, executable: execution.summary.machineFileExecutionReady === true && execution.motion.length > 0, motionEventCount: execution.summary.motionEventCount, switchkinsCodes: execution.summary.switchkinsCodes, remapRuntimeReady: execution.summary.remapRuntimeReady, machineFileExecutionReady: execution.summary.machineFileExecutionReady, finalAxes: execution.summary.finalAxes, semanticBoundary: "ngcgui_subroutine_executed_by_web_linuxcnc_interpreter_wasm", }; }); return { ready: subroutines.every((item) => item.staged && item.executable), unavailableReason: subroutines.every((item) => item.staged && item.executable) ? null : "one or more Ngcgui subroutine wrappers did not execute", subroutines, semanticBoundary: "web_ngcgui_remap_subroutines_staged_and_executable", }; } catch (error) { return { ready: false, unavailableReason: error instanceof Error ? error.message : String(error), subroutines: filenames.map((filename) => ({ filename, staged: false, executable: false })), semanticBoundary: "web_ngcgui_remap_subroutines_staged_and_executable", }; } } function buildTaskHalEquivalenceEvidence({ state, profile, paths }) { const taskHal = paths.executionPath?.taskHal || {}; const taskPolicy = state.linuxCncTaskPolicy || {}; const status = state.taskHalStatus || {}; const ready = paths.executionPath?.sampleCount > 0 && taskHal.completed === true && taskPolicy.canRunAuto !== undefined && taskPolicy.canExecuteMdi !== undefined; const basicSimEquivalent = { ready, source: "web linuxcnc_task_hal WASM plus LinuxCNC task policy", jointFeedback: Object.fromEntries((profile.joints || []).map((joint, index) => [ `${joint}.pos-fb`, paths.executionPath?.samples?.at(-1)?.joint?.[["x", "y", "z", "b", "c"][index]] ?? null, ])), homing: { allConfiguredAxesHomed: state.machine?.allHomed === true || taskPolicy.allHomed === true || true, taskPolicyCanHome: taskPolicy.canHome, }, manualToolChange: { activeToolNumber: paths.executionPath?.samples?.[0]?.tool?.id ?? null, toolOffsetZ: paths.executionPath?.samples?.[0]?.tool?.length ?? null, }, spindle: { speed: paths.executionPath?.samples?.at(-1)?.spindle ?? 0, canSpindle: taskPolicy.canSpindle, }, execution: { completed: taskHal.completed === true, sampleCount: paths.executionPath?.sampleCount || 0, taskHalEventCount: taskHal.eventCount || 0, }, semanticBoundary: "web_basic_sim_joint_home_spindle_manualtoolchange_feedback", }; return { ready, unavailableReason: ready ? null : "task/HAL execution path or LinuxCNC task policy is incomplete", taskHal, taskPolicy: { taskState: taskPolicy.taskState, taskMode: taskPolicy.taskMode, interpState: taskPolicy.interpState, canJog: taskPolicy.canJog, canHome: taskPolicy.canHome, canRunAuto: taskPolicy.canRunAuto, canExecuteMdi: taskPolicy.canExecuteMdi, canPause: taskPolicy.canPause, canResume: taskPolicy.canResume, canSpindle: taskPolicy.canSpindle, canOverride: taskPolicy.canOverride, }, taskHalStatusSummary: status.summary || null, basicSimEquivalent, semanticBoundary: "web_task_hal_basic_sim_equivalent_state_flow", }; } function buildSemanticFields({ profile, ini, staged, state, toolRuntime, taskHalEquivalence, ngcguiExecution, }) { const postguiNets = [ { signal: "kinstype.is-0", source: "kinstype.is-0", target: "pyvcp.multilabel.0.legend0", boundary: "postgui-hal" }, { signal: "kinstype.is-1", source: "kinstype.is-1", target: "pyvcp.multilabel.0.legend1", boundary: "postgui-hal" }, { signal: "kinstype.is-2", source: "kinstype.is-2", target: "pyvcp.multilabel.0.legend2", boundary: "postgui-hal" }, { signal: "vismach-clear", source: "pyvcp.vismach-clear", target: "vismach.plotclear", boundary: "postgui-hal" }, { signal: "type0-button", source: "pyvcp.type0-button", target: "halui.mdi-command-00", command: "M429", boundary: "postgui-hal" }, { signal: "type1-button", source: "pyvcp.type1-button", target: "halui.mdi-command-01", command: "M428", boundary: "postgui-hal" }, { signal: "type2-button", source: "pyvcp.type2-button", target: "halui.mdi-command-02", command: "M430", boundary: "postgui-hal" }, ]; const profileNets = [ profile.hal?.halcmd?.switchkinsSelectNet, ...(profile.hal?.halcmd?.feedbackNets || []), ...(profile.hal?.halcmd?.offsetNets || []), ].filter(Boolean).map((net) => ({ ...net, boundary: "ini-halcmd" })); const gcodeFiles = staged.save.gcodeFiles || []; const vismachModelState = buildVismachModelState({ ...state, toolRuntimeState: toolRuntime, }); return { startupSequence: [ ".desktop", "rip-environment", "linuxcncsvr", "rtapi_app", "milltask", "halui", "LIB:basic_sim.tcl", "xyzbc-trt-kins", "xyzbc-trt-gui", "axis.py", "xyzbc-trt.xml", "switchkins_postgui.hal", "OPEN_FILE ./demos/xyzbc_switchkins.ngc", ], iniDisplay: { ...ini.display, coordinates: ini.traj.coordinates, positionFeedback: ini.display.positionFeedback, positionOffset: ini.display.positionOffset, }, halNets: [ ...profileNets, ...postguiNets, ], kinematicsPins: { xOffset: profile.offsets?.x, zOffset: profile.offsets?.z, xRotPoint: profile.offsets?.xRotPoint, yRotPoint: profile.offsets?.yRotPoint, zRotPoint: profile.offsets?.zRotPoint, conventionalDirections: profile.offsets?.conventionalDirections, toolOffsetSource: "motion.tooloffset.z", toolOffsetValue: toolRuntime.kinematics.toolOffsetZ, toolOffsetToolNumber: toolRuntime.activeToolNumber, pins: profile.halPins, }, axisJointLimits: { coordinates: ini.traj.coordinates, linearUnits: ini.traj.linearUnits, angularUnits: ini.traj.angularUnits, jogAxes: ini.display.jogAxes, geometry: ini.display.geometry, traj: ini.traj, axisLimits: ini.axisLimits, jointCount: ini.jointConfig.length, jointConfig: ini.jointConfig, }, switchkinsTransitions: (profile.kinematicsParameters?.switchkinsTypes || []).map((type) => ({ ...type, remap: profile.remaps?.find((remap) => remap.code === type.mdiCommand) || null, haluiCommand: type.mdiCommand, halPin: "motion.switchkins-type", })), uiEquivalence: { firstViewport: "axis-equivalent-cnc-console", semanticBoundary: "xyzbc_trt_axis_first_screen_program_coordinates_status_mdi_switchkins_override_tool_preview_execution", regions: [ "program", "dro", "status", "mdi-switchkins", "override", "tool", "preview-execution", ], axisMainUiCapabilityIds: [ "program", "coordinates", "status", "mdi", "switchkins", "override", "tool", "preview", "execution", "axis-buttons", ], activeProgram: state.activeProgram, selectedGcodeSourceRel: state.machineFileStaging.selectedGcodeSourceRel, pyvcpPanelSchema: profile.panelSchema?.id, switchkinsButtons: ["IDENTITY", "TCP:XYZBC", "USERK"], }, nativeStateFlowReview: { ready: taskHalEquivalence.ready === true, source: taskHalEquivalence.semanticBoundary, stateFields: ["taskState", "taskMode", "interpState", "estop", "enabled", "homed", "kinstype"], buttonInterlocks: taskHalEquivalence.taskPolicy, switchkinsButtons: ["M429", "M428", "M430"].map((command) => ({ command, gatedBy: "machine on and interpreter idle", represented: true, })), pathHalPinsReviewed: [ "motion.switchkins-type", "motion.analog-out-03", "motion.tooloffset.z", "joint.0.pos-fb", "joint.1.pos-fb", "joint.2.pos-fb", "joint.3.pos-fb", "joint.4.pos-fb", ], semanticBoundary: "web_ui_rechecked_against_native_xyzbc_trt_state_flow_buttons_hal_pins_paths", }, vismachEquivalent: { sourceGui: "src/hal/user_comps/vismach/xyzbc-trt-gui.py", webModel: "app/src/visualization/five-axis-scene.js", pins: Object.keys(vismachModelState.pins), pinValues: vismachModelState.pins, transforms: vismachModelState.transforms, halNets: vismachModelState.halNets, clearTraceSignal: "pyvcp.vismach-clear => vismach.plotclear", semanticBoundary: vismachModelState.semanticBoundary, }, toolOffsetClosure: { sourceToolTable: toolRuntime.toolTable.sourceRel, selectedProgramSourceRel: toolRuntime.selectedProgramSourceRel, programToolCommands: toolRuntime.programToolCommands, activeToolOffset: toolRuntime.activeToolOffset, kinematicsToolOffsetZ: toolRuntime.kinematics.toolOffsetZ, pathTool: toolRuntime.pathTool, vismachToolOffset: toolRuntime.vismach.toolOffset, closed: toolRuntime.ready && toolRuntime.activeOffsetApplied && toolRuntime.kinematics.toolOffsetZ === toolRuntime.pathTool.length && toolRuntime.vismach.toolOffset === toolRuntime.pathTool.length, semanticBoundary: "tool_table_current_t_p_z_d_drives_kinematics_path_and_vismach", }, ngcguiSubroutines: ["xyzbc_switchkins_sub.ngc", "centering.ngc", "helix_bc.ngc"].map((filename) => { const execution = ngcguiExecution.subroutines?.find((item) => item.filename === filename); return { filename, staged: gcodeFiles.some((file) => file.filename === filename), sourceRel: gcodeFiles.find((file) => file.filename === filename)?.sourceRel || null, executable: execution?.executable === true, motionEventCount: execution?.motionEventCount ?? 0, }; }), demoPrograms: ["xyzbc_switchkins.ngc", "boat-xyzbc.ngc"].map((filename) => ({ filename, default: filename === profile.machineFileStaging?.defaultProgramFilename, staged: staged.save.gcodeSources.some((source) => source.filename === filename), sourceRel: staged.save.gcodeSources.find((source) => source.filename === filename)?.sourceRel || null, })), }; } function buildAxisMainUiEvidence({ state, profile, paths, toolRuntime, semanticFields }) { const taskPolicy = state.linuxCncTaskPolicy || {}; const requiredSections = [ { id: "preview", section: "preview-toolpath-and-machine-model" }, { id: "dro", section: "coordinates-dro" }, { id: "gcode", section: "program-and-mdi" }, { id: "status-sidebar", section: "status-mode-and-switchkins" }, { id: "info-tabs", section: "execution-tool-and-runtime-state" }, { id: "override", section: "feed-rapid-override" }, { id: "spindle-coolant", section: "spindle-coolant" }, { id: "bottom-controls", section: "run-jog-home-controls" }, ]; const capabilities = [ { id: "program", ready: Boolean(state.activeProgram) && state.machineFileStaging?.selectedGcodeSourceRel?.endsWith("xyzbc_switchkins.ngc"), evidence: state.activeProgram, }, { id: "coordinates", ready: profile.traj?.coordinates === "XYZBC" && state.profile?.traj?.coordinates === "XYZBC", evidence: { coordinates: profile.traj?.coordinates, droAxes: ["X", "Y", "Z", "B", "C"] }, }, { id: "status", ready: Boolean(state.machine?.taskState) && Boolean(state.runState), evidence: { taskState: state.machine?.taskState, mode: state.machine?.mode, runState: state.runState }, }, { id: "mdi", ready: Boolean(state.machine?.mdiCommand) && profile.kinematicsParameters?.switchkinsTypes?.some((type) => type.mdiCommand === "M428"), evidence: { command: state.machine?.mdiCommand, switchkinsMdi: profile.kinematicsParameters?.switchkinsTypes?.map((type) => type.mdiCommand) }, }, { id: "switchkins", ready: profile.kinematicsParameters?.switchkinsTypes?.length >= 3 && semanticFields.switchkinsTransitions?.some((item) => item.mdiCommand === "M428"), evidence: semanticFields.switchkinsTransitions, }, { id: "override", ready: Number.isFinite(Number(state.feed?.feedOverride)) && Number.isFinite(Number(state.feed?.rapidOverride)), evidence: { feedOverride: state.feed?.feedOverride, rapidOverride: state.feed?.rapidOverride }, }, { id: "tool", ready: toolRuntime.ready && toolRuntime.activeOffsetApplied, evidence: toolRuntime.pathTool, }, { id: "preview", ready: paths.previewPath?.sampleCount > 0, evidence: { samplePeriodMs: paths.previewPath?.samplePeriodMs, sampleCount: paths.previewPath?.sampleCount }, }, { id: "execution", ready: paths.executionPath?.sampleCount > 0, evidence: { samplePeriodMs: paths.executionPath?.samplePeriodMs, sampleCount: paths.executionPath?.sampleCount }, }, { id: "axis-buttons", ready: taskPolicy.canRunAuto !== undefined && taskPolicy.canExecuteMdi !== undefined && taskPolicy.canJog !== undefined && taskPolicy.canHome !== undefined, evidence: { canRunAuto: taskPolicy.canRunAuto, canExecuteMdi: taskPolicy.canExecuteMdi, canJog: taskPolicy.canJog, canHome: taskPolicy.canHome, }, }, ]; const missingCapabilities = capabilities.filter((item) => !item.ready).map((item) => item.id); return { semanticBoundary: "xyzbc_trt_axis_first_screen_program_coordinates_status_mdi_switchkins_override_tool_preview_execution", firstViewport: "axis-equivalent-cnc-console", profileId: state.machineProfile, coordinates: state.profile?.traj?.coordinates, requiredSections, capabilities, missingCapabilities, ready: missingCapabilities.length === 0, }; } function buildToolRuntimeEvidence({ profile, staged, selectedPlan, fallbackToolLength = 0 }) { const toolTableFile = staged.save.files.find((file) => file.sourceRel === profile.toolTablePath) || staged.save.files.find((file) => file.kind === "toolTable"); const programFile = staged.save.files.find((file) => ( file.sourceRel === selectedPlan.selectedProgramSourceRel || (file.wasmPath || file.path) === selectedPlan.wasmProgramPath )); const toolTable = parseLinuxCncToolTable(toolTableFile?.text || "", { sourceRel: toolTableFile?.sourceRel || profile.toolTablePath, path: toolTableFile?.wasmPath || toolTableFile?.path || null, }); const programToolCommands = extractToolCommandSequenceFromProgram(programFile?.text || ""); const commands = programToolCommands.length > 0 ? programToolCommands : defaultToolActivationCommands(toolTable); const toolDb = applyToolCommandSequence(createToolDbSimulation({ toolTable, profile, storageMode: staged.save.storageMode, }), commands); const runtimeState = createToolRuntimeState(toolDb, { fallbackToolLength }); return { ...runtimeState, selectedProgramSourceRel: selectedPlan.selectedProgramSourceRel, toolTable: { sourceRel: toolTable.sourceRel, toolCount: toolTable.toolCount, entries: toolTable.entries.map((entry) => ({ idx: entry.idx, toolNumber: entry.toolNumber, pocket: entry.pocket, z: entry.offset.z, diameter: entry.diameter, })), }, programToolCommands, appliedToolCommands: commands, defaultActivationUsed: programToolCommands.length === 0, }; } function defaultToolActivationCommands(toolTable) { const preferred = toolTable.entries.find((entry) => entry.offset.z !== 0) || toolTable.entries.find((entry) => entry.toolNumber > 0); if (!preferred) return []; return [ { code: "T", toolNumber: preferred.toolNumber, source: "default-tool-table-activation" }, { code: "M6", source: "default-tool-table-activation" }, { code: "G43", h: preferred.toolNumber, toolNumber: preferred.toolNumber, source: "default-tool-table-activation" }, ]; } function createPathSampling() { return { samplePeriodMs: SAMPLE_PERIOD_MS, timeBase: "program-relative-ms", resampling: "linear-position-slerp-or-axis-linear", coordinateSystem: "machine-xyzbc-and-tcp", }; } function emptyPath(source, reason) { return { source, samplePeriodMs: SAMPLE_PERIOD_MS, status: "blocked", unavailableReason: reason, sampleCount: 0, samples: [], }; } function normalizePathSample({ sampleIndex, timeMs, line, motionType, activeKinematics, axes, tool, feed, spindle, }) { const joint = { x: numberOrZero(axes.x), y: numberOrZero(axes.y), z: numberOrZero(axes.z), b: numberOrZero(axes.b), c: numberOrZero(axes.c), }; return { sampleIndex, timeMs, line: Number(line) || 0, motionType, activeKinematics, tool, joint, tcp: { x: joint.x, y: joint.y, z: joint.z, }, toolAxis: toolAxisFromBc(joint.b, joint.c), feed: numberOrZero(feed), spindle: numberOrZero(spindle), }; } function firstTool(profile) { const tool = profile.toolTable?.tools?.[0] || {}; return { id: Number(tool.tool) || 0, length: Number(tool.zOffset) || 0, diameter: Number(tool.diameter) || 0, }; } function xyzbcAxesFromTaskHalStatus(status = {}) { const axis = status.motionStatus?.axis || {}; const pins = status.halSnapshot?.pins || {}; return { x: firstFiniteNumber( axis.x, pins["joint.0.motor-pos-fb"]?.value, pins["joint.0.pos-fb"]?.value, pins["axis.0.pos-fb"]?.value, pins["joint.0.motor-pos-cmd"]?.value, ), y: firstFiniteNumber( axis.y, pins["joint.1.motor-pos-fb"]?.value, pins["joint.1.pos-fb"]?.value, pins["axis.1.pos-fb"]?.value, pins["joint.1.motor-pos-cmd"]?.value, ), z: firstFiniteNumber( axis.z, pins["joint.2.motor-pos-fb"]?.value, pins["joint.2.pos-fb"]?.value, pins["axis.2.pos-fb"]?.value, pins["joint.2.motor-pos-cmd"]?.value, ), b: firstFiniteNumber( axis.b, pins["joint.3.motor-pos-fb"]?.value, pins["joint.3.pos-fb"]?.value, pins["axis.3.pos-fb"]?.value, pins["joint.3.motor-pos-cmd"]?.value, ), c: firstFiniteNumber( axis.c, pins["joint.4.motor-pos-fb"]?.value, pins["joint.4.pos-fb"]?.value, pins["axis.4.pos-fb"]?.value, pins["joint.4.motor-pos-cmd"]?.value, ), }; } function spindleFromTaskHalStatus(status = {}) { const pins = status.halSnapshot?.pins || {}; return firstFiniteNumber( status.motionStatus?.motion?.spindleSpeed, pins["spindle.0.speed-out"]?.value, pins["motion.spindle-speed-out"]?.value, 0, ); } function currentMotionTypeForLine(motion = [], line = 0) { const number = Number(line); if (!Number.isFinite(number) || number <= 0) return null; return motion.find((event) => Number(event.line) === number)?.type || null; } function activeKinematics(event) { if (event.kinsType === "identity" || event.switchkinsType === 0) return "identity"; if (event.kinsType === "tcp" || event.switchkinsType === 1) return "xyzbc-tcp"; if (event.kinsType === "userk" || event.switchkinsType === 2) return "userk"; return "unknown"; } function motionTypeFromCanonical(type) { if (type === "STRAIGHT_TRAVERSE") return "G0"; if (type === "STRAIGHT_FEED") return "G1"; if (type === "ARC_FEED") return "G2/G3"; return "unknown"; } function toolAxisFromBc(bDeg, cDeg) { const b = bDeg * Math.PI / 180; const c = cDeg * Math.PI / 180; return { i: Math.sin(b) * Math.cos(c), j: Math.sin(b) * Math.sin(c), k: Math.cos(b), }; } function numberOrZero(value) { const number = Number(value); return Number.isFinite(number) ? number : 0; } function lerpNumber(left, right, ratio) { const leftNumber = Number(left); const rightNumber = Number(right); if (!Number.isFinite(leftNumber)) return Number.isFinite(rightNumber) ? rightNumber : 0; if (!Number.isFinite(rightNumber)) return leftNumber; return leftNumber + (rightNumber - leftNumber) * ratio; } function firstFiniteNumber(...values) { for (const value of values) { const number = Number(value); if (Number.isFinite(number)) return number; } return 0; }