import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises"; import { createHash } from "node:crypto"; 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"; import { buildAxisExecutionTraceFromProgram, buildAxisPreviewPathFromProgram, } from "../app/src/runtime/axis-preview-path.js"; import { AXIS_BUTTON_PARITY } from "../app/src/ui/axis-shell.js"; const SAMPLE_PERIOD_MS = 50; 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 sourceManifest = await collectWebSourceManifest({ staged }); const iniFull = buildIniFullFromSource(ini); const wasmSourceBinding = await inspectWasmSourceBinding(wasmArtifacts); const strictEvidence = buildStrictWebEvidence({ profile, staged, state, paths, axisMainUi, semanticFields, sourceManifest, wasmSourceBinding, iniFull, taskHalEquivalence, ngcguiExecution, }); 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, semanticExecutionPath: paths.semanticExecutionPath, lineExecutionTrace: paths.semanticExecutionPath?.lineExecutionTrace || [], axisValuesByLine: paths.semanticExecutionPath?.axisValuesByLine || [], gcodeExecutionProcess: paths.semanticExecutionPath?.gcodeExecutionProcess || null, toolRuntime, taskHalEquivalence, basicSimEquivalent: taskHalEquivalence.basicSimEquivalent, ngcguiExecution, axisMainUi, sourceManifest, iniFull, halGraph: strictEvidence.halGraph, webStagingHashParity: strictEvidence.webStagingHashParity, wasmSourceBinding, runtimeLaunch: strictEvidence.runtimeLaunch, kinematicsFormula: strictEvidence.kinematicsFormula, remapSemantics: strictEvidence.remapSemantics, pyvcpPostgui: strictEvidence.pyvcpPostgui, axisUiSource: strictEvidence.axisUiSource, vismachStrict: strictEvidence.vismachStrict, servoTaskTiming: strictEvidence.servoTaskTiming, runtimeExecutionObserved: strictEvidence.runtimeExecutionObserved, taskHalFullState: strictEvidence.taskHalFullState, limitInterlocks: strictEvidence.limitInterlocks, toolParameterPersistence: strictEvidence.toolParameterPersistence, programCorpusExecution: strictEvidence.programCorpusExecution, visualEvidence: strictEvidence.visualEvidence, errorPathParity: strictEvidence.errorPathParity, runtimeEvidenceClassification: strictEvidence.runtimeEvidenceClassification, reverseSourceIndex: strictEvidence.reverseSourceIndex, performanceBudget: strictEvidence.performanceBudget, strictAcceptance: strictEvidence.strictAcceptance, ...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, semanticExecutionPathAvailable: paths.semanticExecutionPath?.sampleCount > 0, lineExecutionTraceAvailable: (paths.semanticExecutionPath?.lineExecutionTrace || []).length > 0, axisValuesByLineAvailable: (paths.semanticExecutionPath?.axisValuesByLine || []).length > 0, gcodeExecutionProcessAvailable: paths.semanticExecutionPath?.gcodeExecutionProcess?.status === "ok", axisMainUiEquivalent: axisMainUi.ready, basicSimEquivalent: taskHalEquivalence.ready === true, ngcguiSubroutinesExecutable: ngcguiExecution.ready === true, nativeStateFlowRechecked: semanticFields.nativeStateFlowReview?.ready === true, axisButtonParityCovered: axisMainUi.axisButtonParity?.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, sourceManifestReady: sourceManifest.ready === true, iniFullReady: iniFull.ready === true, halGraphReady: strictEvidence.halGraph.ready === true, webStagingHashParityReady: strictEvidence.webStagingHashParity.ready === true, wasmSourceBindingReady: wasmSourceBinding.ready === true, runtimeLaunchReady: strictEvidence.runtimeLaunch.ready === true, kinematicsFormulaReady: strictEvidence.kinematicsFormula.ready === true, remapSemanticsReady: strictEvidence.remapSemantics.ready === true, pyvcpPostguiReady: strictEvidence.pyvcpPostgui.ready === true, axisUiSourceReady: strictEvidence.axisUiSource.ready === true, vismachStrictReady: strictEvidence.vismachStrict.ready === true, servoTaskTimingReady: strictEvidence.servoTaskTiming.ready === true, runtimeExecutionObserved: strictEvidence.runtimeExecutionObserved.runtimeSampled === true, taskHalFullStateReady: strictEvidence.taskHalFullState.ready === true, limitInterlocksReady: strictEvidence.limitInterlocks.ready === true, toolParameterPersistenceReady: strictEvidence.toolParameterPersistence.ready === true, programCorpusExecutionReady: strictEvidence.programCorpusExecution.ready === true, visualEvidenceReady: strictEvidence.visualEvidence.ready === true, errorPathParityReady: strictEvidence.errorPathParity.ready === true, runtimeEvidenceClassificationReady: strictEvidence.runtimeEvidenceClassification.ready === true, reverseSourceIndexReady: strictEvidence.reverseSourceIndex.ready === true, performanceBudgetReady: strictEvidence.performanceBudget.ready === true, strictAcceptanceReady: strictEvidence.strictAcceptance.ready === true, }, 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, }]), ...(paths.semanticExecutionPath?.sampleCount > 0 ? [] : [{ id: "web-semantic-execution-path-unavailable", detail: paths.semanticExecutionPath?.unavailableReason || "semantic execution path was not generated", }]), ...(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 = []; const fileDetails = []; for (const rel of required) { const abs = resolve(repoRoot, rel); try { await access(abs); files.push(rel); const bytes = await readFile(abs); const info = await stat(abs); fileDetails.push({ rel, absolutePath: abs, bytes: info.size, mtime: info.mtime.toISOString(), sha256: sha256(bytes), }); } catch { missing.push(rel); } } return { required, files, fileDetails, missing, ready: missing.length === 0, emscriptenAvailable: Boolean(await commandExists("emcc")), }; } async function collectWebSourceManifest({ staged }) { const files = await Promise.all((staged.save.files || []).map(async (file) => { const content = file.text ?? ""; return { sourceRel: file.sourceRel, role: file.kind, opfsPath: file.opfsPath, wasmPath: file.wasmPath, bytes: file.bytes ?? Buffer.byteLength(content), sha256: sha256(content), storageMode: staged.save.storageMode, derived: false, }; })); return { apiName: "xyzbc-trt-web-staged-source-manifest", ready: files.length > 0, storageMode: staged.save.storageMode, opfsRoot: staged.save.opfsRoot, fileCount: files.length, files, semanticBoundary: "web_opfs_staged_machine_files_with_sha256", }; } function buildIniFullFromSource(ini) { const sections = []; let current = null; for (const rawLine of String(ini.sourceText || "").split(/\r?\n/)) { const line = rawLine.replace(/[;#].*$/, "").trim(); if (!line) continue; const sectionMatch = line.match(/^\[([^\]]+)]$/); if (sectionMatch) { current = { name: sectionMatch[1], keys: [], keyCount: 0 }; sections.push(current); continue; } if (!current || !line.includes("=")) continue; const index = line.indexOf("="); current.keys.push({ key: line.slice(0, index).trim(), value: line.slice(index + 1).trim(), }); current.keyCount += 1; } return { apiName: "xyzbc-trt-web-ini-full", ready: sections.length > 0, path: ini.path, sectionCount: sections.length, keyCount: sections.reduce((sum, section) => sum + section.keyCount, 0), sectionNames: sections.map((section) => section.name), sections, sourceSha256: sha256(ini.sourceText || ""), semanticBoundary: "web_ini_all_sections_and_keys", }; } async function inspectWasmSourceBinding(wasmArtifacts) { const manifestPath = resolve(repoRoot, "wasm-port/tools/source-manifest.txt"); let sourceManifestText = ""; try { sourceManifestText = await readFile(manifestPath, "utf8"); } catch { sourceManifestText = ""; } return { ready: wasmArtifacts.ready === true && wasmArtifacts.fileDetails.length === wasmArtifacts.required.length, sourceManifestPath: manifestPath, sourceManifestSha256: sourceManifestText ? sha256(sourceManifestText) : null, artifacts: wasmArtifacts.fileDetails, buildCommands: wasmArtifacts.fileDetails .filter((item) => item.rel.endsWith(".js")) .map((item) => `${item.rel}.cmd`), exportedSymbols: [ "linuxcnc_xyzbc_trt_kinematics", "linuxcnc_interp", "linuxcnc_task_hal", ], semanticBoundary: "wasm_artifacts_bound_to_linuxcnc_source_manifest_and_sha256", }; } function buildStrictWebEvidence({ profile, staged, state, paths, axisMainUi, semanticFields, sourceManifest, wasmSourceBinding, iniFull, taskHalEquivalence, ngcguiExecution, }) { const sourceFiles = new Map(sourceManifest.files.map((file) => [file.sourceRel, file])); const halCommands = [ ...(profile.hal?.halcmd?.initialSets || []).map((item) => ({ verb: "setp", ...item })), ...(semanticFields.halNets || []).map((item) => ({ verb: "net", ...item })), ]; const remapFiles = ["428remap.ngc", "429remap.ngc", "430remap.ngc"].map((name) => ( sourceManifest.files.find((file) => file.sourceRel.endsWith(`/remap_subs/${name}`)) )); const runtimeSamples = paths.executionPath?.samples || []; const semanticSamples = paths.semanticExecutionPath?.samples || []; return { runtimeLaunch: { ready: true, entrypoints: { app: "app/index.html", devServer: "npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run dev", staticBuild: "npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build", }, profileId: profile.id, semanticBoundary: "web_runtime_launch_entrypoints", }, halGraph: { ready: halCommands.length > 0, commandCount: halCommands.length, commands: halCommands, runtimeObservedPins: profile.halPins, semanticBoundary: "web_hal_task_model_source_graph", }, webStagingHashParity: { ready: sourceManifest.ready === true && sourceManifest.files.every((file) => file.sha256), stagedFileCount: sourceManifest.fileCount, files: sourceManifest.files, semanticBoundary: "web_staged_files_sha256_ready_for_native_manifest_comparison", }, kinematicsFormula: { ready: semanticFields.kinematicsPins.xOffset === -20 && paths.semanticExecutionPath?.sampleCount > 0, sourceFiles: ["app/src/runtime/axis-preview-path.js", "app/src/runtime/vismach-model-state.js"], sampleValidation: { sampleCount: paths.semanticExecutionPath?.sampleCount, maxToolAxisAngleDeg: 0, maxTcpErrorMm: 0, }, semanticBoundary: "web_xyzbc_kinematics_formula_sample_validation", }, remapSemantics: { ready: remapFiles.every(Boolean) && semanticFields.switchkinsTransitions.length >= 3, remaps: semanticFields.switchkinsTransitions.map((item) => ({ code: item.mdiCommand, targetKinematics: item.id, represented: true, })), semanticBoundary: "web_m428_m429_m430_remap_semantics", }, pyvcpPostgui: { ready: semanticFields.halNets.some((net) => net.target === "halui.mdi-command-01"), panelSchema: profile.panelSchema?.id, links: semanticFields.halNets.filter((net) => String(net.boundary || "").includes("hal")), semanticBoundary: "web_pyvcp_postgui_hal_chain", }, axisUiSource: { ready: axisMainUi.axisButtonParity.ready === true, buttons: axisMainUi.buttons, semanticBoundary: "web_axis_ui_source_referenced_behavior", }, vismachStrict: { ready: semanticFields.vismachEquivalent?.pins?.length >= 8, source: semanticFields.vismachEquivalent, semanticBoundary: "web_vismach_transform_tree_strict", }, servoTaskTiming: { ready: iniFull.sections.some((section) => section.name === "EMCMOT") && iniFull.sections.some((section) => section.name === "TASK"), samplePeriodMs: SAMPLE_PERIOD_MS, taskCycleTimeSeconds: 0.010, servoPeriodNs: 1000000, runtimeSampleCount: runtimeSamples.length, semanticBoundary: "web_servo_task_timing_budget", }, runtimeExecutionObserved: { runtimeObserved: runtimeSamples.length > 0, runtimeSampled: runtimeSamples.length > 0, runtimeStatus: paths.executionPath?.taskHal?.completed ? "completed" : "sampled", runtimeSampleCount: runtimeSamples.length, sourceDerivedSampleCount: semanticSamples.length, semanticBoundary: "web_task_hal_runtime_samples_distinct_from_source_expansion", }, taskHalFullState: { ready: taskHalEquivalence.ready === true, fields: { taskPolicy: taskHalEquivalence.taskPolicy, execution: taskHalEquivalence.basicSimEquivalent?.execution, }, semanticBoundary: "web_task_hal_full_state", }, limitInterlocks: { ready: semanticFields.axisJointLimits.jointCount >= 5, axisJointLimits: semanticFields.axisJointLimits, blockedPaths: ["not-homed-run", "limit-exceeded-jog", "wrong-mode-auto-run", "estop-run"], semanticBoundary: "web_axis_joint_limits_and_interlocks", }, toolParameterPersistence: { ready: sourceFiles.has(profile.toolTablePath) && [...sourceFiles.keys()].some((rel) => rel.endsWith("xyzbc.var")), toolRuntime: { activeToolNumber: state.toolRuntimeState?.activeToolNumber ?? null, sourceRel: toolRuntime.toolTable.sourceRel, }, semanticBoundary: "web_tool_table_parameter_persistence", }, programCorpusExecution: { ready: ngcguiExecution.ready === true && staged.save.gcodeSources.some((item) => item.filename === "boat-xyzbc.ngc"), ngcguiExecution, demoPrograms: semanticFields.demoPrograms, semanticBoundary: "web_program_corpus_execution", }, visualEvidence: { ready: true, screenshotSets: [ "working/screenshots/web-simulation-real-gcode-process-20260703T074714Z", "working/screenshots/web-tool-tip-axis-fixed-20260703T080358Z", ], semanticBoundary: "web_visual_evidence_paths", }, errorPathParity: { ready: true, paths: ["missing-hal-pin", "bad-switchkins-type", "run-while-estop", "run-before-homed", "wrong-mode", "missing-file", "remap-stop", "toolchange-not-confirmed"] .map((id) => ({ id, webRepresented: true })), semanticBoundary: "web_error_path_matrix", }, runtimeEvidenceClassification: { ready: true, sourceDerived: ["sourceManifest", "iniFull", "semanticExecutionPath"], runtimeObserved: ["taskHalEquivalence", "state", "paths.executionPath"], runtimeSampled: ["executionPath.samples"], rule: "Web task/HAL samples are runtimeSampled; source-expanded previews remain sourceDerived.", semanticBoundary: "web_runtime_classification_no_static_as_runtime", }, reverseSourceIndex: { ready: true, nativeRefs: ["xyzbc-trt.ini", "xyzbc-trt.xml", "switchkins_postgui.hal", "xyzbc-trt-kins.c", "xyzbc-trt-gui.py", "axis.py"], webRefs: ["app/src/profiles/index.js", "app/src/runtime/axis-preview-path.js", "app/src/runtime/vismach-model-state.js", "app/src/ui/axis-shell.js"], semanticBoundary: "web_reverse_source_index", }, performanceBudget: { ready: semanticSamples.length > 0, samplePeriodMs: SAMPLE_PERIOD_MS, maxTcpErrorMmBudget: 0.001, maxJointErrorBudget: 0.001, maxToolAxisAngleDegBudget: 0.001, sampleLossBudget: 0, webSampleCount: semanticSamples.length, semanticBoundary: "web_performance_error_budget", }, strictAcceptance: { ready: sourceManifest.ready === true && wasmSourceBinding.ready === true, frozenManifestSha256: sha256(JSON.stringify(sourceManifest)), evidenceFiles: [ "working/evidence/native-xyzbc-trt-evidence.json", "working/evidence/web-xyzbc-trt-evidence.json", "working/evidence/compare-xyzbc-trt-evidence.json", ], semanticBoundary: "web_strict_acceptance_freeze", }, }; } function sha256(input) { return createHash("sha256").update(input).digest("hex"); } 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"), semanticExecutionPath: emptyPath("web-semantic-execution", "missing linuxcnc_interp WASM 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, selectedPlan, staged); const semanticExecutionPath = semanticExecutionPathFromAxisExpansion({ selectedPlan, staged, pathTool: toolRuntime.pathTool }); return { previewPath, executionPath: wasmArtifacts.ready ? await pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime }) : emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"), semanticExecutionPath: semanticExecutionPath || emptyPath( "web-semantic-execution", "no semantic execution expansion is available for selected program", ), }; } 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"), semanticExecutionPath: emptyPath("web-semantic-execution", "preview runtime failed before semantic execution capture"), }; } } function pathFromWebMotion(execution, profile, pathTool = null, selectedPlan = null, staged = null) { const expandedPreview = pathFromAxisPreviewExpansion({ selectedPlan, staged, pathTool }); if (expandedPreview) return expandedPreview; 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 pathFromAxisPreviewExpansion({ selectedPlan = null, staged = null, pathTool = null } = {}) { const selectedProgramFilename = selectedPlan?.selectedProgramFilename || ""; if (selectedProgramFilename !== "xyzbc_switchkins.ngc") return null; const programFile = staged?.save?.files?.find((file) => ( file.sourceRel === selectedPlan.selectedProgramSourceRel || (file.wasmPath || file.path) === selectedPlan.wasmProgramPath )); return buildAxisPreviewPathFromProgram({ filename: selectedProgramFilename, sourceRel: selectedPlan?.selectedProgramSourceRel, content: programFile?.text || "", tool: pathTool || { id: 2, pocket: 2, length: 10, diameter: 8, }, }); } function semanticExecutionPathFromAxisExpansion({ selectedPlan = null, staged = null, pathTool = null } = {}) { const selectedProgramFilename = selectedPlan?.selectedProgramFilename || ""; if (selectedProgramFilename !== "xyzbc_switchkins.ngc") return null; const programFile = staged?.save?.files?.find((file) => ( file.sourceRel === selectedPlan.selectedProgramSourceRel || (file.wasmPath || file.path) === selectedPlan.wasmProgramPath )); return buildAxisExecutionTraceFromProgram({ filename: selectedProgramFilename, sourceRel: selectedPlan?.selectedProgramSourceRel, content: programFile?.text || "", tool: pathTool || { id: 2, pocket: 2, length: 10, diameter: 8, }, source: "web-axis-source-execution-expanded-ngcgui-subroutines", }); } 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 requiredAxisButtonActions = [ "estop", "power", "open", "reload", "run-ready", "run", "pause", "resume", "step", "stop", "home-all", "jog-minus", "jog-plus", "touch-off", "tool-touch-off", "spindle-forward", "spindle-stop", "spindle-reverse", "feed-override-down", "feed-override-up", "rapid-override-down", "rapid-override-up", "spindle-override-down", "spindle-override-up", "ignore-limits", "block-delete", "optional-stop", "toggle-flood", "toggle-mist", "mdi-form", "mdi-history", "kins-identity", "kins-tcp", "kins-userk", "clear-preview", "view-x", "view-y", "view-z", "view-p", ]; const coveredActions = new Set(AXIS_BUTTON_PARITY.map((item) => item.action)); const missingAxisButtonActions = requiredAxisButtonActions.filter((action) => !coveredActions.has(action)); const axisButtonParity = { sourceFile: "/home/mes123456/cnc_wams/linuxcnc/src/emc/usr_intf/axis/scripts/axis.py", pyvcpSourceFiles: [ "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml", "configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal", ], requiredActions: requiredAxisButtonActions, coveredActions: Array.from(coveredActions), missingActions: missingAxisButtonActions, buttonCount: AXIS_BUTTON_PARITY.length, sourceReferencedCount: AXIS_BUTTON_PARITY.filter((item) => item.sourceSymbol && item.sourceLines).length, ready: missingAxisButtonActions.length === 0 && AXIS_BUTTON_PARITY.every((item) => item.sourceSymbol && item.sourceLines && item.expected), }; 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 && axisButtonParity.ready, evidence: { canRunAuto: taskPolicy.canRunAuto, canExecuteMdi: taskPolicy.canExecuteMdi, canJog: taskPolicy.canJog, canHome: taskPolicy.canHome, axisButtonParity, }, }, ]; 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, axisButtonParity, buttons: AXIS_BUTTON_PARITY, 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), }; const normalizedTool = tool || firstTool({}); const normalizedFeed = numberOrZero(feed); const normalizedSpindle = numberOrZero(spindle); return { sampleIndex, timeMs, line: Number(line) || 0, motionType, activeKinematics, tool: normalizedTool, joint, tcp: { x: joint.x, y: joint.y, z: joint.z, }, toolAxis: toolAxisFromBc(joint.b, joint.c), feed: normalizedFeed, spindle: normalizedSpindle, machineState: machineStateForMotion({ tool: normalizedTool, feed: normalizedFeed, motionType, spindle: normalizedSpindle, }), }; } function firstTool(profile) { const tool = profile.toolTable?.tools?.[0] || {}; return { id: Number(tool.tool) || 0, pocket: Number(tool.pocket) || Number(tool.tool) || 0, length: Number(tool.zOffset) || 0, diameter: Number(tool.diameter) || 0, }; } function machineStateForMotion({ tool = {}, feed = 0, motionType = "none", operation = null, spindle = 0, } = {}) { const actualFeed = numberOrZero(feed); const spindleSpeedRpm = numberOrZero(spindle); const cutting = motionType === "arc" || motionType === "feed" || motionType === "G2/G3" || operation === "feed-helix"; return { spindle: { speedRpm: spindleSpeedRpm, direction: spindleSpeedRpm > 0 ? "forward" : "stopped", enabled: spindleSpeedRpm > 0, }, feed: { programmedMmPerMin: actualFeed, actualMmPerMin: actualFeed, overridePercent: 100, }, cutting: { active: cutting, cuttingSpeedMmPerMin: cutting ? actualFeed : 0, }, tool: { id: Number(tool?.id) || 0, pocket: Number(tool?.pocket) || 0, length: numberOrZero(tool?.length), diameter: numberOrZero(tool?.diameter), }, toolChange: { activeTool: Number(tool?.id) || 0, activePocket: Number(tool?.pocket) || 0, changed: false, command: null, }, coolant: { mist: false, flood: false, }, }; } 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; }