推进 INI 面板 restore/load session workflow helper

This commit is contained in:
2026-06-14 21:30:19 +08:00
parent ddc9a4e0f3
commit 8e8a319d78
5 changed files with 364 additions and 22 deletions

View File

@@ -8,9 +8,9 @@ for summarizing machine session snapshot state.
for machine-session load state. These helpers are pure so browser UI panels,
SDK-facing wrappers, and Node smoke tests can share the same data shape without
parsing log text.
`runtime/ui/ini-panel/session-workflow.js` contains the reusable UI workflow for
saving the machine text files, default G-code, and machine session snapshot as
one host-boundary operation.
`runtime/ui/ini-panel/session-workflow.js` contains the reusable UI workflows
for saving, restoring, and restore-loading a machine session snapshot as
host-boundary operations.
The helpers do not implement CNC behavior. G-code execution, canonical events,
tool handling, parameters, kinematics, and planner behavior remain owned by the
@@ -72,6 +72,40 @@ The persistence functions are injected by the caller:
}
```
## `restoreMachineSessionSnapshotWorkflow(input)`
The workflow loads the machine session snapshot, loads the persisted machine
text files so the editor can be restored, and optionally loads the machine
session into the interpreter WASM filesystem.
When `loadSession` is omitted, the returned `fields` contain the OPFS snapshot
paths. When `loadSession` is supplied, the returned `fields` contain the loaded
WASM paths for INI/parameter/tool-table files and the snapshot G-code OPFS
path.
The returned object has this shape:
```js
{
snapshot: { ... },
editorIniText: "[EMC]\n...",
summary: { snapshotLabel: "...", workflow: "save-session-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"
},
loadSummary: { iniWasmPath: "/work/session-machine.ini" },
loadedSession: { ... },
logLines: [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot"
]
}
```
The returned object has this shape:
```js
@@ -102,6 +136,8 @@ The returned object has this shape:
OPFS-to-WASM mapping and captured result lines.
- `sessionSnapshotSaveLogLines(summary)` renders the save-workflow snapshot
paths used by the INI panel.
- `sessionSnapshotRestoreLogLines(summary)` renders the restore-workflow
snapshot paths used by the INI panel.
## Boundary Rules

View File

