diff --git a/wasm-port/docs/compatibility-validation.md b/wasm-port/docs/compatibility-validation.md index 523e250..d2ee1be 100644 --- a/wasm-port/docs/compatibility-validation.md +++ b/wasm-port/docs/compatibility-validation.md @@ -120,7 +120,8 @@ exported C ABI backed by vendored LinuxCNC `Interp::execute()` and named-parameter file path and negative interpreter fixtures with expected 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()`. +through vendored `Interp::restore_parameters()` and `Interp::save_parameters()`, +and to load/save LinuxCNC tool tables through vendored `tooldata_common.cc`. 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 @@ -254,10 +255,11 @@ the raw canonical-event display fed directly by LinuxCNC interpreter WASM output, the Node parameter/tool-table bridges that copy OPFS text through the SDK into vendored LinuxCNC file APIs, the Node machine-session bridge that groups INI, parameter, and tool-table loading, the random-toolchanger flag -derived from vendored LinuxCNC INI boolean parsing and passed through to the -LinuxCNC-backed tool-table ABI, and a browser interpreter smoke that uses the -same session bridge before saving OPFS-backed parameter and tool-table text -through vendored LinuxCNC file APIs. +derived from vendored LinuxCNC INI boolean parsing, INI-derived +`[RS274NGC]PARAMETER_FILE` and `[EMCIO]TOOL_TABLE` file names mapped to OPFS +machine files, and a browser interpreter smoke that uses the same session +bridge before saving OPFS-backed parameter and tool-table text through +vendored LinuxCNC file APIs. Full browser coverage, full SDK coverage, and richer machine-state validation remain future work. @@ -367,12 +369,36 @@ Browser interpreter negative coverage currently includes every fixture under - `tool_length_offset_not_found` - `tool_not_found` -Browser interpreter file-path coverage currently includes: +Browser interpreter file-path coverage currently includes the same +`INTERP_FILE_FIXTURES` list as the Node WASM smoke: -- `position_params` through the dedicated `canon_file/` expectation +- `minimal_linear` +- `arc_semantics` +- `length_units` +- `modal_incremental` +- `plane_selection` +- `coordinate_offsets` +- `g53_machine_coordinates` +- `feed_control_modes` +- `probe_semantics` +- `spindle_orient` +- `comment_logging` +- `numbered_params` +- `tool_semantics` +- `tool_table_setup` +- `tool_reload` +- `canned_cycles` +- `cutter_comp_motion` +- `threading_sync` +- `nurbs_g5_semantics` +- `nurbs_g6_semantics` +- `state_tag_motion` +- `canon_runtime_edges` +- `program_end_modal_reset` - `file_open_reset` - `percent_file_finish` - `oword_subroutine` +- `position_params` through the dedicated `canon_file/` expectation - `namedparam_semantics` through the INI-aware file ABI All current positive G-code fixtures have browser interpreter smoke coverage diff --git a/wasm-port/docs/drift-report.md b/wasm-port/docs/drift-report.md index bae036e..5a195d1 100644 --- a/wasm-port/docs/drift-report.md +++ b/wasm-port/docs/drift-report.md @@ -24,7 +24,7 @@ semantic rewrites: | --- | --- | | RTAPI | Minimal compatibility shim in `runtime/core/shims/rtapi.h`. | | HAL lookup | Standalone HAL adapter for `_hal[...]` named parameter reads. | -| INI lookup | Standalone INI adapter around vendored LinuxCNC INI parser behavior, including boolean conversion through LinuxCNC `iniFindBool()`. | +| INI lookup | Standalone INI adapter around vendored LinuxCNC INI parser behavior, including boolean conversion through LinuxCNC `iniFindBool()` and string lookup for INI-declared machine file names. | | Canonical output | Canonical calls are captured as test events instead of driving hardware. | | Feed-rate state | `SET_FEED_RATE` updates standalone canonical runtime state so vendored `convert_length_units()` can read it back through `GET_EXTERNAL_FEED_RATE()` during G20/G21 changes. | | Tool-data reload | LinuxCNC `RELOAD_TOOLDATA` is captured as a canonical test event; browser/native tool-table reload plumbing remains a future host/runtime adapter. | @@ -44,7 +44,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, `runtime/opfs/machine-file-store.js` owns pure-text machine-file and G-code persistence, `runtime/opfs/linuxcnc-parameter-bridge.js` only copies parameter files between OPFS text storage and the LinuxCNC-backed WASM parameter-file ABI, `runtime/opfs/linuxcnc-tool-table-bridge.js` only copies tool tables between OPFS text storage and the LinuxCNC-backed WASM tool-table ABI, and `runtime/opfs/linuxcnc-machine-session-bridge.js` groups INI, parameter, and tool-table loading while using the LinuxCNC-backed INI SDK for `[EMCIO]RANDOM_TOOLCHANGER` when available. | +| 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, `runtime/opfs/linuxcnc-parameter-bridge.js` only copies parameter files between OPFS text storage and the LinuxCNC-backed WASM parameter-file ABI, `runtime/opfs/linuxcnc-tool-table-bridge.js` only copies tool tables between OPFS text storage and the LinuxCNC-backed WASM tool-table ABI, and `runtime/opfs/linuxcnc-machine-session-bridge.js` groups INI, parameter, and tool-table loading while using the LinuxCNC-backed INI SDK for `[EMCIO]RANDOM_TOOLCHANGER`, `[RS274NGC]PARAMETER_FILE`, and `[EMCIO]TOOL_TABLE` when available. | ## Enforced Non-Drift Rules diff --git a/wasm-port/docs/source-reuse-map.md b/wasm-port/docs/source-reuse-map.md index 1810043..f315dd5 100644 --- a/wasm-port/docs/source-reuse-map.md +++ b/wasm-port/docs/source-reuse-map.md @@ -33,7 +33,7 @@ Current validation is intentionally mechanical: | Capability | LinuxCNC source files | Port classification | Standalone boundary | Current validation | | --- | --- | --- | --- | --- | -| INI parsing | `src/emc/ini/inifile.cc`, `inifile.h`, `inifile.hh` | Copy unchanged | Native file IO remains LinuxCNC-style in the vendored parser; browser OPFS integration remains outside this layer; `runtime/sdk/src/index.js` exports the INI SDK wrapper around the generated WASM C ABI, including LinuxCNC `iniFindBool()` for machine-session flags | Vendor byte sync, `linuxcnc_ini_probe`, `linuxcnc_inifile_source_probe`, `tests/wasm/node/verify_ini_wasm.sh`, `tests/browser/verify_ini_panel_browser.sh`, `tests/host/verify_host_smokes.sh` | +| INI parsing | `src/emc/ini/inifile.cc`, `inifile.h`, `inifile.hh` | Copy unchanged | Native file IO remains LinuxCNC-style in the vendored parser; browser OPFS integration remains outside this layer; `runtime/sdk/src/index.js` exports the INI SDK wrapper around the generated WASM C ABI, including LinuxCNC `iniFindBool()` for machine-session flags and LinuxCNC string lookup for INI-declared machine file names | Vendor byte sync, `linuxcnc_ini_probe`, `linuxcnc_inifile_source_probe`, `tests/wasm/node/verify_ini_wasm.sh`, `tests/browser/verify_ini_panel_browser.sh`, `tests/host/verify_host_smokes.sh` | | RTAPI compatibility headers | `src/rtapi/rtapi_*.h` in the manifest | Copy unchanged plus standalone shim include path | `runtime/core/shims/rtapi.h` supplies the minimal standalone RTAPI surface needed by vendored code | Vendor byte sync, compile coverage through dependent source probes | | Canon/NML-facing interpreter types | `src/emc/nml_intf/canon*.hh`, `emctool.h`, `interp_return.hh`, `motion_types.h`, `emcpose.*`, `emcpos.h`, `debugflags.h`, `src/emc/linuxcnc.h` | Copy unchanged plus narrow standalone status shim | NML transport is not ported; `runtime/core/shims/nml_intf/emc.hh` exposes only the `emcStatus` machine-units status edge currently needed by vendored interpreter conversion and initialization code | Vendor byte sync, dependent source probes, `linuxcnc_emc_status_probe`, `linuxcnc_tp_api_probe`, interpreter harnesses | | Motion state headers | `src/emc/motion/state_tag.h`, `emcmotcfg.h`, `simple_tp.h`, `motion.h`, `mot_priv.h`, `axis.h` | Copy unchanged | Realtime motion process is not ported; standalone probes seed the small motion status/config state required by TP calls | Vendor byte sync, `linuxcnc_tp_api_probe` | @@ -54,7 +54,7 @@ Current validation is intentionally mechanical: | Dependency | LinuxCNC files that expose it | Standalone treatment | | --- | --- | --- | -| Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, `tooldata_common.cc`, parameter file paths, tool table 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()` and tool-table load/save by calling vendored `tooldata_load()` and `tooldata_save()` 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`, OPFS-to-WASM parameter-file copying in `runtime/opfs/linuxcnc-parameter-bridge.js`, OPFS-to-WASM tool-table copying in `runtime/opfs/linuxcnc-tool-table-bridge.js`, and grouped INI/parameter/tool-table session loading in `runtime/opfs/linuxcnc-machine-session-bridge.js` | +| Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, `tooldata_common.cc`, parameter file paths, tool table 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()` and tool-table load/save by calling vendored `tooldata_load()` and `tooldata_save()` 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`, OPFS-to-WASM parameter-file copying in `runtime/opfs/linuxcnc-parameter-bridge.js`, OPFS-to-WASM tool-table copying in `runtime/opfs/linuxcnc-tool-table-bridge.js`, and grouped INI/parameter/tool-table session loading in `runtime/opfs/linuxcnc-machine-session-bridge.js`, including INI-derived `[RS274NGC]PARAMETER_FILE` and `[EMCIO]TOOL_TABLE` OPFS filename selection through the LinuxCNC-backed INI SDK | | 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/` | @@ -93,7 +93,8 @@ Current validation is intentionally mechanical: back the LinuxCNC-saved tool table. `runtime/opfs/linuxcnc-machine-session-bridge.js` groups INI, parameter, and tool-table loading into one host-side session load boundary and can derive - the random-toolchanger tooldata mode from `[EMCIO]RANDOM_TOOLCHANGER` through - the LinuxCNC-backed INI SDK. Full machine-state restoration remains future - work. + the random-toolchanger tooldata mode from `[EMCIO]RANDOM_TOOLCHANGER` plus + parameter/tool-table OPFS file names from `[RS274NGC]PARAMETER_FILE` and + `[EMCIO]TOOL_TABLE` through the LinuxCNC-backed INI SDK. Full machine-state + restoration remains future work. - Native LinuxCNC GUI code remains out of scope for implementation. diff --git a/wasm-port/runtime/opfs/linuxcnc-machine-session-bridge.js b/wasm-port/runtime/opfs/linuxcnc-machine-session-bridge.js index af62cc5..812b21d 100644 --- a/wasm-port/runtime/opfs/linuxcnc-machine-session-bridge.js +++ b/wasm-port/runtime/opfs/linuxcnc-machine-session-bridge.js @@ -21,22 +21,35 @@ function requireIniSdk(iniSdk) { if (!iniSdk) { return; } - for (const method of ["writeTextFile", "getBool"]) { + for (const method of ["writeTextFile", "getString", "getBool"]) { if (typeof iniSdk[method] !== "function") { throw new Error(`INI SDK is missing ${method}().`); } } } -function resolveSessionPaths(machineId, options = {}) { +function iniString(iniSdk, iniWasmPath, section, tag) { + if (!iniSdk) { + return undefined; + } + const value = iniSdk.getString(iniWasmPath, section, tag); + return value === null || value === "" ? undefined : value; +} + +function resolveSessionPaths(machineId, options = {}, iniValues = {}) { return { iniOpfsPath: options.iniOpfsPath ?? machineIniPath(machineId, options.iniFilename), iniWasmPath: options.iniWasmPath ?? DEFAULT_WASM_INI_PATH, parameterOpfsPath: - options.parameterOpfsPath ?? parameterFilePath(machineId, options.parameterFilename), + options.parameterOpfsPath ?? + parameterFilePath( + machineId, + options.parameterFilename ?? iniValues.parameterFilename, + ), parameterWasmPath: options.parameterWasmPath ?? DEFAULT_WASM_PARAMETER_PATH, toolTableOpfsPath: - options.toolTableOpfsPath ?? toolTablePath(machineId, options.toolTableFilename), + options.toolTableOpfsPath ?? + toolTablePath(machineId, options.toolTableFilename ?? iniValues.toolTableFilename), toolTableWasmPath: options.toolTableWasmPath ?? DEFAULT_WASM_TOOL_TABLE_PATH, }; } @@ -53,8 +66,18 @@ function randomToolChangerFromIni(iniSdk, iniWasmPath, iniText) { export async function loadMachineSessionFromOpfs(interp, machineId, options = {}) { requireSessionSdk(interp); requireIniSdk(options.iniSdk); - const paths = resolveSessionPaths(machineId, options); - const iniText = await loadTextFile(paths.iniOpfsPath, options.storage); + const iniPaths = resolveSessionPaths(machineId, options); + const iniText = await loadTextFile(iniPaths.iniOpfsPath, options.storage); + options.iniSdk?.writeTextFile(iniPaths.iniWasmPath, iniText); + const paths = resolveSessionPaths(machineId, options, { + parameterFilename: iniString( + options.iniSdk, + iniPaths.iniWasmPath, + "RS274NGC", + "PARAMETER_FILE", + ), + toolTableFilename: iniString(options.iniSdk, iniPaths.iniWasmPath, "EMCIO", "TOOL_TABLE"), + }); interp.writeTextFile(paths.iniWasmPath, iniText); const randomToolChanger = typeof options.randomToolChanger === "boolean" diff --git a/wasm-port/tests/browser/interp_smoke.html b/wasm-port/tests/browser/interp_smoke.html index 3fd5998..4b9b81e 100644 --- a/wasm-port/tests/browser/interp_smoke.html +++ b/wasm-port/tests/browser/interp_smoke.html @@ -14,6 +14,9 @@ import { saveMachineParametersToOpfs, } from "../../runtime/opfs/linuxcnc-parameter-bridge.js"; + import { + saveTextFile, + } from "../../runtime/opfs/file-service.js"; import { saveMachineToolTableToOpfs, } from "../../runtime/opfs/linuxcnc-tool-table-bridge.js"; @@ -323,6 +326,80 @@ ].join("\n"), ); + await saveMachineTextFiles("browser-ini-file-session", { + ini: [ + "[EMC]", + "MACHINE = browser-ini-file-session", + "", + "[RS274NGC]", + "PARAMETER_FILE = browser-custom.var", + "", + "[EMCIO]", + "TOOL_TABLE = browser-custom-tool.tbl", + "", + ].join("\n"), + }); + await saveTextFile( + "linuxcnc/machines/browser-ini-file-session/browser-custom.var", + [ + "5161 31.25", + "5162 62.5", + "5220 1", + "5221 4.5", + "5399 99", + "", + ].join("\n"), + ); + await saveTextFile( + "linuxcnc/machines/browser-ini-file-session/browser-custom-tool.tbl", + "T8 P8 Z4.5 D0.5 ;browser custom tool\n", + ); + const loadedIniFileSession = await loadMachineSessionFromOpfs( + interp, + "browser-ini-file-session", + { + iniSdk: ini, + iniWasmPath: "/work/browser-ini-file-session.ini", + parameterWasmPath: "/work/browser-custom.var", + toolTableWasmPath: "/work/browser-custom-tool.tbl", + }, + ); + if ( + loadedIniFileSession.parameters.opfsPath !== + "linuxcnc/machines/browser-ini-file-session/browser-custom.var" + ) { + throw new Error(`unexpected custom parameter path: ${loadedIniFileSession.parameters.opfsPath}`); + } + if ( + loadedIniFileSession.toolTable.opfsPath !== + "linuxcnc/machines/browser-ini-file-session/browser-custom-tool.tbl" + ) { + throw new Error(`unexpected custom tool path: ${loadedIniFileSession.toolTable.opfsPath}`); + } + verifyExpectedOutput( + "opfs_load_ini_named_parameter_file", + loadedIniFileSession.parameters.result, + [ + "restore_parameters=0", + "parameter_5161=31.25", + "parameter_5162=62.5", + "parameter_5221=4.5", + "parameter_5399=99", + ].join("\n"), + ); + verifyExpectedOutput( + "opfs_load_ini_named_tool_table", + loadedIniFileSession.toolTable.result, + [ + "tooldata_load=0", + "tool_1.toolno=8", + "tool_1.pocketno=8", + "tool_1.z=4.5", + "tool_1.diameter=0.5", + "tool_1.comment=browser custom tool", + ].join("\n"), + ); + status.textContent = "browser_interp_smoke=ok"; } catch (error) { status.textContent = `browser_interp_smoke=fail ${error.stack || error.message}`; diff --git a/wasm-port/tests/fixtures/interp-fixture-matrix.mjs b/wasm-port/tests/fixtures/interp-fixture-matrix.mjs index bcd4a00..34322f8 100644 --- a/wasm-port/tests/fixtures/interp-fixture-matrix.mjs +++ b/wasm-port/tests/fixtures/interp-fixture-matrix.mjs @@ -70,11 +70,7 @@ export const INTERP_FILE_FIXTURES = [ "oword_subroutine", ]; -export const INTERP_BROWSER_FILE_FIXTURES = [ - "file_open_reset", - "percent_file_finish", - "oword_subroutine", -]; +export const INTERP_BROWSER_FILE_FIXTURES = INTERP_FILE_FIXTURES; export const INTERP_INI_FIXTURE = "namedparam_semantics"; export const INTERP_POSITION_PARAMS_FILE_FIXTURE = "position_params"; diff --git a/wasm-port/tests/opfs/node/verify_file_service.mjs b/wasm-port/tests/opfs/node/verify_file_service.mjs index 5124cc2..ffc618a 100644 --- a/wasm-port/tests/opfs/node/verify_file_service.mjs +++ b/wasm-port/tests/opfs/node/verify_file_service.mjs @@ -201,6 +201,23 @@ const bridgeIniSdk = { writeTextFile(path, text) { bridgeFiles.set(`ini:${path}`, text); }, + getString(path, section, tag) { + if ( + path === "/work/ini-file-session.ini" && + section === "RS274NGC" && + tag === "PARAMETER_FILE" + ) { + return "custom.var"; + } + if ( + path === "/work/ini-file-session.ini" && + section === "EMCIO" && + tag === "TOOL_TABLE" + ) { + return "custom-tool.tbl"; + } + return null; + }, getBool(path, section, tag) { if ( path === "/work/ini-derived-session.ini" && @@ -350,6 +367,55 @@ assert.equal( "tooldata_load=0\nload_tool_path=/work/ini-derived-session-tool.tbl\nrandom_toolchanger=1\n", ); +await saveMachineTextFiles("ini-file-session", { + ini: [ + "[EMC]", + "MACHINE = ini-file-session", + "", + "[RS274NGC]", + "PARAMETER_FILE = custom.var", + "", + "[EMCIO]", + "TOOL_TABLE = custom-tool.tbl", + "", + ].join("\n"), +}, { storage }); +await saveTextFile( + "linuxcnc/machines/ini-file-session/custom.var", + "5161 77.0\n5162 88.0\n", + storage, +); +await saveTextFile( + "linuxcnc/machines/ini-file-session/custom-tool.tbl", + "T8 P8 Z4.5 D0.5\n", + storage, +); +const loadedIniFileSession = await loadMachineSessionFromOpfs( + bridgeInterp, + "ini-file-session", + { + storage, + iniSdk: bridgeIniSdk, + iniWasmPath: "/work/ini-file-session.ini", + parameterWasmPath: "/work/ini-file-session.var", + toolTableWasmPath: "/work/ini-file-session-tool.tbl", + }, +); +assert.equal( + loadedIniFileSession.parameters.opfsPath, + "linuxcnc/machines/ini-file-session/custom.var", +); +assert.equal( + loadedIniFileSession.toolTable.opfsPath, + "linuxcnc/machines/ini-file-session/custom-tool.tbl", +); +assert.equal(bridgeFiles.get("/work/ini-file-session.var"), "5161 77.0\n5162 88.0\n"); +assert.equal(bridgeFiles.get("/work/ini-file-session-tool.tbl"), "T8 P8 Z4.5 D0.5\n"); +assert.equal( + loadedIniFileSession.toolTable.result, + "tooldata_load=0\nload_tool_path=/work/ini-file-session-tool.tbl\nrandom_toolchanger=0\n", +); + await assert.rejects( () => loadTextFile("linuxcnc/machines/missing.ini", storage), /missing file/,