按建议,继续完成后续工作

结论:已新增 OPFS machine file store,支持 INI、tool table、parameter file 和 G-code 的纯文本保存/读取,并在 Node mock 与 Chromium smoke 中验证往返,不引入 CNC 文件格式语义。
This commit is contained in:
2026-06-08 03:34:24 +08:00
parent 72215f8400
commit 1a10e4f260
6 changed files with 114 additions and 9 deletions

View File

@@ -70,12 +70,14 @@ directory creation, text save/load, missing file behavior, invalid relative
paths, unavailable OPFS storage, and the host-side OPFS path model for INI, 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 without defining CNC machine-state semantics. round-trip store plus pure-text machine file and G-code stores without
defining CNC machine-state or file-format semantics.
The browser smoke script serves `wasm-port/` over localhost and runs Chromium The browser smoke script serves `wasm-port/` over localhost and runs Chromium
headless against a test page that imports the JS SDK, loads the INI WASM headless against a test page that imports the JS SDK, loads the INI WASM
module, queries vendored LinuxCNC INI parsing through the SDK, and performs an module, queries vendored LinuxCNC INI parsing through the SDK, and performs an
OPFS text-file and generic session snapshot round trip. OPFS text-file, generic session snapshot, machine file, and G-code text
round trip.
The aggregate host smoke script builds the INI WASM artifact once, then runs The aggregate host smoke script builds the INI WASM artifact once, then runs
the Node WASM smoke, the Node OPFS mock smoke, and the Chromium browser smoke. the Node WASM smoke, the Node OPFS mock smoke, and the Chromium browser smoke.
@@ -127,8 +129,8 @@ The validation fails if:
| Harness | Purpose | | Harness | Purpose |
| --- | --- | | --- | --- |
| `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/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter, path model, and session snapshot 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, and G-code text store used by the browser INI panel without moving file persistence into the WASM core. |
| `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, WASM module loading, OPFS text-file round trip, and generic session snapshot 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/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM, OPFS, and browser smoke validation with a single INI WASM build. | | `tests/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM, OPFS, and browser smoke validation with a single INI WASM build. |
## Fixture Coverage ## Fixture Coverage

View File

@@ -42,7 +42,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, and `runtime/opfs/snapshot-store.js` owns generic JSON session snapshot 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, and `runtime/opfs/machine-file-store.js` owns pure-text machine-file and G-code persistence. |
## Enforced Non-Drift Rules ## Enforced Non-Drift Rules
@@ -61,8 +61,9 @@ semantic rewrites:
- JS SDK validation is currently limited to the INI WASM wrapper around - JS SDK validation is currently limited to the INI WASM wrapper around
vendored LinuxCNC `inifile.cc`. vendored LinuxCNC `inifile.cc`.
- OPFS validation covers a Node mock of the file-service adapter, the - OPFS validation covers a Node mock of the file-service adapter, the
host-side path model, generic session snapshot storage, and a Chromium host-side path model, generic session snapshot storage, pure-text machine
localhost round trip for INI text-file and session snapshot persistence. file and G-code storage, and a Chromium localhost round trip for those
persistence paths.
- Host-side smoke validation is aggregated by - Host-side smoke validation is aggregated by
`tests/host/verify_host_smokes.sh` so Node, WASM, OPFS, and browser checks `tests/host/verify_host_smokes.sh` so Node, WASM, OPFS, and browser checks
run from one command. run from one command.

View File

@@ -51,7 +51,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; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js` and generic snapshot persistence in `runtime/opfs/snapshot-store.js` | | Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, parameter file paths | Allowed in native probes; 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` |
| 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/` |
@@ -75,7 +75,9 @@ Current validation is intentionally mechanical:
`runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now `runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now
defines paths for INI, tool table, parameter file, G-code program, defines paths for INI, tool table, parameter file, G-code program,
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; semantic loading for tool 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; semantic loading for tool
tables, parameter files, G-code programs, and machine-state restoration tables, parameter files, G-code programs, and machine-state restoration
remains future work. remains 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,54 @@
import { loadTextFile, saveTextFile } from "./file-service.js";
import {
gcodeProgramPath,
machineIniPath,
parameterFilePath,
toolTablePath,
} from "./path-model.js";
const MACHINE_FILE_PATHS = {
ini: machineIniPath,
toolTable: toolTablePath,
parameters: parameterFilePath,
};
export function machineFilePaths(machineId) {
return {
ini: machineIniPath(machineId),
toolTable: toolTablePath(machineId),
parameters: parameterFilePath(machineId),
};
}
export async function saveMachineTextFiles(machineId, files, options = {}) {
if (files === null || typeof files !== "object" || Array.isArray(files)) {
throw new Error("machine files must be a plain object.");
}
const paths = machineFilePaths(machineId);
for (const [name, pathFor] of Object.entries(MACHINE_FILE_PATHS)) {
if (Object.hasOwn(files, name)) {
await saveTextFile(pathFor(machineId), String(files[name]), options.storage);
}
}
return paths;
}
export async function loadMachineTextFiles(machineId, options = {}) {
const paths = machineFilePaths(machineId);
return {
ini: await loadTextFile(paths.ini, options.storage),
toolTable: await loadTextFile(paths.toolTable, options.storage),
parameters: await loadTextFile(paths.parameters, options.storage),
};
}
export async function saveGcodeProgram(filename, text, options = {}) {
const path = gcodeProgramPath(filename);
await saveTextFile(path, text, options.storage);
return path;
}
export async function loadGcodeProgram(filename, options = {}) {
return loadTextFile(gcodeProgramPath(filename), options.storage);
}