@@ -42,6 +42,7 @@ import {
sessionSnapshotSummaryLogLines,
} from "./session-summary.js";
import {
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow,
} from "./session-workflow.js";
@@ -600,16 +601,24 @@ document.getElementById("save-session-snapshot").addEventListener("click", async
document.getElementById("restore-session-snapshot").addEventListener("click", async () => {
try {
const snapshot = await restoreSessionSnapshotToEditor();
const summary = createSessionSnapshotSummary(snapshot, sessionSnapshotLabel(snapshot.sessionId));
const workflow = await restoreMachineSessionSnapshotWorkflow({
machineId: MACHINE_ID,
snapshotId: UI_SESSION.snapshotId,
snapshotFilename: UI_SESSION.snapshotFilename,
snapshotLabel: sessionSnapshotLabel(UI_SESSION),
loadMachineSessionSnapshot,
loadMachineTextFiles,
});
editor.value = workflow.editorIniText;
setField("sessionIni", workflow.fields.sessionIni);
setField("sessionParameters", workflow.fields.sessionParameters);
setField("sessionToolTable", workflow.fields.sessionToolTable);
setField("sessionGcode", workflow.fields.sessionGcode);
setField("sessionSnapshot", workflow.fields.sessionSnapshot);
setLog(
[
"Restored machine session snapshot from OPFS.",
...sessionSnapshotSummaryLogLines(summary),
`INI: ${summary.iniOpfsPath}`,
`Parameter file: ${summary.parameterOpfsPath}`,
`Tool table file: ${summary.toolTableOpfsPath}`,
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
...workflow.logLines,
].join("\n"),
);
} catch (error) {
@@ -634,17 +643,25 @@ document.getElementById("load-session").addEventListener("click", async () => {
document.getElementById("restore-load-session").addEventListener("click", async () => {
try {
const snapshot = await restoreSessionSnapshotToEditor();
await loadMachineSessionIntoWasm();
setField("sessionGcode", snapshot.payload.files.gcode);
const summary = createSessionSnapshotSummary(snapshot, sessionSnapshotLabel(snapshot.sessionId));
const loadSummary = createMachineSessionLoadSummary(loadedSession);
const workflow = await restoreMachineSessionSnapshotWorkflow({
machineId: MACHINE_ID,
snapshotId: UI_SESSION.snapshotId,
snapshotFilename: UI_SESSION.snapshotFilename,
snapshotLabel: sessionSnapshotLabel(UI_SESSION),
loadMachineSessionSnapshot,
loadMachineTextFiles,
loadSession: () => loadMachineSessionIntoWasm(),
});
editor.value = workflow.editorIniText;
setField("sessionIni", workflow.fields.sessionIni);
setField("sessionParameters", workflow.fields.sessionParameters);
setField("sessionToolTable", workflow.fields.sessionToolTable);
setField("sessionGcode", workflow.fields.sessionGcode);
setField("sessionSnapshot", workflow.fields.sessionSnapshot);
setLog(
[
"Restored and loaded machine session snapshot.",
...sessionSnapshotSummaryLogLines(summary),
...machineSessionLoadLogLines(loadSummary),
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
...workflow.logLines,
].join("\n"),
);
} catch (error) {

View File

@@ -1,3 +1,7 @@
import {
createMachineSessionLoadSummary,
machineSessionLoadLogLines,
} from "./session-load-summary.js";
import {
createSessionSnapshotSummary,
sessionSnapshotSummaryLogLines,
@@ -23,6 +27,16 @@ export function sessionSnapshotSaveLogLines(summary) {
];
}
export function sessionSnapshotRestoreLogLines(summary) {
return [
...sessionSnapshotSummaryLogLines(summary),
`INI: ${summary.iniOpfsPath}`,
`Parameter file: ${summary.parameterOpfsPath}`,
`Tool table file: ${summary.toolTableOpfsPath}`,
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
];
}
export async function saveMachineSessionSnapshotWorkflow({
machineId,
snapshotId,
@@ -63,3 +77,54 @@ export async function saveMachineSessionSnapshotWorkflow({
logLines: sessionSnapshotSaveLogLines(summary),
};
}
export async function restoreMachineSessionSnapshotWorkflow({
machineId,
snapshotId,
snapshotFilename,
snapshotLabel,
loadMachineSessionSnapshot,
loadMachineTextFiles,
loadSession = null,
}) {
const snapshot = await loadMachineSessionSnapshot(
snapshotId,
machineId,
{ filename: snapshotFilename },
);
const files = await loadMachineTextFiles(machineId);
const summary = createSessionSnapshotSummary(snapshot, snapshotLabel ?? snapshotLabelFromSession(snapshot));
const fields = createSessionSnapshotFieldState(summary);
const result = {
snapshot,
editorIniText: files.ini,
summary,
fields,
loadSummary: null,
loadedSession: null,
logLines: sessionSnapshotRestoreLogLines(summary),
};
if (loadSession) {
const loadedSession = await loadSession();
const loadSummary = createMachineSessionLoadSummary(loadedSession);
result.loadedSession = loadedSession;
result.loadSummary = loadSummary;
result.fields = {
...fields,
sessionIni: loadSummary.iniWasmPath,
sessionParameters: loadSummary.parameterWasmPath,
sessionToolTable: loadSummary.toolTableWasmPath,
sessionGcode: summary.gcodeOpfsPath,
};
result.logLines = [
...sessionSnapshotSummaryLogLines(summary),
...machineSessionLoadLogLines(loadSummary),
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
];
}
return result;
}
function snapshotLabelFromSession(snapshot) {
return snapshot?.metadata?.snapshotLabel ?? snapshot?.sessionId ?? null;
}

View File

@@ -1,11 +1,12 @@
import assert from "node:assert/strict";
import {
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow,
} from "../../../runtime/ui/ini-panel/session-workflow.js";
const calls = [];
const workflow = await saveMachineSessionSnapshotWorkflow({
const saveWorkflow = await saveMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json",
@@ -76,7 +77,7 @@ assert.deepEqual(calls, [
],
]);
assert.deepEqual(workflow.summary, {
assert.deepEqual(saveWorkflow.summary, {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
@@ -86,7 +87,7 @@ assert.deepEqual(workflow.summary, {
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
});
assert.deepEqual(workflow.fields, {
assert.deepEqual(saveWorkflow.fields, {
sessionIni: "linuxcnc/machines/xyzab-tdr/machine.ini",
sessionParameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
sessionToolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
@@ -94,7 +95,7 @@ assert.deepEqual(workflow.fields, {
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
});
assert.deepEqual(workflow.logLines, [
assert.deepEqual(saveWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
@@ -104,4 +105,148 @@ assert.deepEqual(workflow.logLines, [
"G-code: linuxcnc/gcode/ui-session.ngc",
]);
const restoreCalls = [];
const restoreWorkflow = await restoreMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
loadMachineSessionSnapshot: async (snapshotId, machineId, options) => {
restoreCalls.push(["loadMachineSessionSnapshot", snapshotId, machineId, options]);
return {
sessionId: snapshotId,
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcode: "linuxcnc/gcode/ui-session.ngc",
},
},
};
},
loadMachineTextFiles: async (machineId) => {
restoreCalls.push(["loadMachineTextFiles", machineId]);
return {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
};
},
});
assert.deepEqual(restoreCalls, [
[
"loadMachineSessionSnapshot",
"ui-machine-session",
"xyzab-tdr",
{ filename: "ui-machine-session.json" },
],
["loadMachineTextFiles", "xyzab-tdr"],
]);
assert.deepEqual(restoreWorkflow.fields, {
sessionIni: "linuxcnc/machines/xyzab-tdr/machine.ini",
sessionParameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
sessionToolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
});
assert.deepEqual(restoreWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"INI: linuxcnc/machines/xyzab-tdr/machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var",
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl",
"G-code: linuxcnc/gcode/ui-session.ngc",
]);
const restoreLoadCalls = [];
const restoreLoadWorkflow = await restoreMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
loadMachineSessionSnapshot: async (snapshotId, machineId, options) => {
restoreLoadCalls.push(["loadMachineSessionSnapshot", snapshotId, machineId, options]);
return {
sessionId: snapshotId,
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcode: "linuxcnc/gcode/ui-session.ngc",
},
},
};
},
loadMachineTextFiles: async (machineId) => {
restoreLoadCalls.push(["loadMachineTextFiles", machineId]);
return {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
};
},
loadSession: async () => {
restoreLoadCalls.push(["loadSession"]);
return {
ini: {
opfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
wasmPath: "/work/session-machine.ini",
},
parameters: {
opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
wasmPath: "/work/session-linuxcnc.var",
result: "restore_parameters=0",
},
toolTable: {
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
wasmPath: "/work/session-tool.tbl",
result: "tooldata_load=0",
},
};
},
});
assert.deepEqual(restoreLoadCalls, [
[
"loadMachineSessionSnapshot",
"ui-machine-session",
"xyzab-tdr",
{ filename: "ui-machine-session.json" },
],
["loadMachineTextFiles", "xyzab-tdr"],
["loadSession"],
]);
assert.deepEqual(restoreLoadWorkflow.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",
});
assert.deepEqual(restoreLoadWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"INI: linuxcnc/machines/xyzab-tdr/machine.ini -> /work/session-machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var -> /work/session-linuxcnc.var",
"Parameters: restore_parameters=0",
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl -> /work/session-tool.tbl",
"Tool table: tooldata_load=0",
"G-code: linuxcnc/gcode/ui-session.ngc",
]);
console.log("ini_panel_session_workflow_node_smoke=ok");