推进 INI 面板 run G-code workflow helper

This commit is contained in:
2026-06-14 21:38:37 +08:00
parent 8e8a319d78
commit 75818c1379
7 changed files with 412 additions and 66 deletions

View File

@@ -3,9 +3,13 @@
## Purpose
`runtime/ui/ini-panel/run-summary.js` contains small host-boundary helpers for
summarizing an INI panel G-code run. These helpers are intentionally pure so
browser UI panels, SDK-facing wrappers, and Node smoke tests can share the same
summary shape without reading DOM text or parsing log output.
summarizing an INI panel G-code run.
`runtime/ui/ini-panel/run-workflow.js` contains the reusable UI workflow for
selecting the session/default G-code program, loading it from OPFS, staging it
into the interpreter WASM filesystem, invoking the LinuxCNC interpreter, and
returning run field/log state. These helpers are intentionally pure so browser
UI panels, SDK-facing wrappers, and Node smoke tests can share the same summary
shape without reading DOM text or parsing log output.
The helpers do not implement CNC behavior. G-code execution, canonical events,
tool handling, parameters, kinematics, and planner behavior remain owned by the
@@ -74,11 +78,53 @@ The helper returns:
`canonicalEventCount` only counts result lines beginning with `canon_event=`.
`motionSnapshotCount` is only `snapshots.length`.
## `runGcodeSessionWorkflow(input)`
The workflow requires a loaded machine session and injected host/runtime
functions:
```js
{
interp,
loadedSession,
snapshot,
loadGcodeProgram,
gcodeFilenameFromProgramPath,
summarizeRun
}
```
It returns the selected program mapping, raw interpreter result text, run
summary, UI field state, log lines, and optional UI motion snapshots produced
by the injected `summarizeRun` callback:
```js
{
program: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
text: "G0 X1\n"
},
resultText: "canon_event=...",
summary: { runStatus: "ok", programSource: "snapshot" },
fields: { runStatus: "ok", sessionGcode: "linuxcnc/gcode/ui-session.ngc" },
motionSnapshots: [{ line: 1 }],
logLines: ["Program: linuxcnc/gcode/ui-session.ngc -> /work/ui-session.ngc"]
}
```
`selectSessionGcodeProgram(input)` is the pure program-selection helper used by
the workflow. Invalid snapshot G-code paths fall back to the UI default program.
## Boundary Rules
- Do not parse G-code text in these helpers.
- Do not infer tool, parameter, kinematics, planner, or modal semantics.
- Do not derive new canonical events in JavaScript.
- Keep canonical-event and motion parsing in the existing UI display layer or
inject it as already-observed UI state; the workflow must not own CNC
semantics.
- Do not make UI helper output a promotion signal for runtime families blocked
by host dependencies.
- Keep new fields limited to host/runtime boundary facts already produced by
@@ -90,6 +136,7 @@ Run the focused Node smoke after changing these helpers:
```bash
wasm-port/tests/ui/node/verify_ini_panel_run_summary.sh
wasm-port/tests/ui/node/verify_ini_panel_run_workflow.sh
```
The aggregate UI Node smoke is:

View File

