diff --git a/wasm-port/docs/compatibility-validation.md b/wasm-port/docs/compatibility-validation.md index 87c8e96..c765d89 100644 --- a/wasm-port/docs/compatibility-validation.md +++ b/wasm-port/docs/compatibility-validation.md @@ -107,7 +107,13 @@ and missing named-parameter lookup. It also writes selected G-code fixtures into the Emscripten filesystem through the SDK and runs them through LinuxCNC `Interp::open()`, `Interp::read()`, and `Interp::execute()` to validate the -file execution path. The same Node smoke writes LinuxCNC-format parameter +file execution path. It also writes the vendored LinuxCNC `xyzac-trt` and +`xyzbc-trt` table-rotary-tilting sample machine INI, `remap_subs/*.ngc`, and +`xyzac_switchkins.ngc`/`xyzbc_switchkins.ngc` demo files into the Emscripten +filesystem, then validates the WASM C ABI/SDK path for vendored LinuxCNC +`REMAP` parsing, O-word remap execution, `M68`/`M66` HAL synchronization, and +file `open()`/`read()`/`execute()` completion without JavaScript M-code or +kinematics semantics. The same Node smoke writes LinuxCNC-format parameter files into the Emscripten filesystem and validates vendored `Interp::restore_parameters()` and `Interp::save_parameters()`, including the saved parameter values, missing required numeric parameter defaulting, and the @@ -238,7 +244,7 @@ The validation fails if: | 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, including LinuxCNC-backed boolean conversion and machine-session file-name string lookup. | -| `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 interpreter-core WASM module can be built from vendored LinuxCNC interpreter/remap 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, run vendored `xyzac-trt`/`xyzbc-trt` switchkins remap demo files through the WASM C ABI/SDK path, and run parameter-file restore/save through vendored LinuxCNC `Interp::restore_parameters()` and `Interp::save_parameters()`. | | `tests/wasm/node/verify_tp_wasm.sh` | Validates a standalone trajectory-planner WASM module can be built from vendored LinuxCNC TP/TC/Ruckig support source, loaded in Node, and run the same linear, arc, and queued-line planner probe paths covered by the native TP harness. | | `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter, path model, session snapshot store including custom filenames and envelope/path rejection paths, machine file store, G-code text store including filename rejection paths, OPFS-to-WASM parameter/tool-table bridges, and grouped machine-session loading without moving file persistence, parameter semantics, or tool-table semantics into the WASM core. | | `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, INI/interpreter WASM module loading, LinuxCNC-backed INI machine-session file-name string lookup, OPFS text-file round trip, generic session snapshot round trip plus custom filename and envelope/path rejection paths, machine file text round trip, G-code text round trip plus filename rejection paths, and the INI panel UI's machine-session load with default OPFS parameter/tool-table file mapping, G-code run, and canonical-event display paths in a real browser runtime. | @@ -603,5 +609,6 @@ standalone remap descriptor path. It reads the vendored `xyzac-trt` and `xyzbc-trt` INI `REMAP` entries, resolves their `remap_subs/*.ngc` files through LinuxCNC `find_ngc_file()`, and validates the resulting `_setup.m_remapped` descriptors for `M428`, `M429`, and `M430`. Python -callbacks, O-word remap execution, and HAL synchronization remain runtime -boundaries. +callbacks remain runtime boundaries. Native and WASM validation now execute +the NGC remap/file paths through vendored LinuxCNC O-word dispatch and the +standalone HAL adapter boundary. diff --git a/wasm-port/docs/porting-steps-standalone.md b/wasm-port/docs/porting-steps-standalone.md index a710e84..420ab4e 100644 --- a/wasm-port/docs/porting-steps-standalone.md +++ b/wasm-port/docs/porting-steps-standalone.md @@ -806,13 +806,17 @@ Current five-axis switchkins status: `xyzac_switchkins.ngc`, `xyzbc_switchkins.ngc`, `xyzac_switchkins_test_1.ngc`, and `xyzac_switchkins_test_3.ngc` through LinuxCNC remap/file execution paths. +7. The WASM interpreter core now links vendored `interp_remap.cc` plus the + Python-only runtime edge stub, and the Node WASM smoke runs vendored + `xyzac_switchkins.ngc` and `xyzbc_switchkins.ngc` through the SDK C ABI + without JavaScript M-code or kinematics semantics. Next work: -1. Export the validated switchkins/remap status through the WASM SDK and INI - panel without adding JavaScript CNC semantics. -2. Add browser/Node smoke coverage that runs the vendored 5-axis remap demo - fixtures through the WASM interpreter boundary. +1. Add browser smoke/UI exposure for the validated switchkins/remap status + without adding JavaScript CNC semantics. +2. Continue broadening five-axis fixture coverage after the browser path is + stable. Involved LinuxCNC source: diff --git a/wasm-port/docs/source-reuse-map.md b/wasm-port/docs/source-reuse-map.md index a445f05..9379067 100644 --- a/wasm-port/docs/source-reuse-map.md +++ b/wasm-port/docs/source-reuse-map.md @@ -74,10 +74,10 @@ Current validation is intentionally mechanical: dual-rotary, and table-rotary-tilting LinuxCNC sample machines. NGC remap descriptors for the TRT sample machines are parsed through vendored `Interp::parse_remap()` and `find_ngc_file()` by - `linuxcnc_remap_parse_harness`. Full standalone execution still needs - LinuxCNC O-word remap dispatch, the `M68`/`M66` synchronization path, and - `_hal[motion.switchkins-type]` readback connected through the standalone HAL - boundary. + `linuxcnc_remap_parse_harness`; native and WASM Node validation execute the + selected TRT switchkins demo files through vendored LinuxCNC O-word remap + dispatch, file `open()`/`read()`/`execute()`, and the standalone HAL adapter + boundary for `M68`/`M66` plus `_hal[motion.switchkins-type]` readback. - Cutter compensation positive motion and negative interpreter paths are fixture-covered through vendored `interp_convert.cc` and `interp_queue.cc`. - Browser/WASM C ABI and JS SDK layers are now present for the INI parser, the diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp index 151910e..7e34239 100644 --- a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp @@ -11,6 +11,7 @@ #include #include "canon_event_sink.hh" +#include "emc/ini/inifile.hh" #include "linuxcnc_hal_adapter.hh" #include "linuxcnc_tool_adapter.hh" #include "nml_intf/emc.hh" @@ -58,6 +59,19 @@ void seed_hal_values() HAL_FLOAT, disconnected, false); } +void seed_switchkins_hal() +{ + standalone::reset_hal_adapter(); + + hal_data_u switchkins{}; + switchkins.f = 0.0; + standalone::set_hal_value(standalone::HalValueKind::Pin, "motion.switchkins-type", + HAL_FLOAT, switchkins); + standalone::set_hal_value(standalone::HalValueKind::Pin, "motion.analog-out-03", + HAL_FLOAT, switchkins); + standalone::link_hal_pin_alias("motion.analog-out-03", "motion.switchkins-type"); +} + void seed_tool_values() { standalone::reset_tool_adapter(); @@ -92,6 +106,29 @@ void initialize_minimal_interp(Interp &interp) std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers)); } +void initialize_fiveaxis_remap_interp(Interp &interp) +{ + seed_switchkins_hal(); + seed_tool_values(); + interp._setup.length_units = CANON_UNITS_MM; + interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE; + interp._setup.ijk_distance_mode = DISTANCE_MODE::INCREMENTAL; + interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE; + interp._setup.plane = CANON_PLANE::XY; + interp._setup.motion_mode = G_0; + interp._setup.percent_flag = false; + interp._setup.sequence_number = 0; + interp._setup.parameter_occurrence = 0; + interp._setup.num_spindles = 1; + interp._setup.feature_set = FEATURE_INI_VARS | FEATURE_HAL_PIN_VARS; + interp._setup.parameter_g73_peck_clearance = 1.0; + interp._setup.parameter_g83_peck_clearance = 1.0; + interp._setup.parameters[5599] = 1.0; + interp.load_tool_table(); + std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers)); + interp.init_named_parameters(); +} + void initialize_indexer_interp(Interp &interp) { standalone::reset_hal_adapter(); @@ -220,6 +257,51 @@ void append_named_value(std::ostringstream &output, Interp &interp, const char * output << name << ": rc=" << rc << " found=" << status << " value=" << value << "\n"; } +std::string parent_path(const char *path) +{ + std::string value = path ? path : ""; + const std::string::size_type slash = value.find_last_of('/'); + if (slash == std::string::npos) { + return "."; + } + if (slash == 0) { + return "/"; + } + return value.substr(0, slash); +} + +bool parse_machine_remaps(Interp &interp, std::ostringstream &output, const char *ini_path) +{ + const std::string machine_dir = parent_path(ini_path); + const std::string remap_dir = machine_dir + "/remap_subs"; + + linuxcnc::IniFile ini(ini_path ? ini_path : ""); + const bool opened = static_cast(ini); + output << "fiveaxis_ini_open=" << (opened ? 1 : 0) << "\n"; + if (!opened) { + return false; + } + + std::snprintf(interp._setup.program_prefix, sizeof(interp._setup.program_prefix), "%s", + machine_dir.c_str()); + interp._setup.subroutines[0] = strstore(remap_dir.c_str()); + + bool ok = true; + int remap_count = 0; + while (auto remap = ini.findString(++remap_count, "REMAP", "RS274NGC")) { + const int line_number = ini.lineOf(remap_count, "REMAP", "RS274NGC").second; + const int rc = interp.parse_remap(remap->c_str(), line_number); + output << "fiveaxis_parse_remap_" << remap_count << "=" << rc << "\n"; + ok &= rc == INTERP_OK; + } + + remap_count -= 1; + output << "fiveaxis_parsed_remap_count=" << remap_count << "\n"; + ok &= remap_count == 3; + output << "fiveaxis_remaps_ready=" << (ok ? 1 : 0) << "\n"; + return ok; +} + void apply_parameter_assignments(Interp &interp, const char *assignments) { std::istringstream input(assignments ? assignments : ""); @@ -368,6 +450,102 @@ char *lcinterp_run_file(const char *path) return lcinterp_run_file_with_ini(path, nullptr); } +EMSCRIPTEN_KEEPALIVE +char *lcinterp_run_fiveaxis_remap_file(const char *path, const char *ini_path) +{ + if (ini_path && ini_path[0] != '\0') { + setenv("INI_FILE_NAME", ini_path, 1); + } else { + unsetenv("INI_FILE_NAME"); + } + + Interp interp; + standalone::reset_canon_events(); + initialize_fiveaxis_remap_interp(interp); + + std::ostringstream output; + bool ok = parse_machine_remaps(interp, output, ini_path); + + const int open_rc = interp.open(path); + output << "fiveaxis_file_open=" << open_rc << "\n"; + if (open_rc != INTERP_OK) { + append_error_text(output, interp, "fiveaxis_file_error_text=", open_rc); + output << "fiveaxis_linuxcnc_remap_file_execute=0\n"; + unsetenv("INI_FILE_NAME"); + return copy_result(output.str()); + } + + constexpr int max_file_steps = 4096; + int file_read_count = 0; + int file_execute_count = 0; + int file_finish_count = 0; + int final_rc = INTERP_OK; + bool reached_exit = false; + + while ((file_read_count < max_file_steps) && (file_execute_count < max_file_steps)) { + const int read_rc = interp.read(); + if (read_rc == INTERP_ENDFILE) { + output << "fiveaxis_file_read_eof=" << read_rc << "\n"; + final_rc = read_rc; + break; + } + + ++file_read_count; + if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) { + output << "fiveaxis_file_read_" << file_read_count << "=" << read_rc << "\n"; + append_error_text(output, interp, "fiveaxis_file_error_text=", read_rc); + final_rc = read_rc; + ok = false; + break; + } + + const int execute_rc = interp.execute(); + ++file_execute_count; + final_rc = execute_rc; + if (execute_rc == INTERP_EXECUTE_FINISH) { + ++file_finish_count; + } + + if ((execute_rc != INTERP_OK) && (execute_rc != INTERP_EXIT) && + (execute_rc != INTERP_EXECUTE_FINISH)) { + output << "fiveaxis_file_execute_" << file_execute_count << "=" + << execute_rc << "\n"; + append_error_text(output, interp, "fiveaxis_file_error_text=", execute_rc); + ok = false; + break; + } + + if (execute_rc == INTERP_EXIT) { + reached_exit = true; + break; + } + } + + int hal_status = 0; + double switchkins_type = 0.0; + const int hal_rc = + interp.find_named_param("_hal[motion.switchkins-type]", &hal_status, &switchkins_type); + + output << "fiveaxis_file_read_count=" << file_read_count << "\n"; + output << "fiveaxis_file_execute_count=" << file_execute_count << "\n"; + output << "fiveaxis_file_finish_count=" << file_finish_count << "\n"; + output << "fiveaxis_file_final_rc=" << final_rc << "\n"; + output << "fiveaxis_file_reached_exit=" << (reached_exit ? 1 : 0) << "\n"; + output << "fiveaxis_hal_switchkins: rc=" << hal_rc << " found=" << hal_status + << " value=" << switchkins_type << "\n"; + + ok &= reached_exit; + ok &= file_read_count > 0; + ok &= file_execute_count > 0; + ok &= file_read_count < max_file_steps; + ok &= file_execute_count < max_file_steps; + ok &= hal_rc == INTERP_OK && hal_status == 1 && switchkins_type == 0.0; + + output << "fiveaxis_linuxcnc_remap_file_execute=" << (ok ? 1 : 0) << "\n"; + unsetenv("INI_FILE_NAME"); + return copy_result(output.str()); +} + EMSCRIPTEN_KEEPALIVE char *lcinterp_restore_parameters(const char *path) { diff --git a/wasm-port/runtime/sdk/src/linuxcnc-interp.js b/wasm-port/runtime/sdk/src/linuxcnc-interp.js index fd2c966..c38eba5 100644 --- a/wasm-port/runtime/sdk/src/linuxcnc-interp.js +++ b/wasm-port/runtime/sdk/src/linuxcnc-interp.js @@ -74,6 +74,10 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) { return callStringResult(mod, "lcinterp_run_file_with_ini", path, iniPath); }, + runFiveAxisRemapFile(path, iniPath) { + return callStringResult(mod, "lcinterp_run_fiveaxis_remap_file", path, iniPath); + }, + restoreParameters(path) { return callStringResult(mod, "lcinterp_restore_parameters", path); }, diff --git a/wasm-port/tests/wasm/node/verify_interp_wasm.mjs b/wasm-port/tests/wasm/node/verify_interp_wasm.mjs index cd5c69a..6053919 100644 --- a/wasm-port/tests/wasm/node/verify_interp_wasm.mjs +++ b/wasm-port/tests/wasm/node/verify_interp_wasm.mjs @@ -1,4 +1,4 @@ -import { readFileSync } from "node:fs"; +import { readdirSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; import assert from "node:assert/strict"; @@ -88,6 +88,41 @@ const interp = await createLinuxCncInterpSdk({ }, }); +function writeVendorTextFile(sourcePath, wasmPath) { + interp.writeTextFile(wasmPath, readFileSync(sourcePath, "utf8")); +} + +function writeFiveAxisTrtMachineFiles() { + const sourceDir = resolve( + rootDir, + "vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting", + ); + const wasmDir = "/work/fiveaxis/table-rotary-tilting"; + + writeVendorTextFile(resolve(sourceDir, "xyzac-trt.ini"), `${wasmDir}/xyzac-trt.ini`); + writeVendorTextFile(resolve(sourceDir, "xyzbc-trt.ini"), `${wasmDir}/xyzbc-trt.ini`); + + for (const filename of readdirSync(resolve(sourceDir, "remap_subs"))) { + if (filename.endsWith(".ngc")) { + writeVendorTextFile( + resolve(sourceDir, "remap_subs", filename), + `${wasmDir}/remap_subs/${filename}`, + ); + } + } + + writeVendorTextFile( + resolve(sourceDir, "demos/xyzac_switchkins.ngc"), + `${wasmDir}/demos/xyzac_switchkins.ngc`, + ); + writeVendorTextFile( + resolve(sourceDir, "demos/xyzbc_switchkins.ngc"), + `${wasmDir}/demos/xyzbc_switchkins.ngc`, + ); + + return wasmDir; +} + const namedParamIniPath = "/work/namedparams.ini"; interp.writeTextFile( namedParamIniPath, @@ -303,6 +338,54 @@ verifyExpectedOutput( ).trimEnd(), ); +const fiveAxisTrtDir = writeFiveAxisTrtMachineFiles(); +verifyExpectedOutput( + "fiveaxis_xyzac_switchkins_wasm", + interp.runFiveAxisRemapFile( + `${fiveAxisTrtDir}/demos/xyzac_switchkins.ngc`, + `${fiveAxisTrtDir}/xyzac-trt.ini`, + ), + [ + "fiveaxis_ini_open=1", + "fiveaxis_parse_remap_1=0", + "fiveaxis_parse_remap_2=0", + "fiveaxis_parse_remap_3=0", + "fiveaxis_parsed_remap_count=3", + "fiveaxis_remaps_ready=1", + "fiveaxis_file_open=0", + "fiveaxis_file_read_count=620", + "fiveaxis_file_execute_count=620", + "fiveaxis_file_finish_count=21", + "fiveaxis_file_final_rc=1", + "fiveaxis_file_reached_exit=1", + "fiveaxis_hal_switchkins: rc=0 found=1 value=0", + "fiveaxis_linuxcnc_remap_file_execute=1", + ].join("\n"), +); +verifyExpectedOutput( + "fiveaxis_xyzbc_switchkins_wasm", + interp.runFiveAxisRemapFile( + `${fiveAxisTrtDir}/demos/xyzbc_switchkins.ngc`, + `${fiveAxisTrtDir}/xyzbc-trt.ini`, + ), + [ + "fiveaxis_ini_open=1", + "fiveaxis_parse_remap_1=0", + "fiveaxis_parse_remap_2=0", + "fiveaxis_parse_remap_3=0", + "fiveaxis_parsed_remap_count=3", + "fiveaxis_remaps_ready=1", + "fiveaxis_file_open=0", + "fiveaxis_file_read_count=620", + "fiveaxis_file_execute_count=620", + "fiveaxis_file_finish_count=21", + "fiveaxis_file_final_rc=1", + "fiveaxis_file_reached_exit=1", + "fiveaxis_hal_switchkins: rc=0 found=1 value=0", + "fiveaxis_linuxcnc_remap_file_execute=1", + ].join("\n"), +); + const parameterFilePath = "/work/rs274ngc.var"; interp.writeTextFile( parameterFilePath, diff --git a/wasm-port/tools/build_wasm_core.sh b/wasm-port/tools/build_wasm_core.sh index fd6214a..95db5a3 100755 --- a/wasm-port/tools/build_wasm_core.sh +++ b/wasm-port/tools/build_wasm_core.sh @@ -59,11 +59,12 @@ INTERP_CORE_SOURCES=( "$VENDOR_DIR/src/emc/rs274ngc/interp_namedparams.cc" "$VENDOR_DIR/src/emc/rs274ngc/interp_write.cc" "$VENDOR_DIR/src/emc/rs274ngc/interp_o_word.cc" + "$VENDOR_DIR/src/emc/rs274ngc/interp_remap.cc" "$VENDOR_DIR/src/emc/rs274ngc/rs274ngc_pre.cc" "$VENDOR_DIR/src/emc/tooldata/tooldata_common.cc" "$WRAP_DIR/linuxcnc_hal_adapter.cpp" "$WRAP_DIR/linuxcnc_emc_status_runtime.cpp" - "$WRAP_DIR/linuxcnc_interp_edge_stubs.cpp" + "$WRAP_DIR/linuxcnc_interp_python_edge_stubs.cpp" "$WRAP_DIR/linuxcnc_runtime_state_stubs.cpp" "$WRAP_DIR/linuxcnc_tool_adapter.cpp" "$WRAP_DIR/linuxcnc_interp_minimal_runtime.cpp" @@ -96,5 +97,5 @@ link_wasm_module \ -s STACK_SIZE=2MB \ -s NO_EXIT_RUNTIME=1 \ -s FORCE_FILESYSTEM=1 \ - -s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_run_file_with_ini","_lcinterp_restore_parameters","_lcinterp_save_parameters","_lcinterp_load_tool_table","_lcinterp_load_tool_table_random","_lcinterp_save_tool_table","_lcinterp_probe_init_and_synch","_lcinterp_probe_indexer","_lcinterp_probe_named_parameters","_lcinterp_free_string"]' \ + -s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_run_file_with_ini","_lcinterp_run_fiveaxis_remap_file","_lcinterp_restore_parameters","_lcinterp_save_parameters","_lcinterp_load_tool_table","_lcinterp_load_tool_table_random","_lcinterp_save_tool_table","_lcinterp_probe_init_and_synch","_lcinterp_probe_indexer","_lcinterp_probe_named_parameters","_lcinterp_free_string"]' \ -s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'