import { createLinuxCncIniSdk, createLinuxCncInterpSdk, } from "../../sdk/src/index.js"; import { getOpfsRoot, loadTextFile, saveTextFile, } from "../../opfs/file-service.js"; import { loadGcodeProgram, loadMachineTextFiles, machineFilePaths, saveGcodeProgram, saveMachineTextFiles, } from "../../opfs/machine-file-store.js"; import { loadMachineSessionFromOpfs, } from "../../opfs/linuxcnc-machine-session-bridge.js"; import { loadMachineSessionSnapshot, saveMachineSessionSnapshot, } from "../../opfs/snapshot-store.js"; const SAMPLE_PATH = "../../../vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini"; const FIVEAXIS_TRT_SOURCE_PATH = "../../../vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting"; const FIVEAXIS_TRT_WASM_DIR = "/work/ui-fiveaxis/table-rotary-tilting"; const FIVEAXIS_REMAP_FILES = [ "428remap.ngc", "429remap.ngc", "430remap.ngc", "centering.ngc", "helix_ac.ngc", "helix_bc.ngc", "xyzac_switchkins_sub.ngc", "xyzbc_switchkins_sub.ngc", ]; const MACHINE_ID = "xyzab-tdr"; const MACHINE_PATHS = machineFilePaths(MACHINE_ID); const OPFS_FILE = MACHINE_PATHS.ini; const WASM_FILE = "/work/xyzab-tdr.ini"; const SESSION_WASM_FILES = { iniWasmPath: "/work/session-machine.ini", parameterWasmPath: "/work/session-linuxcnc.var", toolTableWasmPath: "/work/session-tool.tbl", }; const GCODE_FILENAME = "ui-session.ngc"; const GCODE_WASM_FILE = "/work/ui-session.ngc"; const SESSION_SNAPSHOT_ID = "ui-machine-session"; const SESSION_SNAPSHOT_FILENAME = "ui-machine-session.json"; const DEFAULT_TOOL_TABLE = "T2 P7 Z3.125 D1.5 I12 J34 Q4 ;ui session tool\n"; const DEFAULT_PARAMETERS = [ "5161 0.0", "5162 0.0", "5220 1", "5221 0.0", "5399 0.0", "", ].join("\n"); const DEFAULT_GCODE = [ "G0 X1.0 Y2.0 (Comment)", "G1 X3.0 Y4.0 F120.0", "", ].join("\n"); const editor = document.getElementById("ini-editor"); const logNode = document.getElementById("log"); const eventNode = document.getElementById("canon-events"); const eventFilterNode = document.getElementById("event-filter"); const eventCountNode = document.getElementById("event-count"); const wasmBadge = document.getElementById("wasm-badge"); const interpBadge = document.getElementById("interp-badge"); const opfsBadge = document.getElementById("opfs-badge"); const runProgressNode = document.getElementById("run-progress"); const runProgressLabelNode = document.getElementById("run-progress-label"); const runMotionNode = document.getElementById("run-motion"); const runLineNode = document.getElementById("run-line"); const runStatementNode = document.getElementById("run-statement"); const axisNodes = { x: document.getElementById("axis-x"), y: document.getElementById("axis-y"), z: document.getElementById("axis-z"), a: document.getElementById("axis-a"), b: document.getElementById("axis-b"), c: document.getElementById("axis-c"), u: document.getElementById("axis-u"), v: document.getElementById("axis-v"), w: document.getElementById("axis-w"), }; const fields = { machine: document.getElementById("field-machine"), display: document.getElementById("field-display"), kinematics: document.getElementById("field-kinematics"), joints: document.getElementById("field-joints"), coordinates: document.getElementById("field-coordinates"), parameterFile: document.getElementById("field-parameter-file"), toolTable: document.getElementById("field-tool-table"), sessionIni: document.getElementById("field-session-ini"), sessionParameters: document.getElementById("field-session-parameters"), sessionToolTable: document.getElementById("field-session-tool-table"), sessionGcode: document.getElementById("field-session-gcode"), sessionSnapshot: document.getElementById("field-session-snapshot"), runStatus: document.getElementById("field-run-status"), fiveaxisRemap: document.getElementById("field-fiveaxis-remap"), }; let iniSdk = null; let interpSdk = null; let loadedSession = null; let canonicalEventText = ""; let runPlaybackTimer = null; function setBadge(node, text, className = "badge") { node.className = className; node.textContent = text; } function setLog(message, isError = false) { logNode.textContent = message; logNode.className = `log${isError ? " danger" : ""}`; } function setCanonicalEvents(text) { canonicalEventText = text || ""; renderCanonicalEvents(); } function renderCanonicalEvents() { const lines = canonicalEventText.split("\n").filter(Boolean); const filter = eventFilterNode.value.trim().toLowerCase(); const visibleLines = filter ? lines.filter((line) => line.toLowerCase().includes(filter)) : lines; eventNode.textContent = visibleLines.join("\n"); eventCountNode.textContent = `${visibleLines.length} / ${lines.length}`; } function setField(name, value) { fields[name].textContent = value ?? "-"; } function nextFrame() { return new Promise((resolve) => requestAnimationFrame(resolve)); } function formatAxisValue(value) { return Number.isFinite(value) ? value.toFixed(3) : "0.000"; } function clearRunPlayback() { if (runPlaybackTimer) { clearInterval(runPlaybackTimer); runPlaybackTimer = null; } } function setRunMonitor(progress, label, axes = {}, motionText = "idle", lineText = "line -", statement = "-") { runProgressNode.value = Math.max(0, Math.min(100, progress)); runProgressLabelNode.textContent = label; runMotionNode.textContent = motionText; runLineNode.textContent = lineText; runStatementNode.textContent = statement; for (const [axis, node] of Object.entries(axisNodes)) { node.textContent = formatAxisValue(axes[axis] ?? 0); } } function readCanonicalNumber(line, name) { const match = line.match(new RegExp(`(?:^| )${name}=([-+0-9.eE]+)`)); return match ? Number(match[1]) : null; } function programLineMap(programText) { const lines = new Map(); programText.split(/\r?\n/).forEach((line, index) => { lines.set(index + 1, line.trim() || "(blank)"); }); return lines; } function parseRunMotion(resultText, programText) { const axes = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 }; const snapshots = []; const sourceLines = programLineMap(programText); const motionByLine = new Map(); for (const line of resultText.split("\n")) { const motion = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/); if (!motion) { continue; } const sourceLine = readCanonicalNumber(line, "line"); if (Number.isFinite(sourceLine)) { motionByLine.set(sourceLine, motion[1]); } } for (const line of resultText.split("\n")) { if (!line.startsWith("run_step phase=execute ")) { continue; } for (const axis of Object.keys(axes)) { const value = readCanonicalNumber(line, axis); if (value !== null && Number.isFinite(value)) { axes[axis] = value; } } const sourceLine = readCanonicalNumber(line, "line"); const statementIndex = line.indexOf(" statement_uri="); const statementValue = statementIndex >= 0 ? line.slice(statementIndex + " statement_uri=".length).trim() : ""; let statement = ""; try { statement = statementValue ? decodeURIComponent(statementValue) : ""; } catch { statement = statementValue; } snapshots.push({ type: motionByLine.get(sourceLine) ?? "EXECUTE", line: sourceLine, statement: statement || (Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-"), axes: { ...axes }, }); } if (snapshots.length > 0) { return snapshots; } for (const line of resultText.split("\n")) { const motion = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/); if (!motion) { continue; } for (const axis of Object.keys(axes)) { const value = readCanonicalNumber(line, axis); if (value !== null && Number.isFinite(value)) { axes[axis] = value; } } const sourceLine = readCanonicalNumber(line, "line"); snapshots.push({ type: motion[1], line: sourceLine, statement: Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-", axes: { ...axes }, }); } return snapshots; } function showRunSnapshot(snapshot, index, total) { const progress = total > 0 ? Math.round(((index + 1) / total) * 100) : 0; const lineText = Number.isFinite(snapshot.line) ? `line ${snapshot.line}` : "line -"; setRunMonitor( progress, `${progress}%`, snapshot.axes, `${snapshot.type} ${lineText} (${index + 1}/${total})`, lineText, snapshot.statement, ); } function playRunMotion(resultText, programText) { clearRunPlayback(); const snapshots = parseRunMotion(resultText, programText); if (snapshots.length === 0) { setRunMonitor(100, "100%", {}, "complete; no axis motion"); return; } let index = 0; showRunSnapshot(snapshots[index], index, snapshots.length); runPlaybackTimer = setInterval(() => { index += 1; if (index >= snapshots.length) { clearRunPlayback(); showRunSnapshot(snapshots[snapshots.length - 1], snapshots.length - 1, snapshots.length); return; } showRunSnapshot(snapshots[index], index, snapshots.length); }, 220); } function requireIniSdk() { if (!iniSdk) { throw new Error("INI WASM module is not ready yet."); } return iniSdk; } function requireInterpSdk() { if (!interpSdk) { throw new Error("Interpreter WASM module is not ready yet."); } return interpSdk; } function syncEditorToWasmFs() { requireIniSdk().writeTextFile(WASM_FILE, editor.value); } async function loadSample() { setLog(`Fetching ${SAMPLE_PATH}`); const response = await fetch(SAMPLE_PATH); if (!response.ok) { throw new Error(`Cannot fetch sample INI (${response.status})`); } editor.value = await response.text(); setLog("Loaded LinuxCNC sample INI into the standalone editor."); } async function fetchTextFile(path) { const response = await fetch(path); if (!response.ok) { throw new Error(`Cannot fetch ${path} (${response.status})`); } return response.text(); } async function writeFetchedTextFile(interp, sourcePath, wasmPath) { interp.writeTextFile(wasmPath, await fetchTextFile(sourcePath)); } async function writeFiveAxisTrtMachineFiles(interp) { await writeFetchedTextFile( interp, `${FIVEAXIS_TRT_SOURCE_PATH}/xyzac-trt.ini`, `${FIVEAXIS_TRT_WASM_DIR}/xyzac-trt.ini`, ); await writeFetchedTextFile( interp, `${FIVEAXIS_TRT_SOURCE_PATH}/xyzac-trt.tbl`, `${FIVEAXIS_TRT_WASM_DIR}/xyzac-trt.tbl`, ); for (const filename of FIVEAXIS_REMAP_FILES) { await writeFetchedTextFile( interp, `${FIVEAXIS_TRT_SOURCE_PATH}/remap_subs/${filename}`, `${FIVEAXIS_TRT_WASM_DIR}/remap_subs/${filename}`, ); } await writeFetchedTextFile( interp, `${FIVEAXIS_TRT_SOURCE_PATH}/demos/impeller-7bl-xyzac.ngc`, `${FIVEAXIS_TRT_WASM_DIR}/demos/impeller-7bl-xyzac.ngc`, ); } async function boot() { try { iniSdk = await createLinuxCncIniSdk(); setBadge(wasmBadge, "INI WASM: ready"); } catch (error) { setBadge(wasmBadge, "INI WASM: failed", "badge danger"); setLog(`INI WASM init failed: ${error.message}`, true); throw error; } try { interpSdk = await createLinuxCncInterpSdk(); setBadge(interpBadge, "Interpreter WASM: ready"); } catch (error) { setBadge(interpBadge, "Interpreter WASM: failed", "badge danger"); setLog(`Interpreter WASM init failed: ${error.message}`, true); throw error; } try { await getOpfsRoot(); setBadge(opfsBadge, "OPFS: ready"); } catch (error) { setBadge(opfsBadge, "OPFS: unavailable", "badge danger"); setLog(`OPFS check failed: ${error.message}`, true); } } document.getElementById("load-sample").addEventListener("click", async () => { try { await loadSample(); } catch (error) { setLog(error.message, true); } }); document.getElementById("save-opfs").addEventListener("click", async () => { try { await saveTextFile(OPFS_FILE, editor.value); setLog(`Saved current INI text to OPFS at ${OPFS_FILE}`); } catch (error) { setLog(`OPFS save failed: ${error.message}`, true); } }); document.getElementById("save-machine-files").addEventListener("click", async () => { try { const paths = await saveMachineTextFiles(MACHINE_ID, { ini: editor.value, toolTable: DEFAULT_TOOL_TABLE, parameters: DEFAULT_PARAMETERS, }); const gcodePath = await saveGcodeProgram(GCODE_FILENAME, DEFAULT_GCODE); setLog( [ "Saved machine text files to OPFS.", `INI: ${paths.ini}`, `Tool table: ${paths.toolTable}`, `Parameters: ${paths.parameters}`, `G-code: ${gcodePath}`, ].join("\n"), ); } catch (error) { setLog(`Machine file save failed: ${error.message}`, true); } }); document.getElementById("save-session-snapshot").addEventListener("click", async () => { try { await saveMachineTextFiles(MACHINE_ID, { ini: editor.value, toolTable: DEFAULT_TOOL_TABLE, parameters: DEFAULT_PARAMETERS, }); await saveGcodeProgram(GCODE_FILENAME, DEFAULT_GCODE); const snapshot = await saveMachineSessionSnapshot( SESSION_SNAPSHOT_ID, MACHINE_ID, { filename: SESSION_SNAPSHOT_FILENAME, gcodeFilename: GCODE_FILENAME, metadata: { source: "ini-panel" }, }, ); setField("sessionIni", snapshot.payload.files.ini); setField("sessionParameters", snapshot.payload.files.parameters); setField("sessionToolTable", snapshot.payload.files.toolTable); setField("sessionGcode", snapshot.payload.files.gcode); setField("sessionSnapshot", `${SESSION_SNAPSHOT_ID}/${SESSION_SNAPSHOT_FILENAME}`); setLog( [ "Saved machine session snapshot to OPFS.", `Snapshot: ${SESSION_SNAPSHOT_ID}/${SESSION_SNAPSHOT_FILENAME}`, `INI: ${snapshot.payload.files.ini}`, `Parameter file: ${snapshot.payload.files.parameters}`, `Tool table file: ${snapshot.payload.files.toolTable}`, `G-code: ${snapshot.payload.files.gcode}`, ].join("\n"), ); } catch (error) { setLog(`Machine session snapshot save failed: ${error.message}`, true); } }); document.getElementById("restore-session-snapshot").addEventListener("click", async () => { try { const snapshot = await loadMachineSessionSnapshot( SESSION_SNAPSHOT_ID, MACHINE_ID, { filename: SESSION_SNAPSHOT_FILENAME }, ); const files = await loadMachineTextFiles(MACHINE_ID); editor.value = files.ini; setField("sessionIni", snapshot.payload.files.ini); setField("sessionParameters", snapshot.payload.files.parameters); setField("sessionToolTable", snapshot.payload.files.toolTable); setField("sessionGcode", snapshot.payload.files.gcode); setField("sessionSnapshot", `${snapshot.sessionId}/${SESSION_SNAPSHOT_FILENAME}`); setLog( [ "Restored machine session snapshot from OPFS.", `Snapshot: ${snapshot.sessionId}/${SESSION_SNAPSHOT_FILENAME}`, `INI: ${snapshot.payload.files.ini}`, `Parameter file: ${snapshot.payload.files.parameters}`, `Tool table file: ${snapshot.payload.files.toolTable}`, `G-code: ${snapshot.payload.files.gcode ?? "-"}`, ].join("\n"), ); } catch (error) { setLog(`Machine session snapshot restore failed: ${error.message}`, true); } }); document.getElementById("load-session").addEventListener("click", async () => { try { loadedSession = await loadMachineSessionFromOpfs( requireInterpSdk(), MACHINE_ID, { ...SESSION_WASM_FILES, iniSdk: requireIniSdk(), }, ); setField("sessionIni", loadedSession.ini.wasmPath); setField("sessionParameters", loadedSession.parameters.wasmPath); setField("sessionToolTable", loadedSession.toolTable.wasmPath); setLog( [ "Loaded machine session into the interpreter WASM filesystem.", `INI: ${loadedSession.ini.opfsPath} -> ${loadedSession.ini.wasmPath}`, `Parameter file: ${loadedSession.parameters.opfsPath} -> ${loadedSession.parameters.wasmPath}`, `Parameters: ${loadedSession.parameters.result.trim()}`, `Tool table file: ${loadedSession.toolTable.opfsPath} -> ${loadedSession.toolTable.wasmPath}`, `Tool table: ${loadedSession.toolTable.result.trim()}`, ].join("\n"), ); } catch (error) { setLog(`Machine session load failed: ${error.message}`, true); } }); document.getElementById("run-gcode").addEventListener("click", async () => { try { if (!loadedSession) { throw new Error("Load a machine session before running G-code."); } const interp = requireInterpSdk(); const programText = await loadGcodeProgram(GCODE_FILENAME); interp.writeTextFile(GCODE_WASM_FILE, programText); clearRunPlayback(); setField("runStatus", "running"); setRunMonitor(5, "running", {}, "running"); await nextFrame(); const result = interp.runFileWithIni(GCODE_WASM_FILE, loadedSession.ini.wasmPath); setField("sessionGcode", GCODE_WASM_FILE); setField("runStatus", "ok"); setCanonicalEvents(result.trim()); playRunMotion(result.trim(), programText); setLog( [ "Ran G-code through LinuxCNC interpreter WASM.", `Program: ${GCODE_WASM_FILE}`, `INI: ${loadedSession.ini.wasmPath}`, result.trim(), ].join("\n"), ); } catch (error) { setField("runStatus", "failed"); setCanonicalEvents(""); clearRunPlayback(); setRunMonitor(0, "failed", {}, "failed"); setLog(`G-code run failed: ${error.message}`, true); } }); document.getElementById("run-fiveaxis-remap").addEventListener("click", async () => { try { const interp = requireInterpSdk(); await writeFiveAxisTrtMachineFiles(interp); const iniPath = `${FIVEAXIS_TRT_WASM_DIR}/xyzac-trt.ini`; const programPath = `${FIVEAXIS_TRT_WASM_DIR}/demos/impeller-7bl-xyzac.ngc`; const result = interp.runFiveAxisRemapFile(programPath, iniPath); setField("fiveaxisRemap", "ok"); setCanonicalEvents(result.trim()); setLog( [ "Ran LinuxCNC 5-axis switchkins remap demo through interpreter WASM.", `Program: ${programPath}`, `INI: ${iniPath}`, result.trim(), ].join("\n"), ); } catch (error) { setField("fiveaxisRemap", "failed"); setCanonicalEvents(""); setLog(`5-axis remap run failed: ${error.message}`, true); } }); document.getElementById("load-opfs").addEventListener("click", async () => { try { editor.value = await loadTextFile(OPFS_FILE); setLog(`Loaded INI text from OPFS path ${OPFS_FILE}`); } catch (error) { setLog(`OPFS load failed: ${error.message}`, true); } }); document.getElementById("sync-wasm").addEventListener("click", () => { try { syncEditorToWasmFs(); setLog(`Synced editor contents to WASM filesystem at ${WASM_FILE}`); } catch (error) { setLog(`WASM sync failed: ${error.message}`, true); } }); document.getElementById("query").addEventListener("click", async () => { try { syncEditorToWasmFs(); const values = requireIniSdk().getFields(WASM_FILE, { machine: { section: "EMC", tag: "MACHINE" }, display: { section: "DISPLAY", tag: "DISPLAY" }, kinematics: { section: "KINS", tag: "KINEMATICS" }, joints: { section: "KINS", tag: "JOINTS" }, coordinates: { section: "TRAJ", tag: "COORDINATES" }, parameterFile: { section: "RS274NGC", tag: "PARAMETER_FILE" }, toolTable: { section: "EMCIO", tag: "TOOL_TABLE" }, }); for (const [name, value] of Object.entries(values)) { setField(name, value); } setLog("Queried LinuxCNC INI fields through the standalone WASM parser."); } catch (error) { setLog(`Query failed: ${error.message}`, true); } }); document.getElementById("load-machine-files").addEventListener("click", async () => { try { const files = await loadMachineTextFiles(MACHINE_ID); editor.value = files.ini; setLog( [ "Loaded machine text files from OPFS.", `INI bytes: ${files.ini.length}`, `Tool table bytes: ${files.toolTable.length}`, `Parameter bytes: ${files.parameters.length}`, ].join("\n"), ); } catch (error) { setLog(`Machine file load failed: ${error.message}`, true); } }); eventFilterNode.addEventListener("input", () => { renderCanonicalEvents(); }); boot().catch(() => {});