@@ -29,16 +29,16 @@ import {
machineFileSaveLogLines,
} from "./machine-file-summary.js";
import {
createRunSummary,
summarizeRunResult,
} from "./run-summary.js";
import {
runGcodeSessionWorkflow,
} from "./run-workflow.js";
import {
createMachineSessionLoadSummary,
machineSessionLoadLogLines,
} from "./session-load-summary.js";
import {
createSessionSnapshotSummary,
sessionSnapshotMetadataLogLines,
sessionSnapshotSummaryLogLines,
} from "./session-summary.js";
import {
@@ -361,19 +361,6 @@ function sessionSnapshotMetadata() {
};
}
function sessionMetadataLogLines(metadata = {}) {
return sessionSnapshotMetadataLogLines(createSessionSnapshotSummary({ metadata }));
}
function runSessionContextLogLines(program, snapshot = restoredSessionSnapshot) {
return [
`Program: ${program.opfsPath} -> ${program.wasmPath}`,
`Program source: ${program.source}`,
`Snapshot: ${snapshot ? sessionSnapshotLabel(snapshot.sessionId) : "-"}`,
...sessionMetadataLogLines(snapshot?.metadata),
];
}
function clearLastRunSummary() {
window.linuxCncIniPanelLastRunSummary = null;
setField("runSource", "-");
@@ -383,15 +370,7 @@ function clearLastRunSummary() {
setField("runWasmPath", "-");
}
function setLastRunSummary(program, runtime = {}, snapshot = restoredSessionSnapshot) {
const summary = createRunSummary(
program,
{
...runtime,
snapshotLabel: snapshot ? sessionSnapshotLabel(snapshot.sessionId) : null,
},
snapshot,
);
function setLastRunSummary(summary) {
window.linuxCncIniPanelLastRunSummary = summary;
setField("runSource", summary.programSource);
setField("runSnapshot", summary.snapshotLabel ?? "-");
@@ -401,29 +380,6 @@ function setLastRunSummary(program, runtime = {}, snapshot = restoredSessionSnap
return summary;
}
async function loadSelectedGcodeProgram() {
const opfsPath = restoredSessionSnapshot?.payload?.files?.gcode;
let filename = UI_SESSION.defaultGcodeFilename;
let selectedOpfsPath = defaultGcodeOpfsPath();
let source = "default";
if (typeof opfsPath === "string") {
try {
filename = gcodeFilenameFromProgramPath(opfsPath);
selectedOpfsPath = opfsPath;
source = "snapshot";
} catch {
filename = UI_SESSION.defaultGcodeFilename;
}
}
return {
filename,
opfsPath: selectedOpfsPath,
source,
wasmPath: UI_SESSION.gcodeWasmPath,
text: await loadGcodeProgram(filename),
};
}
async function restoreSessionSnapshotToEditor() {
const snapshot = await loadMachineSessionSnapshot(
UI_SESSION.snapshotId,
@@ -682,25 +638,33 @@ document.getElementById("run-gcode").addEventListener("click", async () => {
clearLastRunSummary();
setRunMonitor(5, "running", {}, "running");
await nextFrame();
const result = interp.runFileWithIni(program.wasmPath, loadedSession.ini.wasmPath);
const trimmedResult = result.trim();
const motionSnapshots = parseRunMotion(trimmedResult, program.text);
const runResultSummary = summarizeRunResult(trimmedResult, motionSnapshots);
setField("sessionGcode", program.opfsPath);
setField("runStatus", "ok");
setLastRunSummary(program, {
runStatus: "ok",
iniWasmPath: loadedSession.ini.wasmPath,
...runResultSummary,
const workflow = await runGcodeSessionWorkflow({
interp,
loadedSession,
snapshot: restoredSessionSnapshot,
defaultFilename: UI_SESSION.defaultGcodeFilename,
defaultOpfsPath: defaultGcodeOpfsPath(),
wasmPath: UI_SESSION.gcodeWasmPath,
gcodeFilenameFromProgramPath,
loadGcodeProgram,
snapshotLabel: restoredSessionSnapshot ? sessionSnapshotLabel(restoredSessionSnapshot.sessionId) : null,
summarizeRun: (resultText, programText) => {
const motionSnapshots = parseRunMotion(resultText, programText);
return {
...summarizeRunResult(resultText, motionSnapshots),
motionSnapshots,
};
},
});
setCanonicalEvents(trimmedResult);
playRunMotion(trimmedResult, program.text, motionSnapshots);
setField("sessionGcode", workflow.fields.sessionGcode);
setField("runStatus", workflow.fields.runStatus);
setLastRunSummary(workflow.summary);
setCanonicalEvents(workflow.resultText);
playRunMotion(workflow.resultText, workflow.program.text, workflow.motionSnapshots);
setLog(
[
"Ran G-code through LinuxCNC interpreter WASM.",
...runSessionContextLogLines(program),
`INI: ${loadedSession.ini.wasmPath}`,
trimmedResult,
...workflow.logLines,
].join("\n"),
);
} catch (error) {

View File

@@ -0,0 +1,109 @@
import {
createRunSummary,
} from "./run-summary.js";
import {
createSessionSnapshotSummary,
sessionSnapshotMetadataLogLines,
} from "./session-summary.js";
export function selectSessionGcodeProgram({
snapshot = null,
defaultFilename,
defaultOpfsPath,
wasmPath,
gcodeFilenameFromProgramPath,
}) {
const snapshotOpfsPath = snapshot?.payload?.files?.gcode;
if (typeof snapshotOpfsPath === "string") {
try {
return {
filename: gcodeFilenameFromProgramPath(snapshotOpfsPath),
opfsPath: snapshotOpfsPath,
source: "snapshot",
wasmPath,
};
} catch {
// Invalid snapshot program paths fall back to the UI default program.
}
}
return {
filename: defaultFilename,
opfsPath: defaultOpfsPath,
source: "default",
wasmPath,
};
}
export function runSessionContextLogLines(program, snapshot = null, snapshotLabel = null) {
const summary = createSessionSnapshotSummary({
metadata: snapshot?.metadata ?? {},
});
return [
`Program: ${program.opfsPath} -> ${program.wasmPath}`,
`Program source: ${program.source}`,
`Snapshot: ${snapshot ? (snapshotLabel ?? snapshot?.metadata?.snapshotLabel ?? snapshot?.sessionId) : "-"}`,
...sessionSnapshotMetadataLogLines(summary),
];
}
export async function runGcodeSessionWorkflow({
interp,
loadedSession,
snapshot = null,
defaultFilename,
defaultOpfsPath,
wasmPath,
gcodeFilenameFromProgramPath,
loadGcodeProgram,
summarizeRun = () => ({}),
snapshotLabel = null,
}) {
if (!loadedSession) {
throw new Error("Load a machine session before running G-code.");
}
const program = selectSessionGcodeProgram({
snapshot,
defaultFilename,
defaultOpfsPath,
wasmPath,
gcodeFilenameFromProgramPath,
});
const programText = await loadGcodeProgram(program.filename);
interp.writeTextFile(program.wasmPath, programText);
const resultText = interp.runFileWithIni(program.wasmPath, loadedSession.ini.wasmPath).trim();
const runResultSummary = summarizeRun(resultText, programText);
const { motionSnapshots = [], ...runCounts } = runResultSummary;
const summary = createRunSummary(
program,
{
runStatus: "ok",
iniWasmPath: loadedSession.ini.wasmPath,
snapshotLabel: snapshot ? snapshotLabel : null,
...runCounts,
},
snapshot,
);
return {
program: {
...program,
text: programText,
},
resultText,
motionSnapshots,
summary,
fields: {
sessionGcode: program.opfsPath,
runStatus: summary.runStatus,
runSource: summary.programSource,
runSnapshot: summary.snapshotLabel ?? "-",
runWorkflow: summary.workflow ?? "-",
runOpfsPath: summary.programOpfsPath,
runWasmPath: summary.programWasmPath,
},
logLines: [
...runSessionContextLogLines(program, snapshot, summary.snapshotLabel),
`INI: ${loadedSession.ini.wasmPath}`,
resultText,
],
};
}

View File

@@ -0,0 +1,149 @@
import assert from "node:assert/strict";
import {
runGcodeSessionWorkflow,
selectSessionGcodeProgram,
} from "../../../runtime/ui/ini-panel/run-workflow.js";
const defaultProgram = selectSessionGcodeProgram({
defaultFilename: "ui-session.ngc",
defaultOpfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
gcodeFilenameFromProgramPath: () => {
throw new Error("not used");
},
});
assert.deepEqual(defaultProgram, {
filename: "ui-session.ngc",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
source: "default",
wasmPath: "/work/ui-session.ngc",
});
const snapshotProgram = selectSessionGcodeProgram({
snapshot: {
payload: {
files: {
gcode: "linuxcnc/gcode/snapshot-session.ngc",
},
},
},
defaultFilename: "ui-session.ngc",
defaultOpfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
gcodeFilenameFromProgramPath: (path) => path.split("/").at(-1),
});
assert.deepEqual(snapshotProgram, {
filename: "snapshot-session.ngc",
opfsPath: "linuxcnc/gcode/snapshot-session.ngc",
source: "snapshot",
wasmPath: "/work/ui-session.ngc",
});
const calls = [];
const workflow = await runGcodeSessionWorkflow({
interp: {
writeTextFile: (wasmPath, text) => {
calls.push(["writeTextFile", wasmPath, text]);
},
runFileWithIni: (programWasmPath, iniWasmPath) => {
calls.push(["runFileWithIni", programWasmPath, iniWasmPath]);
return [
"canon_event=STRAIGHT_FEED line=1 x=1 y=2",
"run_step phase=execute line=1 x=1 y=2 statement_uri=G1%20X1%20Y2",
"",
].join("\n");
},
},
loadedSession: {
ini: {
wasmPath: "/work/session-machine.ini",
},
},
snapshot: {
sessionId: "ui-machine-session",
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
gcode: "linuxcnc/gcode/snapshot-session.ngc",
},
},
},
defaultFilename: "ui-session.ngc",
defaultOpfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
gcodeFilenameFromProgramPath: (path) => path.split("/").at(-1),
loadGcodeProgram: async (filename) => {
calls.push(["loadGcodeProgram", filename]);
return "G1 X1 Y2\n";
},
snapshotLabel: "ui-machine-session/ui-machine-session.json",
summarizeRun: (resultText, programText) => {
calls.push(["summarizeRun", resultText, programText]);
return {
canonicalEventCount: 1,
motionSnapshotCount: 1,
motionSnapshots: [{ line: 1 }],
};
},
});
assert.deepEqual(calls, [
["loadGcodeProgram", "snapshot-session.ngc"],
["writeTextFile", "/work/ui-session.ngc", "G1 X1 Y2\n"],
["runFileWithIni", "/work/ui-session.ngc", "/work/session-machine.ini"],
[
"summarizeRun",
"canon_event=STRAIGHT_FEED line=1 x=1 y=2\nrun_step phase=execute line=1 x=1 y=2 statement_uri=G1%20X1%20Y2",
"G1 X1 Y2\n",
],
]);
assert.deepEqual(workflow.summary, {
runStatus: "ok",
programSource: "snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
programOpfsPath: "linuxcnc/gcode/snapshot-session.ngc",
programWasmPath: "/work/ui-session.ngc",
iniWasmPath: "/work/session-machine.ini",
canonicalEventCount: 1,
motionSnapshotCount: 1,
});
assert.deepEqual(workflow.fields, {
sessionGcode: "linuxcnc/gcode/snapshot-session.ngc",
runStatus: "ok",
runSource: "snapshot",
runSnapshot: "ui-machine-session/ui-machine-session.json",
runWorkflow: "save-session-snapshot",
runOpfsPath: "linuxcnc/gcode/snapshot-session.ngc",
runWasmPath: "/work/ui-session.ngc",
});
assert.deepEqual(workflow.motionSnapshots, [{ line: 1 }]);
assert.deepEqual(workflow.logLines, [
"Program: linuxcnc/gcode/snapshot-session.ngc -> /work/ui-session.ngc",
"Program source: snapshot",
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"INI: /work/session-machine.ini",
"canon_event=STRAIGHT_FEED line=1 x=1 y=2\nrun_step phase=execute line=1 x=1 y=2 statement_uri=G1%20X1%20Y2",
]);
await assert.rejects(
() => runGcodeSessionWorkflow({
loadedSession: null,
}),
/Load a machine session before running G-code/,
);
console.log("ini_panel_run_workflow_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_ini_panel_run_workflow.mjs"

View File

@@ -4,6 +4,7 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_run_summary.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_run_workflow.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_session_summary.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_session_workflow.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_machine_file_summary.sh"