接入虚拟HAL会话保存恢复

结论: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方式数控系统仿真范围。
This commit is contained in:
2026-06-17 22:32:23 +08:00
parent 402fd41d4d
commit 0a33f0f7c1
7 changed files with 263 additions and 1 deletions

View File

@@ -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,
});
}

View File

@@ -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,

View File

@@ -1474,6 +1474,8 @@
<div class="machine-session-actions">
<button type="button" data-check-machine-session>Check Session</button>
<button type="button" data-load-machine-session>Load Session</button>
<button type="button" data-save-virtual-hal-session>Save HAL Session</button>
<button type="button" data-restore-virtual-hal-session>Restore HAL Session</button>
<label><input type="checkbox" checked data-use-loaded-session> Use Session</label>
</div>
<div class="machine-session-status" data-machine-session-status>
@@ -1484,6 +1486,7 @@
<span>Loaded INI: <span data-machine-session-loaded-path="ini">-</span></span>
<span>Loaded parameters: <span data-machine-session-loaded-path="parameters">-</span></span>
<span>Loaded tool table: <span data-machine-session-loaded-path="toolTable">-</span></span>
<span>Virtual HAL session: <span data-virtual-hal-session-status>not saved</span></span>
</div>
</div>
<div class="active-codes" data-axis-shell="active-codes">
@@ -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);
});

View File

@@ -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" ||

View File

@@ -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",

View File

@@ -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", {