按规划继续工作

结论: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
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
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
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
`Interp::open()`/`read()`/`execute()` paths, including the INI-aware
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
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_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_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. |
## 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
of named-parameter-only lines from saved output, and the `.bak` backup produced
by vendored `save_parameters()`.
OPFS validation is limited to the JavaScript host-boundary adapter plus the INI
browser smoke harness. Full browser coverage, full SDK coverage, and full
OPFS validation covers the JavaScript host-boundary adapter, the INI browser
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.
## 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. |
| 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. |
| 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

View File

@@ -53,7 +53,7 @@ Current validation is intentionally mechanical:
| 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` |
| 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/` |
@@ -82,8 +82,9 @@ Current validation is intentionally mechanical:
preview-cache, and session-snapshot targets. `runtime/opfs/snapshot-store.js`
adds a generic JSON session snapshot envelope.
`runtime/opfs/machine-file-store.js` adds pure-text storage for INI, tool
table, parameter file, and G-code program content; browser session plumbing
that feeds OPFS-backed parameter files into the existing WASM
`restore_parameters()`/`save_parameters()` ABI, plus semantic loading for
tool tables and machine-state restoration, remains future work.
table, parameter file, and G-code program content.
`runtime/opfs/linuxcnc-parameter-bridge.js` copies OPFS-backed parameter
files into the interpreter SDK filesystem and writes back the LinuxCNC-saved
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.

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>
<script type="module">
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 {
INTERP_BROWSER_FILE_FIXTURES,
INTERP_BROWSER_MDI_FIXTURES,
@@ -108,6 +116,71 @@
(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";
} catch (error) {
status.textContent = `browser_interp_smoke=fail ${error.stack || error.message}`;

View File

@@ -28,6 +28,10 @@ import {
saveGcodeProgram,
saveMachineTextFiles,
} from "../../../runtime/opfs/machine-file-store.js";
import {
restoreMachineParametersFromOpfs,
saveMachineParametersToOpfs,
} from "../../../runtime/opfs/linuxcnc-parameter-bridge.js";
class MockFileHandle {
constructor(name) {
@@ -153,6 +157,57 @@ assert.equal(
);
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(
() => loadTextFile("linuxcnc/machines/missing.ini", storage),
/missing file/,