按规划继续工作

结论:OPFS 参数文件已通过桥接进入 LinuxCNC-backed WASM restore/save 路径,native 与 host/WASM/browser 验证全部通过。
This commit is contained in:
2026-06-08 07:42:50 +08:00
parent 16585690ca
commit 5ea07606d7
6 changed files with 211 additions and 12 deletions

View File

@@ -99,7 +99,10 @@ paths, unavailable OPFS storage, and the host-side OPFS path model for INI,
tool table, parameter, G-code, preview-cache, and session-snapshot storage tool table, parameter, G-code, preview-cache, and session-snapshot storage
targets. It also validates the host-side session snapshot JSON envelope and targets. It also validates the host-side session snapshot JSON envelope and
round-trip store plus pure-text machine file and G-code stores without round-trip store plus pure-text machine file and G-code stores without
defining CNC machine-state or file-format semantics. defining CNC machine-state or file-format semantics. It now also validates the
OPFS-to-WASM parameter-file bridge with a mock interpreter SDK to ensure the
host boundary copies text into and out of the WASM filesystem without defining
parameter semantics.
The browser INI/OPFS smoke script serves `wasm-port/` over localhost and runs The browser INI/OPFS smoke script serves `wasm-port/` over localhost and runs
Chromium headless against a test page that imports the JS SDK, loads the INI Chromium headless against a test page that imports the JS SDK, loads the INI
@@ -114,7 +117,9 @@ behavior in JavaScript, and verifies existing canonical fixtures through the
exported C ABI backed by vendored LinuxCNC `Interp::execute()` and exported C ABI backed by vendored LinuxCNC `Interp::execute()` and
`Interp::open()`/`read()`/`execute()` paths, including the INI-aware `Interp::open()`/`read()`/`execute()` paths, including the INI-aware
named-parameter file path and negative interpreter fixtures with expected named-parameter file path and negative interpreter fixtures with expected
error text plus absent canonical motion output. error text plus absent canonical motion output. It also uses real browser OPFS
storage plus the interpreter SDK to restore and save a LinuxCNC parameter file
through vendored `Interp::restore_parameters()` and `Interp::save_parameters()`.
The aggregate host smoke script builds the INI and interpreter-core WASM The aggregate host smoke script builds the INI and interpreter-core WASM
artifacts once, then runs the Node WASM smokes, the Node OPFS mock smoke, and artifacts once, then runs the Node WASM smokes, the Node OPFS mock smoke, and
@@ -168,9 +173,9 @@ The validation fails if:
| --- | --- | | --- | --- |
| `tests/wasm/node/verify_ini_wasm.sh` | Validates the browser-facing INI WASM module can be built from vendored LinuxCNC `inifile.cc`, loaded through the JS SDK in Node, and queried through the exported C ABI. | | `tests/wasm/node/verify_ini_wasm.sh` | Validates the browser-facing INI WASM module can be built from vendored LinuxCNC `inifile.cc`, loaded through the JS SDK in Node, and queried through the exported C ABI. |
| `tests/wasm/node/verify_interp_wasm.sh` | Validates the initial interpreter-core WASM module can be built from vendored LinuxCNC interpreter source, loaded through the interpreter JS SDK, run the first fixture group through `Interp::execute()` and selected file fixtures through `Interp::open()`/`read()`/`execute()`, match the native canonical event plus required state readback fixtures, and run parameter-file restore/save through vendored LinuxCNC `Interp::restore_parameters()` and `Interp::save_parameters()`. | | `tests/wasm/node/verify_interp_wasm.sh` | Validates the initial interpreter-core WASM module can be built from vendored LinuxCNC interpreter source, loaded through the interpreter JS SDK, run the first fixture group through `Interp::execute()` and selected file fixtures through `Interp::open()`/`read()`/`execute()`, match the native canonical event plus required state readback fixtures, and run parameter-file restore/save through vendored LinuxCNC `Interp::restore_parameters()` and `Interp::save_parameters()`. |
| `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter, path model, session snapshot store, machine file store, and G-code text store used by the browser INI panel without moving file persistence into the WASM core. | | `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter, path model, session snapshot store, machine file store, G-code text store, and OPFS-to-WASM parameter-file bridge without moving file persistence or parameter semantics into the WASM core. |
| `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, WASM module loading, OPFS text-file round trip, generic session snapshot round trip, machine file text round trip, and G-code text round trip in a real browser runtime. | | `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, WASM module loading, OPFS text-file round trip, generic session snapshot round trip, machine file text round trip, and G-code text round trip in a real browser runtime. |
| `tests/browser/verify_interp_browser.sh` | Validates the interpreter-core WASM module loads through the interpreter JS SDK in a real browser runtime and runs selected positive and negative canonical fixtures through vendored LinuxCNC `Interp::execute()` plus `Interp::open()`/`read()`/`execute()` via the exported C ABI. | | `tests/browser/verify_interp_browser.sh` | Validates the interpreter-core WASM module loads through the interpreter JS SDK in a real browser runtime and runs selected positive and negative canonical fixtures through vendored LinuxCNC `Interp::execute()` plus `Interp::open()`/`read()`/`execute()` via the exported C ABI, including OPFS-backed parameter-file restore/save through vendored LinuxCNC source. |
| `tests/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM interpreter-core, OPFS, and browser smoke validation with shared WASM builds. | | `tests/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM interpreter-core, OPFS, and browser smoke validation with shared WASM builds. |
## Fixture Coverage ## Fixture Coverage
@@ -238,8 +243,10 @@ restore/save behavior through the exported C ABI, including out-of-order file
rejection, missing-file success, required numeric parameter writeback, removal rejection, missing-file success, required numeric parameter writeback, removal
of named-parameter-only lines from saved output, and the `.bak` backup produced of named-parameter-only lines from saved output, and the `.bak` backup produced
by vendored `save_parameters()`. by vendored `save_parameters()`.
OPFS validation is limited to the JavaScript host-boundary adapter plus the INI OPFS validation covers the JavaScript host-boundary adapter, the INI browser
browser smoke harness. Full browser coverage, full SDK coverage, and full smoke harness, and a browser interpreter smoke that copies OPFS-backed
parameter text through the SDK into vendored LinuxCNC parameter-file
restore/save. Full browser coverage, full SDK coverage, and full
machine-session validation remain future work. machine-session validation remain future work.
## WASM/Browser Fixture Matrix ## WASM/Browser Fixture Matrix