View File

@@ -14,6 +14,12 @@
loadSessionSnapshot, loadSessionSnapshot,
saveSessionSnapshot, saveSessionSnapshot,
} from "../../runtime/opfs/snapshot-store.js"; } from "../../runtime/opfs/snapshot-store.js";
import {
loadGcodeProgram,
loadMachineTextFiles,
saveGcodeProgram,
saveMachineTextFiles,
} from "../../runtime/opfs/machine-file-store.js";
const status = document.getElementById("status"); const status = document.getElementById("status");
@@ -72,6 +78,18 @@ JOINTS = 3
assertEqual(loadedSnapshot.payload.files.ini, opfsPath, "snapshot payload"); assertEqual(loadedSnapshot.payload.files.ini, opfsPath, "snapshot payload");
assertEqual(loadedSnapshot.metadata.source, "browser-smoke", "snapshot metadata"); assertEqual(loadedSnapshot.metadata.source, "browser-smoke", "snapshot metadata");
await saveMachineTextFiles("browser-smoke", {
ini: iniText,
toolTable: "T0 P0 ; no tool\n",
parameters: "5161 0.0\n",
});
const machineFiles = await loadMachineTextFiles("browser-smoke");
assertEqual(machineFiles.ini, iniText, "machine ini text");
assertEqual(machineFiles.toolTable, "T0 P0 ; no tool\n", "tool table text");
assertEqual(machineFiles.parameters, "5161 0.0\n", "parameter text");
await saveGcodeProgram("browser-smoke.ngc", "G0 X0 Y0\nM2\n");
assertEqual(await loadGcodeProgram("browser-smoke.ngc"), "G0 X0 Y0\nM2\n", "gcode text");
status.textContent = "browser_ini_opfs_smoke=ok"; status.textContent = "browser_ini_opfs_smoke=ok";
} catch (error) { } catch (error) {
status.textContent = `browser_ini_opfs_smoke=fail ${error.stack || error.message}`; status.textContent = `browser_ini_opfs_smoke=fail ${error.stack || error.message}`;

View File

@@ -21,6 +21,13 @@ import {
saveSessionSnapshot, saveSessionSnapshot,
validateSessionSnapshot, validateSessionSnapshot,
} from "../../../runtime/opfs/snapshot-store.js"; } from "../../../runtime/opfs/snapshot-store.js";
import {
loadGcodeProgram,
loadMachineTextFiles,
machineFilePaths,
saveGcodeProgram,
saveMachineTextFiles,
} from "../../../runtime/opfs/machine-file-store.js";
class MockFileHandle { class MockFileHandle {
constructor(name) { constructor(name) {
@@ -91,6 +98,7 @@ assert.deepEqual(defaultMachinePaths("xyzab-tdr"), {
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl", toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var", parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
}); });
assert.deepEqual(machineFilePaths("xyzab-tdr"), defaultMachinePaths("xyzab-tdr"));
const snapshotPayload = { const snapshotPayload = {
files: { files: {
@@ -129,6 +137,22 @@ await saveSessionSnapshot("session-1", snapshotPayload, {
}); });
assert.deepEqual(await loadSessionSnapshot("session-1", { storage }), snapshot); assert.deepEqual(await loadSessionSnapshot("session-1", { storage }), snapshot);
await saveMachineTextFiles("xyzab-tdr", {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
toolTable: "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n",
parameters: "5161 0.0\n5162 0.0\n",
}, { storage });
assert.deepEqual(await loadMachineTextFiles("xyzab-tdr", { storage }), {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
toolTable: "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n",
parameters: "5161 0.0\n5162 0.0\n",
});
assert.equal(
await saveGcodeProgram("fixture.ngc", "G0 X0 Y0\nM2\n", { storage }),
"linuxcnc/gcode/fixture.ngc",
);
assert.equal(await loadGcodeProgram("fixture.ngc", { storage }), "G0 X0 Y0\nM2\n");
await assert.rejects( await assert.rejects(
() => loadTextFile("linuxcnc/machines/missing.ini", storage), () => loadTextFile("linuxcnc/machines/missing.ini", storage),
/missing file/, /missing file/,
@@ -141,6 +165,10 @@ assert.throws(
() => machineIniPath("../escape"), () => machineIniPath("../escape"),
/Invalid machine id/, /Invalid machine id/,
); );
await assert.rejects(
() => saveMachineTextFiles("xyzab-tdr", null, { storage }),
/machine files must be a plain object/,
);
await saveTextFile(sessionSnapshotPath("bad-json"), "{", storage); await saveTextFile(sessionSnapshotPath("bad-json"), "{", storage);
await assert.rejects( await assert.rejects(
() => loadSessionSnapshot("bad-json", { storage }), () => loadSessionSnapshot("bad-json", { storage }),