推进 INI 面板 machine session state snapshot helper

This commit is contained in:
2026-06-14 21:48:43 +08:00
parent 75818c1379
commit 7d57bce5de
6 changed files with 410 additions and 3 deletions

View File

@@ -30,11 +30,45 @@ reading ad hoc DOM text.
The returned snapshot preserves those nested objects and normalizes missing
badge entries to `null`.
## `createMachineSessionStateSnapshot(input)`
This helper builds the richer state snapshot used by the INI panel and exposed
as `window.linuxCncIniPanelMachineSessionState`.
It preserves the base panel state and adds:
```js
{
selectedProgram: { source: "snapshot", opfsPath: "...", wasmPath: "..." },
fields: {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
runStatus: "ok",
runSource: "snapshot",
runSnapshot: "ui-machine-session/ui-machine-session.json",
runWorkflow: "save-session-snapshot",
runOpfsPath: "linuxcnc/gcode/ui-session.ngc",
runWasmPath: "/work/ui-session.ngc"
},
lastAction: "run-gcode",
error: null
}
```
The INI panel updates this state after save, restore, load, run, and error
paths so browser tests and future API consumers can read one machine-readable
state object instead of collecting DOM fields and log text.
## Boundary Rules
- Do not parse G-code text in this helper.
- Do not infer CNC semantics from the nested summaries.
- Keep the helper as a bundling layer over already-observed UI boundary state.
- Keep workflow result objects as host/runtime boundary facts; this helper must
not promote, reinterpret, or synthesize CNC behavior.
## Validation

View File