View File

@@ -43,7 +43,7 @@ semantic rewrites:
| Kinematics component lifecycle | Kinematics modules are initialized through LinuxCNC module entry points where native runtime probes exist, while HAL component init/ready/exit, HAL pin allocation, and RTAPI module metadata are handled by standalone shims. | | Kinematics component lifecycle | Kinematics modules are initialized through LinuxCNC module entry points where native runtime probes exist, while HAL component init/ready/exit, HAL pin allocation, and RTAPI module metadata are handled by standalone shims. |
| Go math C/C++ linkage | `genserkins` runtime probing compiles vendored `gomath.c` through a narrow C++ wrapper so LinuxCNC `genserfuncs.c` can link to the upstream Go math symbols without editing vendored source. | | Go math C/C++ linkage | `genserkins` runtime probing compiles vendored `gomath.c` through a narrow C++ wrapper so LinuxCNC `genserfuncs.c` can link to the upstream Go math symbols without editing vendored source. |
| Switchkins iterative forward | `genhexkins` runtime probing follows LinuxCNC switchkins iterative-forward behavior, including the first-call warmup path before asserting roundtrip convergence. | | Switchkins iterative forward | `genhexkins` runtime probing follows LinuxCNC switchkins iterative-forward behavior, including the first-call warmup path before asserting roundtrip convergence. |
| Browser storage | OPFS remains outside the native core; `runtime/opfs/file-service.js` owns browser text-file persistence, `runtime/opfs/path-model.js` owns host-side storage paths for INI, tool table, parameter, G-code, preview-cache, and session-snapshot content, `runtime/opfs/snapshot-store.js` owns generic JSON session snapshot persistence, and `runtime/opfs/machine-file-store.js` owns pure-text machine-file and G-code persistence. | | Browser storage | OPFS remains outside the native core; `runtime/opfs/file-service.js` owns browser text-file persistence, `runtime/opfs/path-model.js` owns host-side storage paths for INI, tool table, parameter, G-code, preview-cache, and session-snapshot content, `runtime/opfs/snapshot-store.js` owns generic JSON session snapshot persistence, `runtime/opfs/machine-file-store.js` owns pure-text machine-file and G-code persistence, and `runtime/opfs/linuxcnc-parameter-bridge.js` only copies parameter files between OPFS text storage and the LinuxCNC-backed WASM parameter-file ABI. |
## Enforced Non-Drift Rules ## Enforced Non-Drift Rules

