接入 INI panel session readiness workflow

This commit is contained in:
2026-06-16 06:36:45 +08:00
parent 8e37c69bc4
commit 03db34485e
8 changed files with 363 additions and 4 deletions

View File

@@ -77,6 +77,9 @@ import {
createMachineSessionControlPageController,
createMachineSessionReadonlyStatus,
createMachineSessionReadonlyStatusApiManifest,
createMachineSessionReadinessFieldState,
machineSessionReadinessLogLines,
readMachineSessionReadinessWorkflow,
readMachineSessionReadonlyStatus,
loadMachineSessionForControlPageWorkflow,
createControlPageSessionLoadState,
@@ -127,6 +130,11 @@ outer shell needs one stable read of `stateBundle`, `workflowStatus`,
Use `createMachineSessionReadonlyStatusApiManifest()` or
`controlPage.readonlyStatusApiManifest` when you only need the API shape and
method names.
Use `readMachineSessionReadinessWorkflow()` when an outer shell or panel flow
needs a read-only `ready`/`blocked` readiness report before loading a machine
session into WASM. `createMachineSessionReadinessFieldState()` and
`machineSessionReadinessLogLines()` turn that report into stable fields and
status text without parsing G-code or executing LinuxCNC behavior.
Use `createIniPanelShellControlPageContract()` or `controlPage.contract` when
an outer shell needs a fast contract check for `createController`,
`loadSessionState`, `readStatus`, `refresh`, `renderView`, the readonly status

View File

@@ -18,6 +18,9 @@ import {
import {
loadMachineSessionFromOpfs,
} from "../../opfs/linuxcnc-machine-session-bridge.js";
import {
readMachineSessionReadiness,
} from "../../opfs/machine-session-readiness.js";
import {
loadMachineSessionSnapshot,
saveMachineSessionSnapshot,
@@ -37,6 +40,7 @@ import {
} from "./run-workflow.js";
import {
loadMachineSessionIntoWasmWorkflow,
readMachineSessionReadinessWorkflow,
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow,
} from "./session-workflow.js";
@@ -486,6 +490,14 @@ async function loadMachineSessionIntoWasm() {
return loadedSession;
}
function readCurrentMachineSessionReadiness() {
return readMachineSessionReadiness(MACHINE_ID, {
sessionId: UI_SESSION.snapshotId,
snapshotFilename: UI_SESSION.snapshotFilename,
gcodeFilename: UI_SESSION.defaultGcodeFilename,
});
}
async function loadSample() {
setLog(`Fetching ${SAMPLE_PATH}`);
const response = await fetch(SAMPLE_PATH);
@@ -674,6 +686,7 @@ document.getElementById("restore-session-snapshot").addEventListener("click", as
snapshotLabel: sessionSnapshotLabel(UI_SESSION),
loadMachineSessionSnapshot,
loadMachineTextFiles,
readReadiness: readCurrentMachineSessionReadiness,
});
editor.value = workflow.editorIniText;
setField("sessionIni", workflow.fields.sessionIni);
@@ -684,6 +697,7 @@ document.getElementById("restore-session-snapshot").addEventListener("click", as
restoredSessionSnapshot = workflow.snapshot;
updatePanelMachineSessionState({
sessionSnapshot: workflow.summary,
sessionReadiness: workflow.readiness,
sessionLoad: null,
selectedProgram: null,
fields: workflow.fields,
@@ -707,17 +721,27 @@ document.getElementById("restore-session-snapshot").addEventListener("click", as
document.getElementById("load-session").addEventListener("click", async () => {
try {
const readinessWorkflow = await readMachineSessionReadinessWorkflow({
readReadiness: readCurrentMachineSessionReadiness,
});
const workflow = await loadMachineSessionIntoWasmWorkflow({
loadSession: () => loadMachineSessionIntoWasm(),
});
loadedSession = workflow.loadedSession;
updatePanelMachineSessionState({
sessionReadiness: readinessWorkflow.readiness,
sessionLoad: workflow.loadSummary,
fields: workflow.fields,
fields: {
...readinessWorkflow.fields,
...workflow.fields,
},
lastAction: "load-session",
error: null,
});
setLog(workflow.logLines.join("\n"));
setLog([
...readinessWorkflow.logLines,
...workflow.logLines,
].join("\n"));
} catch (error) {
updatePanelMachineSessionState({
lastAction: "load-session",
@@ -736,6 +760,7 @@ document.getElementById("restore-load-session").addEventListener("click", async
snapshotLabel: sessionSnapshotLabel(UI_SESSION),
loadMachineSessionSnapshot,
loadMachineTextFiles,
readReadiness: readCurrentMachineSessionReadiness,
loadSession: () => loadMachineSessionIntoWasm(),
});
editor.value = workflow.editorIniText;
@@ -750,6 +775,7 @@ document.getElementById("restore-load-session").addEventListener("click", async
}
updatePanelMachineSessionState({
sessionSnapshot: workflow.summary,
sessionReadiness: workflow.readiness,
sessionLoad: workflow.loadSummary,
selectedProgram: null,
fields: workflow.fields,

View File

@@ -52,6 +52,47 @@ export function machineSessionLoadWorkflowLogLines(summary) {
];
}
export function createMachineSessionReadinessFieldState(readiness) {
return {
sessionReadinessPhase: readiness.phase,
sessionReadinessReady: readiness.ready ? "ready" : "blocked",
sessionReadinessMissing: readiness.missing.length === 0 ? "none" : readiness.missing.join(", "),
};
}
export function machineSessionReadinessLogLines(readiness) {
const checks = Array.isArray(readiness.checks) ? readiness.checks : [];
return [
`Session readiness: ${readiness.ready ? "ready" : "blocked"}`,
...checks.map((check) => {
const status = check.ok ? "ok" : `blocked (${check.error})`;
return `${check.name}: ${check.path} -> ${status}`;
}),
...(readiness.snapshotCheck
? [
`snapshot: ${readiness.snapshotCheck.path} -> ${
readiness.snapshotCheck.ok ? "ok" : `blocked (${readiness.snapshotCheck.error})`
}`,
]
: []),
];
}
export async function readMachineSessionReadinessWorkflow({
readiness,
readReadiness,
} = {}) {
const sessionReadiness = readiness ?? await readReadiness?.();
if (!sessionReadiness) {
throw new Error("A machine session readiness reader is required.");
}
return {
readiness: sessionReadiness,
fields: createMachineSessionReadinessFieldState(sessionReadiness),
logLines: machineSessionReadinessLogLines(sessionReadiness),
};
}
export async function loadMachineSessionIntoWasmWorkflow({ loadSession }) {
if (typeof loadSession !== "function") {
throw new Error("A machine session loader is required.");
@@ -114,6 +155,7 @@ export async function restoreMachineSessionSnapshotWorkflow({
snapshotLabel,
loadMachineSessionSnapshot,
loadMachineTextFiles,
readReadiness = null,
loadSession = null,
}) {
const snapshot = await loadMachineSessionSnapshot(
@@ -129,21 +171,36 @@ export async function restoreMachineSessionSnapshotWorkflow({
editorIniText: files.ini,
summary,
fields,
readiness: null,
loadSummary: null,
loadedSession: null,
logLines: sessionSnapshotRestoreLogLines(summary),
};
if (readReadiness) {
const readinessWorkflow = await readMachineSessionReadinessWorkflow({ readReadiness });
result.readiness = readinessWorkflow.readiness;
result.fields = {
...result.fields,
...readinessWorkflow.fields,
};
result.logLines = [
...result.logLines,
...readinessWorkflow.logLines,
];
}
if (loadSession) {
const loadWorkflow = await loadMachineSessionIntoWasmWorkflow({ loadSession });
result.loadedSession = loadWorkflow.loadedSession;
result.loadSummary = loadWorkflow.loadSummary;
result.fields = {
...fields,
...(result.readiness ? createMachineSessionReadinessFieldState(result.readiness) : {}),
...loadWorkflow.fields,
sessionGcode: summary.gcodeOpfsPath,
};
result.logLines = [
...sessionSnapshotSummaryLogLines(summary),
...(result.readiness ? machineSessionReadinessLogLines(result.readiness) : []),
...loadWorkflow.logLines,
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
];

View File

@@ -18,6 +18,11 @@ import {
createControlPageSessionLoadState,
loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js";
import {
createMachineSessionReadinessFieldState,
machineSessionReadinessLogLines,
readMachineSessionReadinessWorkflow,
} from "./session-workflow.js";
import {
createMachineSessionReadonlyStatus,
createMachineSessionReadonlyStatusApiManifest,
@@ -32,6 +37,11 @@ export {
createControlPageSessionLoadState,
loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js";
export {
createMachineSessionReadinessFieldState,
machineSessionReadinessLogLines,
readMachineSessionReadinessWorkflow,
} from "./session-workflow.js";
export {
createMachineSessionReadonlyStatus,
createMachineSessionReadonlyStatusApiManifest,

View File

@@ -66,6 +66,9 @@ import {
createIniPanelShellViewModel,
createMachineSessionReadonlyStatus,
createMachineSessionReadonlyStatusApiManifest,
createMachineSessionReadinessFieldState,
machineSessionReadinessLogLines,
readMachineSessionReadinessWorkflow,
loadMachineSessionForControlPageWorkflow,
readMachineSessionReadonlyStatus,
refreshMachineSessionControlPage,
@@ -96,6 +99,10 @@ const sessionWorkflowText = readFileSync(
resolve(root, "runtime/ui/ini-panel/control-page-session-workflow.js"),
"utf8",
);
const iniPanelSessionWorkflowText = readFileSync(
resolve(root, "runtime/ui/ini-panel/session-workflow.js"),
"utf8",
);
const requiredShellExports = [
"INI_PANEL_ENTRIES",
@@ -168,6 +175,9 @@ const requiredShellExports = [
"createMachineSessionControlPageController",
"createMachineSessionReadonlyStatus",
"createMachineSessionReadonlyStatusApiManifest",
"createMachineSessionReadinessFieldState",
"machineSessionReadinessLogLines",
"readMachineSessionReadinessWorkflow",
"loadMachineSessionForControlPageWorkflow",
"readMachineSessionReadonlyStatus",
"refreshMachineSessionControlPage",
@@ -196,6 +206,15 @@ for (const exportName of [
assert.match(docText, new RegExp(`\\b${exportName}\\b`));
}
for (const exportName of [
"createMachineSessionReadinessFieldState",
"machineSessionReadinessLogLines",
"readMachineSessionReadinessWorkflow",
]) {
assert.match(iniPanelSessionWorkflowText, new RegExp(`\\bexport (?:async )?function ${exportName}\\b`));
assert.match(docText, new RegExp(`\\b${exportName}\\b`));
}
const requiredPages = [
["Launch", "runtime/ui/ini-panel/launch.html"],
["Edit and run", "runtime/ui/ini-panel/index.html"],

View File

@@ -1,9 +1,12 @@
import assert from "node:assert/strict";
import {
createMachineSessionReadinessFieldState,
createMachineSessionLoadFieldState,
loadMachineSessionIntoWasmWorkflow,
machineSessionLoadWorkflowLogLines,
machineSessionReadinessLogLines,
readMachineSessionReadinessWorkflow,
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow,
} from "../../../runtime/ui/ini-panel/session-workflow.js";
@@ -63,6 +66,101 @@ await assert.rejects(
/machine session loader/,
);
const readySessionReadiness = {
ready: true,
phase: "ready",
missing: [],
checks: [
{
name: "ini",
path: "linuxcnc/machines/xyzab-tdr/machine.ini",
ok: true,
error: null,
},
{
name: "parameters",
path: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
ok: true,
error: null,
},
{
name: "toolTable",
path: "linuxcnc/machines/xyzab-tdr/tool.tbl",
ok: true,
error: null,
},
],
snapshotCheck: {
name: "snapshot",
path: "linuxcnc/sessions/ui-machine-session/ui-machine-session.json",
ok: true,
error: null,
},
};
const readinessWorkflowCalls = [];
const readinessWorkflow = await readMachineSessionReadinessWorkflow({
readReadiness: async () => {
readinessWorkflowCalls.push(["readReadiness"]);
return readySessionReadiness;
},
});
assert.deepEqual(readinessWorkflowCalls, [["readReadiness"]]);
assert.equal(readinessWorkflow.readiness, readySessionReadiness);
assert.deepEqual(createMachineSessionReadinessFieldState(readySessionReadiness), {
sessionReadinessPhase: "ready",
sessionReadinessReady: "ready",
sessionReadinessMissing: "none",
});
assert.deepEqual(readinessWorkflow.fields, {
sessionReadinessPhase: "ready",
sessionReadinessReady: "ready",
sessionReadinessMissing: "none",
});
assert.deepEqual(machineSessionReadinessLogLines(readySessionReadiness), [
"Session readiness: ready",
"ini: linuxcnc/machines/xyzab-tdr/machine.ini -> ok",
"parameters: linuxcnc/machines/xyzab-tdr/linuxcnc.var -> ok",
"toolTable: linuxcnc/machines/xyzab-tdr/tool.tbl -> ok",
"snapshot: linuxcnc/sessions/ui-machine-session/ui-machine-session.json -> ok",
]);
assert.deepEqual(readinessWorkflow.logLines, machineSessionReadinessLogLines(readySessionReadiness));
await assert.rejects(
() => readMachineSessionReadinessWorkflow({}),
/machine session readiness reader/,
);
const blockedSessionReadiness = {
ready: false,
phase: "blocked",
missing: ["parameters", "toolTable"],
checks: [
{
name: "parameters",
path: "linuxcnc/machines/missing/linuxcnc.var",
ok: false,
error: "missing file",
},
{
name: "toolTable",
path: "linuxcnc/machines/missing/tool.tbl",
ok: false,
error: "missing file",
},
],
snapshotCheck: null,
};
assert.deepEqual(createMachineSessionReadinessFieldState(blockedSessionReadiness), {
sessionReadinessPhase: "blocked",
sessionReadinessReady: "blocked",
sessionReadinessMissing: "parameters, toolTable",
});
assert.deepEqual(machineSessionReadinessLogLines(blockedSessionReadiness), [
"Session readiness: blocked",
"parameters: linuxcnc/machines/missing/linuxcnc.var -> blocked (missing file)",
"toolTable: linuxcnc/machines/missing/tool.tbl -> blocked (missing file)",
]);
const calls = [];
const saveWorkflow = await saveMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
@@ -224,6 +322,75 @@ assert.deepEqual(restoreWorkflow.logLines, [
"G-code: linuxcnc/gcode/ui-session.ngc",
]);
const restoreReadinessCalls = [];
const restoreReadinessWorkflow = 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) => {
restoreReadinessCalls.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) => {
restoreReadinessCalls.push(["loadMachineTextFiles", machineId]);
return {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
};
},
readReadiness: async () => {
restoreReadinessCalls.push(["readReadiness"]);
return readySessionReadiness;
},
});
assert.deepEqual(restoreReadinessCalls, [
[
"loadMachineSessionSnapshot",
"ui-machine-session",
"xyzab-tdr",
{ filename: "ui-machine-session.json" },
],
["loadMachineTextFiles", "xyzab-tdr"],
["readReadiness"],
]);
assert.equal(restoreReadinessWorkflow.readiness, readySessionReadiness);
assert.deepEqual(restoreReadinessWorkflow.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",
sessionReadinessPhase: "ready",
sessionReadinessReady: "ready",
sessionReadinessMissing: "none",
});
assert.deepEqual(restoreReadinessWorkflow.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",
...machineSessionReadinessLogLines(readySessionReadiness),
]);
const restoreLoadCalls = [];
const restoreLoadWorkflow = await restoreMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",