@@ -45,6 +45,9 @@ import {
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow,
} from "./session-workflow.js";
import {
createMachineSessionStateSnapshot,
} from "./panel-state-summary.js";
const SAMPLE_PATH =
"../../../vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini";
@@ -146,6 +149,20 @@ let loadedSession = null;
let restoredSessionSnapshot = null;
let canonicalEventText = "";
let runPlaybackTimer = null;
let panelMachineSessionState = createMachineSessionStateSnapshot();
function updatePanelMachineSessionState(patch = {}) {
panelMachineSessionState = createMachineSessionStateSnapshot({
...panelMachineSessionState,
...patch,
fields: {
...panelMachineSessionState.fields,
...(patch.fields ?? {}),
},
});
window.linuxCncIniPanelMachineSessionState = panelMachineSessionState;
return panelMachineSessionState;
}
function setBadge(node, text, className = "badge") {
node.className = className;
@@ -377,6 +394,17 @@ function setLastRunSummary(summary) {
setField("runWorkflow", summary.workflow ?? "-");
setField("runOpfsPath", summary.programOpfsPath);
setField("runWasmPath", summary.programWasmPath);
updatePanelMachineSessionState({
run: summary,
fields: {
runStatus: summary.runStatus,
runSource: summary.programSource,
runSnapshot: summary.snapshotLabel ?? "-",
runWorkflow: summary.workflow ?? "-",
runOpfsPath: summary.programOpfsPath,
runWasmPath: summary.programWasmPath,
},
});
return summary;
}
@@ -463,27 +491,45 @@ async function boot() {
try {
iniSdk = await createLinuxCncIniSdk();
setBadge(wasmBadge, "INI WASM: ready");
updatePanelMachineSessionState({ badges: { ...panelMachineSessionState.badges, ini: "INI WASM: ready" } });
} catch (error) {
setBadge(wasmBadge, "INI WASM: failed", "badge danger");
setLog(`INI WASM init failed: ${error.message}`, true);
updatePanelMachineSessionState({
badges: { ...panelMachineSessionState.badges, ini: "INI WASM: failed" },
lastAction: "boot",
error: error.message,
});
throw error;
}
try {
interpSdk = await createLinuxCncInterpSdk();
setBadge(interpBadge, "Interpreter WASM: ready");
updatePanelMachineSessionState({ badges: { ...panelMachineSessionState.badges, interp: "Interpreter WASM: ready" } });
} catch (error) {
setBadge(interpBadge, "Interpreter WASM: failed", "badge danger");
setLog(`Interpreter WASM init failed: ${error.message}`, true);
updatePanelMachineSessionState({
badges: { ...panelMachineSessionState.badges, interp: "Interpreter WASM: failed" },
lastAction: "boot",
error: error.message,
});
throw error;
}
try {
await getOpfsRoot();
setBadge(opfsBadge, "OPFS: ready");
updatePanelMachineSessionState({ badges: { ...panelMachineSessionState.badges, opfs: "OPFS: ready" } });
} catch (error) {
setBadge(opfsBadge, "OPFS: unavailable", "badge danger");
setLog(`OPFS check failed: ${error.message}`, true);
updatePanelMachineSessionState({
badges: { ...panelMachineSessionState.badges, opfs: "OPFS: unavailable" },
lastAction: "boot",
error: error.message,
});
}
}
@@ -513,6 +559,11 @@ document.getElementById("save-machine-files").addEventListener("click", async ()
});
const gcodePath = await saveGcodeProgram(UI_SESSION.defaultGcodeFilename, DEFAULT_GCODE);
const summary = createMachineFileSaveSummary(paths, gcodePath);
updatePanelMachineSessionState({
machineFiles: summary,
lastAction: "save-machine-files",
error: null,
});
setLog(
[
"Saved machine text files to OPFS.",
@@ -544,6 +595,15 @@ document.getElementById("save-session-snapshot").addEventListener("click", async
for (const [name, value] of Object.entries(workflow.fields)) {
setField(name, value);
}
restoredSessionSnapshot = workflow.snapshot;
updatePanelMachineSessionState({
sessionSnapshot: workflow.summary,
sessionLoad: null,
selectedProgram: null,
fields: workflow.fields,
lastAction: "save-session-snapshot",
error: null,
});
setLog(
[
"Saved machine session snapshot to OPFS.",
@@ -551,6 +611,10 @@ document.getElementById("save-session-snapshot").addEventListener("click", async
].join("\n"),
);
} catch (error) {
updatePanelMachineSessionState({
lastAction: "save-session-snapshot",
error: error.message,
});
setLog(`Machine session snapshot save failed: ${error.message}`, true);
}
});
@@ -571,6 +635,15 @@ document.getElementById("restore-session-snapshot").addEventListener("click", as
setField("sessionToolTable", workflow.fields.sessionToolTable);
setField("sessionGcode", workflow.fields.sessionGcode);
setField("sessionSnapshot", workflow.fields.sessionSnapshot);
restoredSessionSnapshot = workflow.snapshot;
updatePanelMachineSessionState({
sessionSnapshot: workflow.summary,
sessionLoad: null,
selectedProgram: null,
fields: workflow.fields,
lastAction: "restore-session-snapshot",
error: null,
});
setLog(
[
"Restored machine session snapshot from OPFS.",
@@ -578,6 +651,10 @@ document.getElementById("restore-session-snapshot").addEventListener("click", as
].join("\n"),
);
} catch (error) {
updatePanelMachineSessionState({
lastAction: "restore-session-snapshot",
error: error.message,
});
setLog(`Machine session snapshot restore failed: ${error.message}`, true);
}
});
@@ -586,6 +663,16 @@ document.getElementById("load-session").addEventListener("click", async () => {
try {
await loadMachineSessionIntoWasm();
const loadSummary = createMachineSessionLoadSummary(loadedSession);
updatePanelMachineSessionState({
sessionLoad: loadSummary,
fields: {
sessionIni: loadSummary.iniWasmPath,
sessionParameters: loadSummary.parameterWasmPath,
sessionToolTable: loadSummary.toolTableWasmPath,
},
lastAction: "load-session",
error: null,
});
setLog(
[
"Loaded machine session into the interpreter WASM filesystem.",
@@ -593,6 +680,10 @@ document.getElementById("load-session").addEventListener("click", async () => {
].join("\n"),
);
} catch (error) {
updatePanelMachineSessionState({
lastAction: "load-session",
error: error.message,
});
setLog(`Machine session load failed: ${error.message}`, true);
}
});
@@ -614,6 +705,18 @@ document.getElementById("restore-load-session").addEventListener("click", async
setField("sessionToolTable", workflow.fields.sessionToolTable);
setField("sessionGcode", workflow.fields.sessionGcode);
setField("sessionSnapshot", workflow.fields.sessionSnapshot);
restoredSessionSnapshot = workflow.snapshot;
if (workflow.loadedSession) {
loadedSession = workflow.loadedSession;
}
updatePanelMachineSessionState({
sessionSnapshot: workflow.summary,
sessionLoad: workflow.loadSummary,
selectedProgram: null,
fields: workflow.fields,
lastAction: "restore-load-session",
error: null,
});
setLog(
[
"Restored and loaded machine session snapshot.",
@@ -621,6 +724,10 @@ document.getElementById("restore-load-session").addEventListener("click", async
].join("\n"),
);
} catch (error) {
updatePanelMachineSessionState({
lastAction: "restore-load-session",
error: error.message,
});
setLog(`Machine session snapshot restore/load failed: ${error.message}`, true);
}
});
@@ -631,8 +738,6 @@ document.getElementById("run-gcode").addEventListener("click", async () => {
throw new Error("Load a machine session before running G-code.");
}
const interp = requireInterpSdk();
const program = await loadSelectedGcodeProgram();
interp.writeTextFile(program.wasmPath, program.text);
clearRunPlayback();
setField("runStatus", "running");
clearLastRunSummary();
@@ -659,6 +764,17 @@ document.getElementById("run-gcode").addEventListener("click", async () => {
setField("sessionGcode", workflow.fields.sessionGcode);
setField("runStatus", workflow.fields.runStatus);
setLastRunSummary(workflow.summary);
updatePanelMachineSessionState({
selectedProgram: {
source: workflow.program.source,
opfsPath: workflow.program.opfsPath,
wasmPath: workflow.program.wasmPath,
},
run: workflow.summary,
fields: workflow.fields,
lastAction: "run-gcode",
error: null,
});
setCanonicalEvents(workflow.resultText);
playRunMotion(workflow.resultText, workflow.program.text, workflow.motionSnapshots);
setLog(
@@ -673,6 +789,13 @@ document.getElementById("run-gcode").addEventListener("click", async () => {
clearRunPlayback();
clearLastRunSummary();
setRunMonitor(0, "failed", {}, "failed");
updatePanelMachineSessionState({
fields: {
runStatus: "failed",
},
lastAction: "run-gcode",
error: error.message,
});
setLog(`G-code run failed: ${error.message}`, true);
}
});

View File

@@ -21,3 +21,45 @@ export function createIniPanelStateSummary({
fiveaxisRemap: fiveaxisRemap ?? null,
};
}
export function createMachineSessionStateSnapshot({
badges = {},
machine = null,
machineFiles = null,
sessionSnapshot = null,
sessionLoad = null,
selectedProgram = null,
run = null,
fields = {},
lastAction = null,
error = null,
fiveaxisRemap = null,
} = {}) {
return {
...createIniPanelStateSummary({
badges,
machine,
machineFiles,
sessionSnapshot,
sessionLoad,
run,
fiveaxisRemap,
}),
selectedProgram,
fields: {
sessionIni: fields.sessionIni ?? null,
sessionParameters: fields.sessionParameters ?? null,
sessionToolTable: fields.sessionToolTable ?? null,
sessionGcode: fields.sessionGcode ?? null,
sessionSnapshot: fields.sessionSnapshot ?? null,
runStatus: fields.runStatus ?? null,
runSource: fields.runSource ?? null,
runSnapshot: fields.runSnapshot ?? null,
runWorkflow: fields.runWorkflow ?? null,
runOpfsPath: fields.runOpfsPath ?? null,
runWasmPath: fields.runWasmPath ?? null,
},
lastAction: lastAction ?? null,
error: error ?? null,
};
}

View File

@@ -660,6 +660,37 @@ TOOL_TABLE = browser-tool.tbl
2,
"UI snapshot run summary motion snapshot count",
);
const machineSessionState = uiDocument.defaultView.linuxCncIniPanelMachineSessionState;
assertEqual(
machineSessionState.lastAction,
"run-gcode",
"UI machine session state last action",
);
assertEqual(
machineSessionState.selectedProgram.opfsPath,
"linuxcnc/gcode/ui-session.ngc",
"UI machine session state selected program OPFS path",
);
assertEqual(
machineSessionState.sessionLoad.iniWasmPath,
"/work/session-machine.ini",
"UI machine session state loaded INI WASM path",
);
assertEqual(
machineSessionState.run.programSource,
"snapshot",
"UI machine session state run source",
);
assertEqual(
machineSessionState.fields.runWorkflow,
"save-session-snapshot",
"UI machine session state run workflow field",
);
assertEqual(
machineSessionState.error,
null,
"UI machine session state error",
);
if (
!runLog.includes("canon_event=STRAIGHT_TRAVERSE") ||
!runLog.includes("canon_event=STRAIGHT_FEED")

View File

@@ -1,6 +1,9 @@
import assert from "node:assert/strict";
import { createIniPanelStateSummary } from "../../../runtime/ui/ini-panel/panel-state-summary.js";
import {
createIniPanelStateSummary,
createMachineSessionStateSnapshot,
} from "../../../runtime/ui/ini-panel/panel-state-summary.js";
const summary = createIniPanelStateSummary({
badges: {
@@ -81,4 +84,106 @@ assert.deepEqual(
},
);
assert.deepEqual(
createMachineSessionStateSnapshot({
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
machine: {
machine: "xyzab-tdr",
},
sessionSnapshot: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
},
sessionLoad: {
iniWasmPath: "/work/session-machine.ini",
},
selectedProgram: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
},
run: {
runStatus: "ok",
programSource: "snapshot",
},
fields: {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
runStatus: "ok",
runSource: "snapshot",
runSnapshot: "ui-machine-session/ui-machine-session.json",
runWorkflow: "save-session-snapshot",
runOpfsPath: "linuxcnc/gcode/ui-session.ngc",
runWasmPath: "/work/ui-session.ngc",
},
lastAction: "run-gcode",
fiveaxisRemap: "ok",
}),
{
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
machine: {
machine: "xyzab-tdr",
},
machineFiles: null,
sessionSnapshot: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
},
sessionLoad: {
iniWasmPath: "/work/session-machine.ini",
},
run: {
runStatus: "ok",
programSource: "snapshot",
},
fiveaxisRemap: "ok",
selectedProgram: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
},
fields: {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
runStatus: "ok",
runSource: "snapshot",
runSnapshot: "ui-machine-session/ui-machine-session.json",
runWorkflow: "save-session-snapshot",
runOpfsPath: "linuxcnc/gcode/ui-session.ngc",
runWasmPath: "/work/ui-session.ngc",
},
lastAction: "run-gcode",
error: null,
},
);
assert.deepEqual(
createMachineSessionStateSnapshot().fields,
{
sessionIni: null,
sessionParameters: null,
sessionToolTable: null,
sessionGcode: null,
sessionSnapshot: null,
runStatus: null,
runSource: null,
runSnapshot: null,
runWorkflow: null,
runOpfsPath: null,
runWasmPath: null,
},
);
console.log("ini_panel_state_summary_node_smoke=ok");