View File

@@ -53,7 +53,7 @@ Current validation is intentionally mechanical:
| Dependency | LinuxCNC files that expose it | Standalone treatment | | Dependency | LinuxCNC files that expose it | Standalone treatment |
| --- | --- | --- | | --- | --- | --- |
| Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, parameter file paths | Allowed in native probes; the interpreter WASM C ABI now validates parameter-file restore/save by calling vendored `Interp::restore_parameters()` and `Interp::save_parameters()` against Emscripten filesystem paths; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js`, generic snapshot persistence in `runtime/opfs/snapshot-store.js`, and pure-text machine-file persistence in `runtime/opfs/machine-file-store.js` | | Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, parameter file paths | Allowed in native probes; the interpreter WASM C ABI validates parameter-file restore/save by calling vendored `Interp::restore_parameters()` and `Interp::save_parameters()` against Emscripten filesystem paths; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js`, generic snapshot persistence in `runtime/opfs/snapshot-store.js`, pure-text machine-file persistence in `runtime/opfs/machine-file-store.js`, and OPFS-to-WASM parameter-file copying in `runtime/opfs/linuxcnc-parameter-bridge.js` |
| RTAPI | `rtapi_*.h`, TP, posemath, motion headers | Minimal standalone shim in `runtime/core/shims/rtapi.h` | | RTAPI | `rtapi_*.h`, TP, posemath, motion headers | Minimal standalone shim in `runtime/core/shims/rtapi.h` |
| NML transport | `emc.hh`, motion/NML type headers | Transport is not ported; only the status/type edges needed by vendored compute code are exposed through standalone shims and probes | | NML transport | `emc.hh`, motion/NML type headers | Transport is not ported; only the status/type edges needed by vendored compute code are exposed through standalone shims and probes |
| HAL runtime | named parameter lookup, kinematics component lifecycle, and runtime status edges | Standalone HAL adapter under `runtime/core/linuxcnc_wrap/` | | HAL runtime | named parameter lookup, kinematics component lifecycle, and runtime status edges | Standalone HAL adapter under `runtime/core/linuxcnc_wrap/` |
@@ -82,8 +82,9 @@ Current validation is intentionally mechanical:
preview-cache, and session-snapshot targets. `runtime/opfs/snapshot-store.js` preview-cache, and session-snapshot targets. `runtime/opfs/snapshot-store.js`
adds a generic JSON session snapshot envelope. adds a generic JSON session snapshot envelope.
`runtime/opfs/machine-file-store.js` adds pure-text storage for INI, tool `runtime/opfs/machine-file-store.js` adds pure-text storage for INI, tool
table, parameter file, and G-code program content; browser session plumbing table, parameter file, and G-code program content.
that feeds OPFS-backed parameter files into the existing WASM `runtime/opfs/linuxcnc-parameter-bridge.js` copies OPFS-backed parameter
`restore_parameters()`/`save_parameters()` ABI, plus semantic loading for files into the interpreter SDK filesystem and writes back the LinuxCNC-saved
tool tables and machine-state restoration, remains future work. parameter file plus backup; UI-level session plumbing, semantic loading for
tool tables, and machine-state restoration remain future work.
- Native LinuxCNC GUI code remains out of scope for implementation. - Native LinuxCNC GUI code remains out of scope for implementation.

View File

