推进 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

@@ -451,3 +451,75 @@ host_wasm_opfs_browser_smokes=ok
更完整的 machine session state snapshot helper供上层 UI/API 直接消费当前 machine、 更完整的 machine session state snapshot helper供上层 UI/API 直接消费当前 machine、
session、loaded WASM mapping、selected G-code、last run summary、panel badges 和 error session、loaded WASM mapping、selected G-code、last run summary、panel badges 和 error
state该 helper 只聚合已有 host/runtime boundary facts不解析 G-code 或扩展 CNC 语义。 state该 helper 只聚合已有 host/runtime boundary facts不解析 G-code 或扩展 CNC 语义。
三、2026-06-14 继续执行记录INI panel machine session state snapshot helper
本轮继续按 `text11.txt` 第一原则推进实质性 UI/API 能力,把 save/restore/load/run workflow
的结果汇总成一个 machine session state snapshot供上层 UI/API 直接读取,而不是继续
依赖分散的 DOM 字段、日志文本和临时全局值。
完成内容:
- 扩展 `wasm-port/runtime/ui/ini-panel/panel-state-summary.js`
- 新增导出 `createMachineSessionStateSnapshot(input)`
- state snapshot 汇总:
- panel badges
- machine
- machineFiles
- sessionSnapshot
- sessionLoad
- selectedProgram
- run summary
- fields
- lastAction
- error
- fiveaxisRemap
- `wasm-port/runtime/ui/ini-panel/app.js` 在 save/restore/load/run/failure/boot 路径后更新
`window.linuxCncIniPanelMachineSessionState`
- browser INI panel smoke 现在直接校验该 state snapshot 的关键字段;
- `wasm-port/tests/ui/node/verify_ini_panel_state_summary.mjs` 覆盖新 state snapshot 形状;
- `wasm-port/docs/ui-panel-state-summary.md` 更新 state snapshot API、window 暴露和边界说明;
- 未改变 OPFS bridge、snapshot-store、machine-file-store、LinuxCNC interpreter、G-code 文本、
canonical-event 语义、tool、parameter、planner 或 LinuxCNC-owned runtime semantics。
验证已通过:
```bash
git diff --check
wasm-port/tests/ui/node/verify_ini_panel_state_summary.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tools/verify_vendor_sync.sh
wasm-port/tools/verify_no_standalone_cnc_semantics.sh
SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_interp_wasm.sh
SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh
wasm-port/tests/host/verify_host_smokes.sh
```
关键输出:
```text
ini_panel_state_summary_node_smoke=ok
ini_panel_run_summary_node_smoke=ok
ini_panel_run_workflow_node_smoke=ok
ini_panel_session_summary_node_smoke=ok
ini_panel_session_workflow_node_smoke=ok
ini_panel_machine_file_summary_node_smoke=ok
ui_node_smokes=ok
browser_ini_opfs_smoke=ok
vendor sync up to date
standalone CNC semantics guard complete
interp_wasm_node_smoke=ok
sim_configs_wasm_node_inventory_executed=28
sim_configs_wasm_node_inventory_passed=28
sim_configs_wasm_node_inventory_skipped=131
sim_configs_wasm_node_inventory_unexpected_fail=0
host_wasm_opfs_browser_smokes=ok
```
下一步建议:
继续按“实质性能力优先”推进。下一批优先把 `window.linuxCncIniPanelMachineSessionState`
整理成更明确的 API/文档使用方式,或者在 `run-gcode`/restore/load/save 之外再补一层
更高阶的 UI consumer helper用于读取状态并生成上层展示/导出结果;只围绕已有
host/runtime boundary facts 组合,不回到 path guard。

View File

@@ -30,11 +30,45 @@ reading ad hoc DOM text.
The returned snapshot preserves those nested objects and normalizes missing The returned snapshot preserves those nested objects and normalizes missing
badge entries to `null`. 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 ## Boundary Rules
- Do not parse G-code text in this helper. - Do not parse G-code text in this helper.
- Do not infer CNC semantics from the nested summaries. - Do not infer CNC semantics from the nested summaries.
- Keep the helper as a bundling layer over already-observed UI boundary state. - 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 ## Validation

View File

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

View File

@@ -21,3 +21,45 @@ export function createIniPanelStateSummary({
fiveaxisRemap: fiveaxisRemap ?? null, 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, 2,
"UI snapshot run summary motion snapshot count", "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 ( if (
!runLog.includes("canon_event=STRAIGHT_TRAVERSE") || !runLog.includes("canon_event=STRAIGHT_TRAVERSE") ||
!runLog.includes("canon_event=STRAIGHT_FEED") !runLog.includes("canon_event=STRAIGHT_FEED")

View File

@@ -1,6 +1,9 @@
import assert from "node:assert/strict"; 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({ const summary = createIniPanelStateSummary({
badges: { 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"); console.log("ini_panel_state_summary_node_smoke=ok");