From 0a33f0f7c15da5d985e1ded42f048ae60393521f Mon Sep 17 00:00:00 2001 From: wangdequan Date: Wed, 17 Jun 2026 22:32:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E8=99=9A=E6=8B=9FHAL?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E4=BF=9D=E5=AD=98=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结论:real simulation page 已提供显式 Save/Restore HAL Session 按钮与 browser API,使用 OPFS session snapshot 保存和恢复 virtual HAL runtime state,并随 payload 校验 source compliance、sim-config coverage、halcmd fixtures 与 motion controller matrix,证明虚拟HAL可恢复地满足Web方式数控系统仿真范围。 --- text17.txt | 84 +++++++++++++ wasm-port/runtime/opfs/snapshot-store.js | 7 ++ wasm-port/runtime/sdk/README.md | 4 + wasm-port/runtime/ui/simulation/index.html | 119 ++++++++++++++++++ .../browser/real_simulation_page_smoke.html | 47 ++++++- .../tests/opfs/node/verify_file_service.mjs | 2 + .../tests/sdk/node/verify_sdk_surface.mjs | 1 + 7 files changed, 263 insertions(+), 1 deletion(-) diff --git a/text17.txt b/text17.txt index 5044162..44ea652 100644 --- a/text17.txt +++ b/text17.txt @@ -866,3 +866,87 @@ git diff --check ok 让 release readiness 页面能显示 browser diagnostics validation rows。 3. 扩展 motion matrix 与 LinuxCNC sim configs 的关联,把 external-offsets、QtDragon multi-joint、vismach remap sims 的 motion source evidence 显式映射到 matrix fixture rows。 + +### 九、2026-06-17 Real Simulation Virtual HAL Session Save/Restore + +继续按本文件铁律推进:虚拟 HAL 的功能完全来源于 LinuxCNC 源程序,并且虚拟 HAL +完全满足 Web 方式数控系统仿真系统。本批完成上一节下一步第 1 项,把 virtual HAL +session payload 接入 real simulation page 的显式保存/恢复按钮与 browser API,让用户可在 +Web CNC 仿真页面直接保存和恢复完整 virtual HAL runtime state。 + +新增实质能力: + +- `runtime/opfs/snapshot-store.js` 扩展 virtual HAL session payload: + - 新增 `motionControllerMatrix`; + - payload validation 要求 motion controller matrix complete; + - `createVirtualHalSessionPayload()` 默认生成 motion matrix evidence; +- real simulation page 新增 Machine Session 面板控件: + - `Save HAL Session`; + - `Restore HAL Session`; + - `Virtual HAL session` 状态行; +- Browser simulation API 新增: + - `getVirtualHalSessionState()`; + - `saveVirtualHalSessionSnapshot(options)`; + - `restoreVirtualHalSessionSnapshot(options)`; +- 保存流程: + - 使用当前 `virtualHalState` 创建 `createVirtualHalSessionPayload()`; + - 使用 `createMachineSessionSnapshotPayload()` 封装 machine/session payload; + - 使用 `saveSessionSnapshot()` 写入 OPFS; + - snapshot metadata 记录 `real-browser-simulation-virtual-hal-session` 与 HAL revision; +- 恢复流程: + - 使用 `loadSessionSnapshot()` 读取 OPFS snapshot; + - 使用 `restoreVirtualHalStateFromSessionSnapshot()` 恢复 virtual HAL state; + - 重新渲染 HAL machine state、statusbar 和 session 状态; +- SDK README OPFS/session persistence section 同步说明: + - `createVirtualHalSessionPayload()`; + - `restoreVirtualHalStateFromSessionSnapshot()`; + - payload 包含 source compliance、sim-config coverage、halcmd fixtures、motion + controller matrix evidence。 + +新增/扩展验证: + +- `verify_file_service.mjs` 覆盖: + - virtual HAL session payload 内含 motion controller matrix complete; + - machine session snapshot payload 内含 motion matrix LinuxCNC source evidence; +- `verify_sdk_surface.mjs` 覆盖: + - virtual HAL session payload 内含 motion controller matrix complete; +- `real_simulation_page_smoke.html` 覆盖: + - 页面存在显式保存/恢复按钮; + - browser API 暴露保存/恢复方法; + - 保存当前 HAL net、loaded component、function、source reports、motion matrix; + - 保存后修改 HAL pin 与 loadusr state; + - 恢复后 `axis.x.pos-cmd` 回到保存值; + - 恢复后临时 `post-save-component` 不再存在; + - 恢复后 `motion-controller` function 仍存在; + - DOM/body dataset 显示 saved/restored 状态。 + +关键验证: + +```text +opfs_file_service_node_smoke=ok +sdk_surface_node_smoke=ok +project_release_gate_manifest_node_smoke=ok +project_release_artifact_url_workflow_node_smoke=ok +project_batch_acceptance_workflow_node_smoke=ok +project_batch_acceptance_artifact_node_smoke=ok +browser_real_simulation_page_smoke=ok +git diff --check ok +``` + +语义边界: + +- 本批没有新增 G-code/interpreter/planner/kinematics 语义; +- session snapshot 只保存和恢复 Web virtual HAL runtime state、source reports 和 + diagnostics evidence; +- virtual HAL 仍不声称提供 Linux kernel hard-realtime ABI、外部硬件驱动 ABI 或 native + HAL module ABI; +- Web 仿真 HAL 的满足性现在可在 real simulation page 中由用户显式保存、恢复并继续验证。 + +下一步工作内容: + +1. 将 browser diagnostics artifact 的 URL workflow 接入 ini-panel workflow overview 页面, + 让 release readiness 页面能显示 browser diagnostics validation rows。 +2. 扩展 motion matrix 与 LinuxCNC sim configs 的关联,把 external-offsets、QtDragon + multi-joint、vismach remap sims 的 motion source evidence 显式映射到 matrix fixture rows。 +3. 将 real simulation page 的 virtual HAL session snapshot 与 release diagnostics artifact + 做联动,使保存的 Web CNC 仿真会话可直接输出 release-ready diagnostics evidence。 diff --git a/wasm-port/runtime/opfs/snapshot-store.js b/wasm-port/runtime/opfs/snapshot-store.js index 6820932..4a3c88b 100644 --- a/wasm-port/runtime/opfs/snapshot-store.js +++ b/wasm-port/runtime/opfs/snapshot-store.js @@ -10,6 +10,7 @@ import { import { cloneVirtualHalState, createVirtualHalCommandScriptFixtureReport, + createVirtualHalMotionControllerMatrixReport, createVirtualHalSimConfigSourceCoverageReport, createVirtualHalSimulationReplacementReport, createVirtualHalSourceComplianceReport, @@ -66,6 +67,7 @@ function validateVirtualHalSessionPayload(virtualHal) { assertPlainObject(virtualHal.sourceCompliance, "virtual HAL session source compliance"); assertPlainObject(virtualHal.simConfigSourceCoverage, "virtual HAL session sim-config source coverage"); assertPlainObject(virtualHal.commandScriptFixtures, "virtual HAL session command script fixtures"); + assertPlainObject(virtualHal.motionControllerMatrix, "virtual HAL session motion controller matrix"); if (virtualHal.state.source !== "browser-virtual-hal") { throw new Error(`Unsupported virtual HAL state source: ${virtualHal.state.source}`); } @@ -78,6 +80,9 @@ function validateVirtualHalSessionPayload(virtualHal) { if (virtualHal.commandScriptFixtures.complete !== true) { throw new Error("virtual HAL command script fixtures are incomplete."); } + if (virtualHal.motionControllerMatrix.complete !== true) { + throw new Error("virtual HAL motion controller matrix is incomplete."); + } return virtualHal; } @@ -87,6 +92,7 @@ export function createVirtualHalSessionPayload(halState = createVirtualHalState( const sourceCompliance = options.sourceCompliance ?? createVirtualHalSourceComplianceReport({ halState: state }); const simConfigSourceCoverage = options.simConfigSourceCoverage ?? createVirtualHalSimConfigSourceCoverageReport(options); const commandScriptFixtures = options.commandScriptFixtures ?? createVirtualHalCommandScriptFixtureReport({ ...options, halState: state }); + const motionControllerMatrix = options.motionControllerMatrix ?? createVirtualHalMotionControllerMatrixReport(options); return validateVirtualHalSessionPayload({ apiName: "linuxcnc-wasm-virtual-hal-session-payload", payloadVersion: 1, @@ -96,6 +102,7 @@ export function createVirtualHalSessionPayload(halState = createVirtualHalState( sourceCompliance, simConfigSourceCoverage, commandScriptFixtures, + motionControllerMatrix, }); } diff --git a/wasm-port/runtime/sdk/README.md b/wasm-port/runtime/sdk/README.md index bf70d45..cc4fc5d 100644 --- a/wasm-port/runtime/sdk/README.md +++ b/wasm-port/runtime/sdk/README.md @@ -486,6 +486,10 @@ private module paths: `saveSessionSnapshot()`, `saveMachineSessionSnapshot()`, `loadMachineSessionSnapshot()`, and `validateSessionSnapshot()` for session snapshot persistence. +- `createVirtualHalSessionPayload()` and + `restoreVirtualHalStateFromSessionSnapshot()` for saving and restoring Web + virtual HAL state with source compliance, sim-config coverage, halcmd + fixtures, and motion controller matrix evidence. - `machineFilePaths()`, `saveMachineTextFiles()`, `loadMachineTextFiles()`, and `gcodeFilenameFromProgramPath()` for machine-file and G-code file helpers. - `readMachineSessionReadiness()` for checking persisted INI, parameter, diff --git a/wasm-port/runtime/ui/simulation/index.html b/wasm-port/runtime/ui/simulation/index.html index 4977f6e..28669f8 100644 --- a/wasm-port/runtime/ui/simulation/index.html +++ b/wasm-port/runtime/ui/simulation/index.html @@ -1474,6 +1474,8 @@
+ +
@@ -1484,6 +1486,7 @@ Loaded INI: - Loaded parameters: - Loaded tool table: - + Virtual HAL session: not saved
@@ -1781,6 +1784,13 @@ M2 import { loadMachineSessionFromOpfs, } from "../../opfs/linuxcnc-machine-session-bridge.js"; + import { + createMachineSessionSnapshotPayload, + createVirtualHalSessionPayload, + loadSessionSnapshot, + restoreVirtualHalStateFromSessionSnapshot, + saveSessionSnapshot, + } from "../../opfs/snapshot-store.js"; import { readMachineSessionReadiness, } from "../../opfs/machine-session-readiness.js"; @@ -1842,6 +1852,8 @@ M2 const machineSessionGcodeInput = document.querySelector("[data-machine-session-gcode]"); const checkMachineSessionButton = document.querySelector("[data-check-machine-session]"); const loadMachineSessionButton = document.querySelector("[data-load-machine-session]"); + const saveVirtualHalSessionButton = document.querySelector("[data-save-virtual-hal-session]"); + const restoreVirtualHalSessionButton = document.querySelector("[data-restore-virtual-hal-session]"); const useLoadedSessionInput = document.querySelector("[data-use-loaded-session]"); const resetButton = document.querySelector("[data-playback-reset]"); const prevButton = document.querySelector("[data-playback-prev]"); @@ -1900,6 +1912,16 @@ M2 opfsPath: "linuxcnc/gcode/axis-editor.ngc", status: "OPFS program not saved", }; + let virtualHalSessionState = { + ready: false, + phase: "not-saved", + sessionId: "axis-browser-session", + filename: "virtual-hal-session.json", + machineId: "axis-browser-sim", + status: "Virtual HAL session not saved", + revision: 0, + restored: false, + }; let machineReadinessState = { machineId: "axis-browser-sim", ready: false, @@ -2644,6 +2666,17 @@ M2 return { ...opfsProgramState }; } + function setVirtualHalSessionState(nextState) { + virtualHalSessionState = { + ...virtualHalSessionState, + ...nextState, + }; + document.body.dataset.virtualHalSessionPhase = virtualHalSessionState.phase; + document.body.dataset.virtualHalSessionReady = virtualHalSessionState.ready ? "true" : "false"; + setAllText("[data-virtual-hal-session-status]", virtualHalSessionState.status); + return { ...virtualHalSessionState }; + } + function renderMachineReadiness(readiness) { machineReadinessState = readiness; document.body.dataset.machineSessionReady = readiness.ready ? "true" : "false"; @@ -3489,6 +3522,69 @@ M2 return state; } + function virtualHalSessionOptions(options = {}) { + const sessionId = String(options.sessionId ?? machineSessionSnapshotInput?.value ?? "axis-browser-session").trim() || "axis-browser-session"; + const machineId = String(options.machineId ?? machineSessionIdInput?.value ?? "axis-browser-sim").trim() || "axis-browser-sim"; + const filename = String(options.filename ?? options.snapshotFilename ?? "virtual-hal-session.json").trim() || "virtual-hal-session.json"; + const gcodeFilename = String(options.gcodeFilename ?? machineSessionGcodeInput?.value ?? "").trim(); + return { + sessionId, + machineId, + filename, + ...(gcodeFilename ? { gcodeFilename } : {}), + }; + } + + async function saveVirtualHalSessionSnapshot(options = {}) { + const session = virtualHalSessionOptions(options); + const virtualHal = createVirtualHalSessionPayload(virtualHalState); + const payload = createMachineSessionSnapshotPayload(session.machineId, { + ...(session.gcodeFilename ? { gcodeFilename: session.gcodeFilename } : {}), + virtualHal, + }); + const snapshot = await saveSessionSnapshot(session.sessionId, payload, { + filename: session.filename, + metadata: { + source: "real-browser-simulation-virtual-hal-session", + virtualHalRevision: virtualHal.state.revision, + }, + }); + pushStatusHistory("hal-session", `saved ${session.sessionId}/${session.filename}`); + return setVirtualHalSessionState({ + ready: true, + phase: "saved", + sessionId: session.sessionId, + machineId: session.machineId, + filename: session.filename, + status: `Saved HAL session: linuxcnc/sessions/${session.sessionId}/${session.filename}`, + revision: virtualHal.state.revision, + restored: false, + snapshot, + }); + } + + async function restoreVirtualHalSessionSnapshot(options = {}) { + const session = virtualHalSessionOptions(options); + const snapshot = await loadSessionSnapshot(session.sessionId, { + filename: session.filename, + }); + virtualHalState = restoreVirtualHalStateFromSessionSnapshot(snapshot); + renderVirtualHalState(); + renderAxisStatusbar(); + pushStatusHistory("hal-session", `restored ${session.sessionId}/${session.filename}`); + return setVirtualHalSessionState({ + ready: true, + phase: "restored", + sessionId: session.sessionId, + machineId: snapshot.payload?.machineId ?? session.machineId, + filename: session.filename, + status: `Restored HAL session: linuxcnc/sessions/${session.sessionId}/${session.filename}`, + revision: virtualHalState.revision, + restored: true, + snapshot, + }); + } + async function checkMachineSessionReadiness(options = {}) { const machineId = String(options.machineId ?? machineSessionIdInput?.value ?? "axis-browser-sim").trim() || "axis-browser-sim"; const sessionId = String(options.sessionId ?? machineSessionSnapshotInput?.value ?? "").trim(); @@ -3876,6 +3972,9 @@ M2 checkMachineSessionReadiness, getMachineSessionLoadState: () => machineSessionLoadState, loadReadyMachineSession, + getVirtualHalSessionState: () => ({ ...virtualHalSessionState }), + saveVirtualHalSessionSnapshot, + restoreVirtualHalSessionSnapshot, getRunMode: () => syncRunMode(), getRunControlState: () => renderRunControlState(), getAxisStatusbarState: () => renderAxisStatusbar(), @@ -4018,6 +4117,26 @@ M2 }); }); }); + saveVirtualHalSessionButton?.addEventListener("click", () => { + window.linuxCncRealSimulationApi.saveVirtualHalSessionSnapshot().catch((error) => { + setVirtualHalSessionState({ + ready: false, + phase: "failed", + status: `HAL session save failed: ${error.message}`, + }); + recordError(error, "hal session save"); + }); + }); + restoreVirtualHalSessionButton?.addEventListener("click", () => { + window.linuxCncRealSimulationApi.restoreVirtualHalSessionSnapshot().catch((error) => { + setVirtualHalSessionState({ + ready: false, + phase: "failed", + status: `HAL session restore failed: ${error.message}`, + }); + recordError(error, "hal session restore"); + }); + }); useLoadedSessionInput?.addEventListener("change", () => { setUseLoadedSession(useLoadedSessionInput.checked); }); diff --git a/wasm-port/tests/browser/real_simulation_page_smoke.html b/wasm-port/tests/browser/real_simulation_page_smoke.html index 2dbfabb..c08946f 100644 --- a/wasm-port/tests/browser/real_simulation_page_smoke.html +++ b/wasm-port/tests/browser/real_simulation_page_smoke.html @@ -252,6 +252,9 @@ if (!api?.getMachineSessionLoadState || !api?.loadReadyMachineSession) { throw new Error("simulation API missing AXIS machine/session load controls"); } + if (!api?.getVirtualHalSessionState || !api?.saveVirtualHalSessionSnapshot || !api?.restoreVirtualHalSessionSnapshot) { + throw new Error("simulation API missing virtual HAL session persistence controls"); + } if (!api?.getRunMode || !api?.getRunControlState || !api?.setUseLoadedSession || !api?.getRunSummary) { throw new Error("simulation API missing AXIS run-mode controls"); } @@ -317,7 +320,14 @@ if (!doc.querySelector("[data-opfs-program-filename]") || !doc.querySelector("[data-save-program-opfs]") || !doc.querySelector("[data-load-program-opfs]")) { throw new Error("simulation page missing OPFS program persistence controls"); } - if (!doc.querySelector('[data-axis-shell="machine-session"]') || !doc.querySelector("[data-check-machine-session]") || !doc.querySelector("[data-load-machine-session]") || !doc.querySelector("[data-use-loaded-session]")) { + if ( + !doc.querySelector('[data-axis-shell="machine-session"]') || + !doc.querySelector("[data-check-machine-session]") || + !doc.querySelector("[data-load-machine-session]") || + !doc.querySelector("[data-save-virtual-hal-session]") || + !doc.querySelector("[data-restore-virtual-hal-session]") || + !doc.querySelector("[data-use-loaded-session]") + ) { throw new Error("simulation page missing machine/session readiness controls"); } if (!doc.querySelector("[data-axis-machine-control-state]")?.textContent.includes("browser virtual HAL active")) { @@ -726,6 +736,41 @@ ) { throw new Error(`AXIS-style virtual halcmd did not execute: ${JSON.stringify(virtualHalcmdResult)}`); } + const savedVirtualHalSession = await api.saveVirtualHalSessionSnapshot({ + sessionId: "axis-browser-virtual-hal-session", + machineId: "axis-browser-virtual-hal", + filename: "virtual-hal-session.json", + gcodeFilename: "axis-opfs-roundtrip.ngc", + }); + if ( + savedVirtualHalSession.phase !== "saved" || + savedVirtualHalSession.revision < virtualHalcmdResult.state.revision || + !savedVirtualHalSession.snapshot?.payload?.virtualHal?.sourceCompliance?.complete || + !savedVirtualHalSession.snapshot?.payload?.virtualHal?.motionControllerMatrix?.complete || + doc.body.dataset.virtualHalSessionPhase !== "saved" || + !doc.querySelector("[data-virtual-hal-session-status]")?.textContent.includes("Saved HAL session") + ) { + throw new Error(`AXIS-style virtual HAL session save failed: ${JSON.stringify(savedVirtualHalSession)}`); + } + api.executeVirtualHalcmd("setp axis.x.pos-cmd 9.5\nloadusr post-save-component"); + if (api.readVirtualHalPin("axis.x.pos-cmd")?.value !== 9.5) { + throw new Error("AXIS-style virtual HAL pre-restore mutation did not apply"); + } + const restoredVirtualHalSession = await api.restoreVirtualHalSessionSnapshot({ + sessionId: "axis-browser-virtual-hal-session", + filename: "virtual-hal-session.json", + }); + if ( + restoredVirtualHalSession.phase !== "restored" || + restoredVirtualHalSession.restored !== true || + api.readVirtualHalPin("axis.x.pos-cmd")?.value !== 1.75 || + api.getVirtualHalState().hal.loadedComponents.some(({ component }) => component === "post-save-component") || + !api.getVirtualHalState().hal.functions.some(({ name }) => name === "motion-controller") || + doc.body.dataset.virtualHalSessionPhase !== "restored" || + !doc.querySelector("[data-virtual-hal-session-status]")?.textContent.includes("Restored HAL session") + ) { + throw new Error(`AXIS-style virtual HAL session restore failed: ${JSON.stringify(restoredVirtualHalSession)}`); + } const virtualHalCommandScriptFixtures = api.getVirtualHalCommandScriptFixtureReport(); if ( virtualHalCommandScriptFixtures.apiName !== "linuxcnc-wasm-virtual-hal-command-script-fixture-report" || diff --git a/wasm-port/tests/opfs/node/verify_file_service.mjs b/wasm-port/tests/opfs/node/verify_file_service.mjs index f1e8251..8bfeb2d 100644 --- a/wasm-port/tests/opfs/node/verify_file_service.mjs +++ b/wasm-port/tests/opfs/node/verify_file_service.mjs @@ -266,12 +266,14 @@ assert.equal(virtualHalSessionPayload.state.hal.nets["x-pos"].includes("axis.x.p assert.equal(virtualHalSessionPayload.sourceCompliance.complete, true); assert.equal(virtualHalSessionPayload.simConfigSourceCoverage.complete, true); assert.equal(virtualHalSessionPayload.commandScriptFixtures.complete, true); +assert.equal(virtualHalSessionPayload.motionControllerMatrix.complete, true); const virtualHalSnapshotPayload = createMachineSessionSnapshotPayload("virtual-hal-machine", { gcodeFilename: "virtual-hal.ngc", virtualHal: virtualHalSessionPayload, }); assert.equal(virtualHalSnapshotPayload.virtualHal.state.hal.signals["x-pos"].value, 2.25); assert.equal(virtualHalSnapshotPayload.virtualHal.commandScriptFixtures.coveredActions.includes("loadusr"), true); +assert.equal(virtualHalSnapshotPayload.virtualHal.motionControllerMatrix.sourceFiles.includes("linuxcnc/src/emc/motion/axis.c"), true); assert.deepEqual( createMachineSessionSnapshotPayload("custom-machine", { parameterFilename: "custom.var", diff --git a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs index 4f8b1b1..5bb4007 100644 --- a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs +++ b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs @@ -747,6 +747,7 @@ assert.equal(virtualHalSessionPayload.state.hal.signals["x-pos"].value, 1.25); assert.equal(virtualHalSessionPayload.sourceCompliance.complete, true); assert.equal(virtualHalSessionPayload.simConfigSourceCoverage.complete, true); assert.equal(virtualHalSessionPayload.commandScriptFixtures.complete, true); +assert.equal(virtualHalSessionPayload.motionControllerMatrix.complete, true); const virtualHalSessionSnapshot = createSessionSnapshot( "sdk-virtual-hal-session", createMachineSessionSnapshotPayload("sdk-mill", {