@@ -0,0 +1,63 @@
import { loadTextFile, saveTextFile } from "./file-service.js";
import { parameterFilePath } from "./path-model.js";
const DEFAULT_WASM_PARAMETER_PATH = "/work/linuxcnc.var";
function requireParameterSdk(interp) {
for (const method of ["writeTextFile", "readTextFile", "restoreParameters", "saveParameters"]) {
if (typeof interp?.[method] !== "function") {
throw new Error(`interpreter SDK is missing ${method}().`);
}
}
}
function resolvePaths(machineId, options = {}) {
const opfsPath = options.opfsPath ?? parameterFilePath(machineId, options.filename);
const wasmPath = options.wasmPath ?? DEFAULT_WASM_PARAMETER_PATH;
return { opfsPath, wasmPath };
}
function readOptionalWasmTextFile(interp, path) {
try {
return interp.readTextFile(path);
} catch {
return null;
}
}
export async function restoreMachineParametersFromOpfs(interp, machineId, options = {}) {
requireParameterSdk(interp);
const { opfsPath, wasmPath } = resolvePaths(machineId, options);
const text = await loadTextFile(opfsPath, options.storage);
interp.writeTextFile(wasmPath, text);
return {
opfsPath,
wasmPath,
result: interp.restoreParameters(wasmPath),
};
}
export async function saveMachineParametersToOpfs(interp, machineId, values, options = {}) {
requireParameterSdk(interp);
const { opfsPath, wasmPath } = resolvePaths(machineId, options);
const existingText = await loadTextFile(opfsPath, options.storage);
interp.writeTextFile(wasmPath, existingText);
const result = interp.saveParameters(wasmPath, values);
const savedText = interp.readTextFile(wasmPath);
const backupText = readOptionalWasmTextFile(interp, `${wasmPath}.bak`);
await saveTextFile(opfsPath, savedText, options.storage);
if (backupText !== null) {
await saveTextFile(`${opfsPath}.bak`, backupText, options.storage);
}
return {
opfsPath,
backupOpfsPath: backupText === null ? null : `${opfsPath}.bak`,
wasmPath,
result,
savedText,
backupText,
};
}

View File

