From 75818c1379a04d821e0c4ad73229333fd65a5937 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Sun, 14 Jun 2026 21:38:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A8=E8=BF=9B=20INI=20=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=20run=20G-code=20workflow=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- text11.txt | 70 ++++++++ wasm-port/docs/ui-run-summary-helpers.md | 53 ++++++- wasm-port/runtime/ui/ini-panel/app.js | 90 ++++------- .../runtime/ui/ini-panel/run-workflow.js | 109 +++++++++++++ .../ui/node/verify_ini_panel_run_workflow.mjs | 149 ++++++++++++++++++ .../ui/node/verify_ini_panel_run_workflow.sh | 6 + .../tests/ui/node/verify_ui_node_smokes.sh | 1 + 7 files changed, 412 insertions(+), 66 deletions(-) create mode 100644 wasm-port/runtime/ui/ini-panel/run-workflow.js create mode 100644 wasm-port/tests/ui/node/verify_ini_panel_run_workflow.mjs create mode 100755 wasm-port/tests/ui/node/verify_ini_panel_run_workflow.sh diff --git a/text11.txt b/text11.txt index 564ba1f..81dcd16 100644 --- a/text11.txt +++ b/text11.txt @@ -381,3 +381,73 @@ helper,覆盖 snapshot/default program selection、G-code OPFS load、WASM wri interpreter run、run summary、field/log state 和 error state。该 helper 仍只能编排 host/runtime boundary,不解析或实现 G-code、planner、tool、parameter 或 canonical-event 语义。 + +二、2026-06-14 继续执行记录:INI panel run G-code workflow helper + +本轮继续按 `text11.txt` 第一原则推进实质性 UI workflow 能力,把 `run-gcode` 用户流程抽成 +可复用 helper,让上层 UI/API 可以直接复用 session/default G-code 选择、OPFS load、WASM +staging、LinuxCNC interpreter run、run summary、field/log state 的编排。 + +完成内容: + +- 新增 `wasm-port/runtime/ui/ini-panel/run-workflow.js`; +- 导出: + - `selectSessionGcodeProgram(input)`; + - `runSessionContextLogLines(program, snapshot, snapshotLabel)`; + - `runGcodeSessionWorkflow(input)`; +- workflow helper 执行 host/runtime boundary 编排: + - 从 snapshot G-code OPFS path 或默认 G-code path 选择程序; + - 通过注入的 `loadGcodeProgram(...)` 读取 OPFS G-code 文本; + - 调用 `interp.writeTextFile(...)` 写入 interpreter WASM filesystem; + - 调用 LinuxCNC-owned `interp.runFileWithIni(...)`; + - 返回 program、resultText、run summary、UI field state、motionSnapshots 和 log lines; +- `wasm-port/runtime/ui/ini-panel/app.js` 的 `Run G-code` 按钮改为调用 workflow helper; +- canonical-event 展示和 motion playback 仍留在 UI 层,workflow 只通过注入的 + `summarizeRun(...)` 接收已观察到的 UI summary/motion state; +- 新增 `wasm-port/tests/ui/node/verify_ini_panel_run_workflow.mjs` 和 shell wrapper; +- `wasm-port/tests/ui/node/verify_ui_node_smokes.sh` 纳入 run workflow smoke; +- `wasm-port/docs/ui-run-summary-helpers.md` 更新 run workflow API、返回结构和边界说明; +- 未改变 OPFS bridge、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_run_workflow.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_run_workflow_node_smoke=ok +ini_panel_run_summary_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 +ini_panel_state_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 +``` + +下一步建议: + +继续按“实质性能力优先”推进。优先把 save/restore/load/run workflow 的结果汇总成一个 +更完整的 machine session state snapshot helper,供上层 UI/API 直接消费当前 machine、 +session、loaded WASM mapping、selected G-code、last run summary、panel badges 和 error +state;该 helper 只聚合已有 host/runtime boundary facts,不解析 G-code 或扩展 CNC 语义。 diff --git a/wasm-port/docs/ui-run-summary-helpers.md b/wasm-port/docs/ui-run-summary-helpers.md index 4369dc4..bbbbcd2 100644 --- a/wasm-port/docs/ui-run-summary-helpers.md +++ b/wasm-port/docs/ui-run-summary-helpers.md @@ -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: diff --git a/wasm-port/runtime/ui/ini-panel/app.js b/wasm-port/runtime/ui/ini-panel/app.js index d7d0e99..393bfe5 100644 --- a/wasm-port/runtime/ui/ini-panel/app.js +++ b/wasm-port/runtime/ui/ini-panel/app.js @@ -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) { diff --git a/wasm-port/runtime/ui/ini-panel/run-workflow.js b/wasm-port/runtime/ui/ini-panel/run-workflow.js new file mode 100644 index 0000000..906e625 --- /dev/null +++ b/wasm-port/runtime/ui/ini-panel/run-workflow.js @@ -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, + ], + }; +} diff --git a/wasm-port/tests/ui/node/verify_ini_panel_run_workflow.mjs b/wasm-port/tests/ui/node/verify_ini_panel_run_workflow.mjs new file mode 100644 index 0000000..cf6ecd0 --- /dev/null +++ b/wasm-port/tests/ui/node/verify_ini_panel_run_workflow.mjs @@ -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"); diff --git a/wasm-port/tests/ui/node/verify_ini_panel_run_workflow.sh b/wasm-port/tests/ui/node/verify_ini_panel_run_workflow.sh new file mode 100755 index 0000000..4eba989 --- /dev/null +++ b/wasm-port/tests/ui/node/verify_ini_panel_run_workflow.sh @@ -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" diff --git a/wasm-port/tests/ui/node/verify_ui_node_smokes.sh b/wasm-port/tests/ui/node/verify_ui_node_smokes.sh index 34c95ba..f2aef26 100755 --- a/wasm-port/tests/ui/node/verify_ui_node_smokes.sh +++ b/wasm-port/tests/ui/node/verify_ui_node_smokes.sh @@ -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"