@@ -8,6 +8,14 @@
<pre id="status">running</pre> <pre id="status">running</pre>
<script type="module"> <script type="module">
import { createLinuxCncInterpSdk } from "../../runtime/sdk/src/index.js"; import { createLinuxCncInterpSdk } from "../../runtime/sdk/src/index.js";
import {
restoreMachineParametersFromOpfs,
saveMachineParametersToOpfs,
} from "../../runtime/opfs/linuxcnc-parameter-bridge.js";
import {
loadMachineTextFiles,
saveMachineTextFiles,
} from "../../runtime/opfs/machine-file-store.js";
import { import {
INTERP_BROWSER_FILE_FIXTURES, INTERP_BROWSER_FILE_FIXTURES,
INTERP_BROWSER_MDI_FIXTURES, INTERP_BROWSER_MDI_FIXTURES,
@@ -108,6 +116,71 @@
(await fetchText(`../fixtures/canon/${INTERP_INI_FIXTURE}.events`)).trimEnd(), (await fetchText(`../fixtures/canon/${INTERP_INI_FIXTURE}.events`)).trimEnd(),
); );
await saveMachineTextFiles("browser-interp", {
ini: "[EMC]\nMACHINE = browser-interp\n",
toolTable: "T0 P0 ; no tool\n",
parameters: [
"5161 10.5",
"5162 20.25",
"5220 1",
"5221 2.25",
"5399 44",
"<_named_param> 123",
"",
].join("\n"),
});
const restoredParameters = await restoreMachineParametersFromOpfs(
interp,
"browser-interp",
{ wasmPath: "/work/browser-linuxcnc.var" },
);
verifyExpectedOutput(
"opfs_restore_parameters",
restoredParameters.result,
[
"restore_parameters=0",
"parameter_5161=10.5",
"parameter_5162=20.25",
"parameter_5221=2.25",
"parameter_5399=44",
].join("\n"),
);
const savedParameters = await saveMachineParametersToOpfs(
interp,
"browser-interp",
{
5161: 12.34,
5162: 56.78,
5220: 1.0,
5221: 9.87,
5399: 66.6,
},
{ wasmPath: "/work/browser-linuxcnc.var" },
);
verifyExpectedOutput(
"opfs_save_parameters",
savedParameters.result,
[
"save_parameters=0",
"parameter_5161=12.34",
"parameter_5162=56.78",
"parameter_5221=9.87",
"parameter_5399=66.6",
].join("\n"),
);
const machineFiles = await loadMachineTextFiles("browser-interp");
verifyExpectedOutput(
"opfs_saved_parameter_text",
machineFiles.parameters,
[
"5161\t12.340000",
"5162\t56.780000",
"5221\t9.870000",
"5399\t66.600000",
"absent=<_named_param>",
].join("\n"),
);
status.textContent = "browser_interp_smoke=ok"; status.textContent = "browser_interp_smoke=ok";
} catch (error) { } catch (error) {
status.textContent = `browser_interp_smoke=fail ${error.stack || error.message}`; status.textContent = `browser_interp_smoke=fail ${error.stack || error.message}`;

View File

@@ -28,6 +28,10 @@ import {
saveGcodeProgram, saveGcodeProgram,
saveMachineTextFiles, saveMachineTextFiles,
} from "../../../runtime/opfs/machine-file-store.js"; } from "../../../runtime/opfs/machine-file-store.js";
import {
restoreMachineParametersFromOpfs,
saveMachineParametersToOpfs,
} from "../../../runtime/opfs/linuxcnc-parameter-bridge.js";
class MockFileHandle { class MockFileHandle {
constructor(name) { constructor(name) {
@@ -153,6 +157,57 @@ assert.equal(
); );
assert.equal(await loadGcodeProgram("fixture.ngc", { storage }), "G0 X0 Y0\nM2\n"); assert.equal(await loadGcodeProgram("fixture.ngc", { storage }), "G0 X0 Y0\nM2\n");
const bridgeFiles = new Map();
const bridgeInterp = {
writeTextFile(path, text) {
bridgeFiles.set(path, text);
},
readTextFile(path) {
if (!bridgeFiles.has(path)) {
throw new Error(`missing wasm file: ${path}`);
}
return bridgeFiles.get(path);
},
restoreParameters(path) {
return `restore_parameters=0\nrestore_path=${path}\n`;
},
saveParameters(path, values) {
bridgeFiles.set(path, "5161\t12.340000\n5162\t56.780000\n");
bridgeFiles.set(`${path}.bak`, "5161 0.0\n5162 0.0\n");
return `save_parameters=0\nsave_path=${path}\nvalues=${Object.keys(values).join(",")}\n`;
},
};
const restoredParameters = await restoreMachineParametersFromOpfs(
bridgeInterp,
"xyzab-tdr",
{ storage, wasmPath: "/work/bridge.var" },
);
assert.deepEqual(restoredParameters, {
opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
wasmPath: "/work/bridge.var",
result: "restore_parameters=0\nrestore_path=/work/bridge.var\n",
});
assert.equal(bridgeFiles.get("/work/bridge.var"), "5161 0.0\n5162 0.0\n");
const savedParameters = await saveMachineParametersToOpfs(
bridgeInterp,
"xyzab-tdr",
{ 5161: 12.34, 5162: 56.78 },
{ storage, wasmPath: "/work/bridge.var" },
);
assert.equal(savedParameters.opfsPath, "linuxcnc/machines/xyzab-tdr/linuxcnc.var");
assert.equal(savedParameters.backupOpfsPath, "linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak");
assert.equal(savedParameters.result.includes("save_parameters=0"), true);
assert.equal(
await loadTextFile("linuxcnc/machines/xyzab-tdr/linuxcnc.var", storage),
"5161\t12.340000\n5162\t56.780000\n",
);
assert.equal(
await loadTextFile("linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak", storage),
"5161 0.0\n5162 0.0\n",
);
await assert.rejects( await assert.rejects(
() => loadTextFile("linuxcnc/machines/missing.ini", storage), () => loadTextFile("linuxcnc/machines/missing.ini", storage),
/missing file/, /missing file/,