From a16f3605faaec19e32fc836a598737ae7cbbbc49 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Mon, 8 Jun 2026 08:31:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E8=A7=84=E5=88=92=E7=BB=A7=E7=BB=AD?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结论:接入 vendored LinuxCNC tooldata_common.cc 负责刀具表解析与保存,WASM/SDK/OPFS/UI 仅做运行时边界搬运;native、WASM、OPFS 和浏览器 smoke 验证已通过。 --- wasm-port/docs/compatibility-validation.md | 22 +- wasm-port/docs/drift-report.md | 11 +- wasm-port/docs/source-reuse-map.md | 16 +- .../linuxcnc_wrap/linuxcnc_interp_wasm.cpp | 54 +++ .../linuxcnc_wrap/linuxcnc_tool_adapter.cpp | 82 ++++ wasm-port/runtime/core/shims/tooldata.hh | 50 +++ .../runtime/core/shims/tooldata/tooldata.hh | 22 +- .../opfs/linuxcnc-machine-session-bridge.js | 59 +++ .../opfs/linuxcnc-tool-table-bridge.js | 46 ++ wasm-port/runtime/sdk/README.md | 2 + wasm-port/runtime/sdk/src/linuxcnc-interp.js | 8 + wasm-port/runtime/ui/ini-panel/app.js | 185 +++++++- wasm-port/runtime/ui/ini-panel/index.html | 52 ++- wasm-port/tests/browser/ini_panel_smoke.html | 116 +++++ wasm-port/tests/browser/interp_smoke.html | 64 ++- .../tests/browser/verify_ini_panel_browser.sh | 3 + wasm-port/tests/host/verify_host_smokes.sh | 2 +- .../tests/native/verify_native_probes.sh | 1 + .../tests/opfs/node/verify_file_service.mjs | 73 +++ .../tests/wasm/node/verify_interp_wasm.mjs | 53 +++ wasm-port/tools/build_native_probes.sh | 7 + wasm-port/tools/build_wasm_core.sh | 3 +- wasm-port/tools/source-manifest.txt | 1 + .../src/emc/tooldata/tooldata_common.cc | 423 ++++++++++++++++++ 24 files changed, 1302 insertions(+), 53 deletions(-) create mode 100644 wasm-port/runtime/core/shims/tooldata.hh create mode 100644 wasm-port/runtime/opfs/linuxcnc-machine-session-bridge.js create mode 100644 wasm-port/runtime/opfs/linuxcnc-tool-table-bridge.js create mode 100644 wasm-port/vendor/linuxcnc/src/emc/tooldata/tooldata_common.cc diff --git a/wasm-port/docs/compatibility-validation.md b/wasm-port/docs/compatibility-validation.md index 484ed10..fff2915 100644 --- a/wasm-port/docs/compatibility-validation.md +++ b/wasm-port/docs/compatibility-validation.md @@ -173,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, 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, including OPFS-backed parameter-file restore/save through vendored LinuxCNC source. | +| `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, 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, OPFS text-file round trip, generic session snapshot round trip, machine file text round trip, G-code text round trip, and the INI panel UI's machine-session load, G-code run, and canonical-event display paths 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, including OPFS-backed parameter-file restore/save and tool-table load/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 @@ -242,12 +242,18 @@ The Node WASM interpreter smoke also covers LinuxCNC parameter-file 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()`. +by vendored `save_parameters()`. It also covers LinuxCNC tool-table +load/save behavior through vendored `tooldata_common.cc`, with the SDK only +copying text into the Emscripten filesystem and calling the exported C ABI. 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. +smoke harness, the INI panel UI's machine-session load and G-code run buttons, +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, 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. ## WASM/Browser Fixture Matrix diff --git a/wasm-port/docs/drift-report.md b/wasm-port/docs/drift-report.md index 38ee422..2dea950 100644 --- a/wasm-port/docs/drift-report.md +++ b/wasm-port/docs/drift-report.md @@ -28,6 +28,7 @@ semantic rewrites: | 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. | +| Tool table storage | Vendored LinuxCNC `tooldata_common.cc` owns tool-table parsing and formatting; the standalone boundary only supplies the in-memory `tooldata_get()`/`tooldata_put()` callbacks and exposes load/save through the WASM C ABI. | | Tool slot status | Standalone tool adapter state supplies `GET_EXTERNAL_TOOL_SLOT()` and `GET_EXTERNAL_SELECTED_TOOL_SLOT()` for vendored `Interp::synch()`; `linuxcnc_interp_init_harness` guards current/selected slot synchronization. | | Interpreter state tags | LinuxCNC `UPDATE_TAG` callbacks are captured from the state tags packed by vendored interpreter code; standalone code does not derive modal state. | | Rotary indexer lock state | LinuxCNC `UNLOCK_ROTARY` and `LOCK_ROTARY` callbacks are captured from the vendored single-axis indexer path; standalone code does not implement rotary-indexing semantics. | @@ -43,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, and `runtime/opfs/linuxcnc-parameter-bridge.js` only copies parameter files between OPFS text storage and the LinuxCNC-backed WASM parameter-file ABI. | +| 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 without interpreting their CNC semantics. | ## Enforced Non-Drift Rules @@ -63,11 +64,13 @@ semantic rewrites: - JS SDK validation now covers the INI WASM wrapper around vendored LinuxCNC `inifile.cc` and the interpreter-core SDK wrapper around the existing exported C ABI. The interpreter SDK only manages strings, Emscripten file - writes, and calls into vendored LinuxCNC execution paths. + writes, and calls into vendored LinuxCNC execution paths, including + parameter-file restore/save and tool-table load/save. - OPFS validation covers a Node mock of the file-service adapter, the host-side path model, generic session snapshot storage, pure-text machine - file and G-code storage, and a Chromium localhost round trip for those - persistence paths. + file and G-code storage, parameter/tool-table bridge copying into the + interpreter SDK filesystem, grouped machine-session loading, and a Chromium + localhost round trip for those persistence paths. - Host-side smoke validation is aggregated by `tests/host/verify_host_smokes.sh` so Node, WASM, OPFS, and browser checks run from one command. diff --git a/wasm-port/docs/source-reuse-map.md b/wasm-port/docs/source-reuse-map.md index 36adf8c..6fa4a1b 100644 --- a/wasm-port/docs/source-reuse-map.md +++ b/wasm-port/docs/source-reuse-map.md @@ -47,13 +47,14 @@ Current validation is intentionally mechanical: | RS274 interpreter state and parser | `src/emc/rs274ngc/modal_state.*`, `interp_internal.*`, `interp_read.cc`, `interp_check.cc`, `interp_execute.cc`, `interp_find.cc`, `interp_array.cc`, `interp_queue.*`, `rs274ngc*`, `units.h` | Copy unchanged | Python/remap/runtime edges are isolated in standalone wrappers and shims; parser and execution logic remain LinuxCNC source | Vendor byte sync, per-file source probes, interpreter harness fixtures | | RS274 conversion semantics | `src/emc/rs274ngc/interp_convert.cc`, `interp_arc.cc`, `interp_inverse.cc`, `interp_cycles.cc`, `interp_g7x.cc`, `interp_o_word.cc`, `interp_write.cc` | Copy unchanged | Canonical calls are captured by standalone event sink functions; conversion behavior stays in vendored LinuxCNC files; feed-rate state is read back through the canonical runtime boundary during length-unit conversion | Vendor byte sync, per-file source probes, canonical fixture suite, no standalone `Interp::convert_g()` guard | | Named parameters and tool slot status | `src/emc/rs274ngc/interp_namedparams.cc`, related interpreter headers | Copy unchanged | `_ini[...]` and `_hal[...]` resolve through standalone INI/HAL adapters while lookup order stays LinuxCNC-derived; current/selected tool slot reads for vendored `Interp::synch()` come from the standalone tool adapter | Vendor byte sync, source probe, `linuxcnc_namedparam_harness`, `linuxcnc_interp_init_harness` | +| Tool table parsing and formatting | `src/emc/tooldata/tooldata_common.cc` | Copy unchanged plus standalone storage callbacks | Native file parsing/formatting stays in vendored LinuxCNC source; the standalone tool adapter supplies `tooldata_get()`/`tooldata_put()` storage and index lookup, while OPFS remains a host-side persistence boundary | Vendor byte sync, `linuxcnc_tooldata_common_source_probe`, `tests/wasm/node/verify_interp_wasm.sh` load/save tool-table assertions | | Dynamic interpreter base | `src/emc/rs274ngc/interp_base.*` | Copy unchanged | `interp_base.cc` source probe uses standalone `EMC2_HOME` compile-time path boundary for LinuxCNC dynamic interpreter lookup | Vendor byte sync, `linuxcnc_interp_base_source_probe` | ## Dependency Matrix | 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 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` | +| 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` | | 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/` | @@ -74,8 +75,9 @@ Current validation is intentionally mechanical: the current interpreter-core smoke scope. The interpreter SDK is a thin allocation, filesystem, and C ABI wrapper over vendored LinuxCNC execution paths, including parameter-file restore/save through vendored - `rs274ngc_pre.cc`; it does not define G-code or parameter semantics. Full - planner/session SDK coverage remains future work. + `rs274ngc_pre.cc` and tool-table load/save through vendored + `tooldata_common.cc`; it does not define G-code, parameter, or tool-table + semantics. Full planner/session SDK coverage remains future work. - OPFS persistence is connected to the INI panel through the host-side `runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now defines paths for INI, tool table, parameter file, G-code program, @@ -85,6 +87,10 @@ Current validation is intentionally mechanical: 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. + parameter file plus backup. `runtime/opfs/linuxcnc-tool-table-bridge.js` + copies OPFS-backed tool tables into the interpreter SDK filesystem and writes + 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. Full + machine-state restoration remains future work. - Native LinuxCNC GUI code remains out of scope for implementation. 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 6814ae4..a749ddc 100644 --- a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp @@ -13,6 +13,7 @@ #include "canon_event_sink.hh" #include "linuxcnc_hal_adapter.hh" #include "linuxcnc_tool_adapter.hh" +#include "tooldata.hh" namespace { @@ -145,6 +146,35 @@ void append_parameter_state(std::ostringstream &output, const Interp &interp) output << "parameter_5399=" << interp._setup.parameters[5399] << "\n"; } +void append_tool_entry(std::ostringstream &output, const char *prefix, const CANON_TOOL_TABLE &tool) +{ + output << prefix << ".toolno=" << tool.toolno << "\n"; + output << prefix << ".pocketno=" << tool.pocketno << "\n"; + output << prefix << ".z=" << tool.offset.tran.z << "\n"; + output << prefix << ".diameter=" << tool.diameter << "\n"; + output << prefix << ".frontangle=" << tool.frontangle << "\n"; + output << prefix << ".backangle=" << tool.backangle << "\n"; + output << prefix << ".orientation=" << tool.orientation << "\n"; + output << prefix << ".comment=" << tool.comment << "\n"; +} + +void append_tool_table_state(std::ostringstream &output) +{ + output << "tooldata_last_index=" << tooldata_last_index_get() << "\n"; + + CANON_TOOL_TABLE tool = standalone::tool_entry_init(); + if (standalone::get_tool_entry(&tool, 0) == 0) { + append_tool_entry(output, "tool_0", tool); + } + if (standalone::get_tool_entry(&tool, 1) == 0) { + append_tool_entry(output, "tool_1", tool); + } + if (standalone::get_tool_entry(&tool, 2) == 0) { + append_tool_entry(output, "tool_2", tool); + } + output << "tool_index_for_tool_2=" << standalone::find_tool_index_for_tool(2) << "\n"; +} + void append_error_text(std::ostringstream &output, Interp &interp, const char *prefix, int rc) { if (rc > INTERP_MIN_ERROR) { @@ -317,6 +347,30 @@ char *lcinterp_save_parameters(const char *path, const char *assignments) return copy_result(output.str()); } +EMSCRIPTEN_KEEPALIVE +char *lcinterp_load_tool_table(const char *path) +{ + standalone::reset_tool_adapter(); + tooldata_init(false); + tooldata_set_db(DB_NOTUSED); + + std::ostringstream output; + const int rc = tooldata_load(path); + output << "tooldata_load=" << rc << "\n"; + append_tool_table_state(output); + return copy_result(output.str()); +} + +EMSCRIPTEN_KEEPALIVE +char *lcinterp_save_tool_table(const char *path) +{ + std::ostringstream output; + const int rc = tooldata_save(path); + output << "tooldata_save=" << rc << "\n"; + append_tool_table_state(output); + return copy_result(output.str()); +} + EMSCRIPTEN_KEEPALIVE void lcinterp_free_string(char *value) { diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tool_adapter.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tool_adapter.cpp index 7a0ec98..79c9e75 100644 --- a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tool_adapter.cpp +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tool_adapter.cpp @@ -2,6 +2,8 @@ #include +#include "tooldata.hh" + namespace { std::array &tool_table() @@ -22,6 +24,12 @@ int ¤t_tool_index_ref() return index; } +int &last_tool_index_ref() +{ + static int index = 0; + return index; +} + CANON_TOOL_TABLE empty_tool() { CANON_TOOL_TABLE tool{}; @@ -30,6 +38,20 @@ CANON_TOOL_TABLE empty_tool() return tool; } +CANON_TOOL_TABLE linuxcnc_empty_tool() +{ + CANON_TOOL_TABLE tool{}; + tool.toolno = -1; + tool.pocketno = -1; + tool.diameter = 0; + tool.frontangle = 0; + tool.backangle = 0; + tool.orientation = 0; + ZERO_EMC_POSE(tool.offset); + tool.comment[0] = 0; + return tool; +} + } // namespace namespace standalone { @@ -117,3 +139,63 @@ void change_selected_tool() } } // namespace standalone + +extern "C" { + +toolidx_t tooldata_put(CANON_TOOL_TABLE tdata, int idx) +{ + if ((idx < 0) || (idx >= CANON_POCKETS_MAX)) { + return IDX_FAIL; + } + + auto &tools = tool_table(); + const bool is_new = tools[idx].toolno == -1; + tools[idx] = tdata; + if (idx > last_tool_index_ref()) { + last_tool_index_ref() = idx; + } + return is_new ? IDX_NEW : IDX_OK; +} + +toolidx_t tooldata_get(CANON_TOOL_TABLE *pdata, int idx) +{ + if ((pdata == nullptr) || (idx < 0) || (idx >= CANON_POCKETS_MAX)) { + return IDX_FAIL; + } + + *pdata = tool_table()[idx]; + return IDX_OK; +} + +void tooldata_reset(void) +{ + auto &tools = tool_table(); + for (int idx = 0; idx < CANON_POCKETS_MAX; ++idx) { + tools[idx] = linuxcnc_empty_tool(); + } + selected_tool_index_ref() = -1; + current_tool_index_ref() = 0; + last_tool_index_ref() = 0; +} + +void tooldata_last_index_set(int idx) +{ + last_tool_index_ref() = idx; +} + +int tooldata_last_index_get(void) +{ + return last_tool_index_ref(); +} + +int tooldata_find_index_for_tool(int toolno) +{ + return standalone::find_tool_index_for_tool(toolno); +} + +int tooldata_db_getall(void) +{ + return -1; +} + +} // extern "C" diff --git a/wasm-port/runtime/core/shims/tooldata.hh b/wasm-port/runtime/core/shims/tooldata.hh new file mode 100644 index 0000000..a73db6a --- /dev/null +++ b/wasm-port/runtime/core/shims/tooldata.hh @@ -0,0 +1,50 @@ +#pragma once + +#include "emc/nml_intf/canon.hh" + +extern "C" { + +typedef enum { + IDX_OK = 0, + IDX_NEW, + IDX_FAIL, +} toolidx_t; + +typedef enum { + DB_NOTUSED = 0, + DB_ACTIVE, +} tooldb_t; + +typedef enum { + SPINDLE_LOAD, + SPINDLE_UNLOAD, + TOOL_OFFSET, +} tool_notify_t; + +struct CANON_TOOL_TABLE tooldata_entry_init(void); +toolidx_t tooldata_put(struct CANON_TOOL_TABLE tdata, int idx); +toolidx_t tooldata_get(CANON_TOOL_TABLE *pdata, int idx); + +void tooldata_init(bool random_tool_changer); +void tooldata_reset(void); +void tooldata_last_index_set(int idx); +int tooldata_last_index_get(void); +int tooldata_find_index_for_tool(int toolno); + +void tooldata_format_toolline(int idx, + bool ignore_zero_values, + CANON_TOOL_TABLE tdata, + char formatted_line[CANON_TOOL_ENTRY_LEN]); + +void tooldata_add_init(int nonrandom_start_idx); +int tooldata_read_entry(const char *input_line); + +void tooldata_set_db(tooldb_t mode); +int tooldata_load(const char *filename); +int tooldata_save(const char *filename); + +#define DB_SPINDLE_SAVE "./db_spindle.tbl" + +int tooldata_db_getall(void); + +} diff --git a/wasm-port/runtime/core/shims/tooldata/tooldata.hh b/wasm-port/runtime/core/shims/tooldata/tooldata.hh index b857b01..3d18f78 100644 --- a/wasm-port/runtime/core/shims/tooldata/tooldata.hh +++ b/wasm-port/runtime/core/shims/tooldata/tooldata.hh @@ -1,23 +1,3 @@ #pragma once -#include "emc/nml_intf/emctool.h" -#include "linuxcnc_tool_adapter.hh" - -enum { - IDX_OK = 0, -}; - -inline CANON_TOOL_TABLE tooldata_entry_init() -{ - return standalone::tool_entry_init(); -} - -inline int tooldata_find_index_for_tool(int toolno) -{ - return standalone::find_tool_index_for_tool(toolno); -} - -inline int tooldata_get(CANON_TOOL_TABLE *tool, int index) -{ - return standalone::get_tool_entry(tool, index) == 0 ? IDX_OK : -1; -} +#include "../tooldata.hh" diff --git a/wasm-port/runtime/opfs/linuxcnc-machine-session-bridge.js b/wasm-port/runtime/opfs/linuxcnc-machine-session-bridge.js new file mode 100644 index 0000000..fc876d9 --- /dev/null +++ b/wasm-port/runtime/opfs/linuxcnc-machine-session-bridge.js @@ -0,0 +1,59 @@ +import { loadTextFile } from "./file-service.js"; +import { + machineIniPath, + parameterFilePath, + toolTablePath, +} from "./path-model.js"; +import { restoreMachineParametersFromOpfs } from "./linuxcnc-parameter-bridge.js"; +import { loadMachineToolTableFromOpfs } from "./linuxcnc-tool-table-bridge.js"; + +const DEFAULT_WASM_INI_PATH = "/work/machine.ini"; +const DEFAULT_WASM_PARAMETER_PATH = "/work/linuxcnc.var"; +const DEFAULT_WASM_TOOL_TABLE_PATH = "/work/tool.tbl"; + +function requireSessionSdk(interp) { + if (typeof interp?.writeTextFile !== "function") { + throw new Error("interpreter SDK is missing writeTextFile()."); + } +} + +function resolveSessionPaths(machineId, options = {}) { + return { + iniOpfsPath: options.iniOpfsPath ?? machineIniPath(machineId, options.iniFilename), + iniWasmPath: options.iniWasmPath ?? DEFAULT_WASM_INI_PATH, + parameterOpfsPath: + options.parameterOpfsPath ?? parameterFilePath(machineId, options.parameterFilename), + parameterWasmPath: options.parameterWasmPath ?? DEFAULT_WASM_PARAMETER_PATH, + toolTableOpfsPath: + options.toolTableOpfsPath ?? toolTablePath(machineId, options.toolTableFilename), + toolTableWasmPath: options.toolTableWasmPath ?? DEFAULT_WASM_TOOL_TABLE_PATH, + }; +} + +export async function loadMachineSessionFromOpfs(interp, machineId, options = {}) { + requireSessionSdk(interp); + const paths = resolveSessionPaths(machineId, options); + const iniText = await loadTextFile(paths.iniOpfsPath, options.storage); + interp.writeTextFile(paths.iniWasmPath, iniText); + + const parameters = await restoreMachineParametersFromOpfs(interp, machineId, { + storage: options.storage, + opfsPath: paths.parameterOpfsPath, + wasmPath: paths.parameterWasmPath, + }); + const toolTable = await loadMachineToolTableFromOpfs(interp, machineId, { + storage: options.storage, + opfsPath: paths.toolTableOpfsPath, + wasmPath: paths.toolTableWasmPath, + }); + + return { + machineId, + ini: { + opfsPath: paths.iniOpfsPath, + wasmPath: paths.iniWasmPath, + }, + parameters, + toolTable, + }; +} diff --git a/wasm-port/runtime/opfs/linuxcnc-tool-table-bridge.js b/wasm-port/runtime/opfs/linuxcnc-tool-table-bridge.js new file mode 100644 index 0000000..99ccb74 --- /dev/null +++ b/wasm-port/runtime/opfs/linuxcnc-tool-table-bridge.js @@ -0,0 +1,46 @@ +import { loadTextFile, saveTextFile } from "./file-service.js"; +import { toolTablePath } from "./path-model.js"; + +const DEFAULT_WASM_TOOL_TABLE_PATH = "/work/tool.tbl"; + +function requireToolTableSdk(interp) { + for (const method of ["writeTextFile", "readTextFile", "loadToolTable", "saveToolTable"]) { + if (typeof interp?.[method] !== "function") { + throw new Error(`interpreter SDK is missing ${method}().`); + } + } +} + +function resolvePaths(machineId, options = {}) { + const opfsPath = options.opfsPath ?? toolTablePath(machineId, options.filename); + const wasmPath = options.wasmPath ?? DEFAULT_WASM_TOOL_TABLE_PATH; + return { opfsPath, wasmPath }; +} + +export async function loadMachineToolTableFromOpfs(interp, machineId, options = {}) { + requireToolTableSdk(interp); + const { opfsPath, wasmPath } = resolvePaths(machineId, options); + const text = await loadTextFile(opfsPath, options.storage); + interp.writeTextFile(wasmPath, text); + return { + opfsPath, + wasmPath, + result: interp.loadToolTable(wasmPath), + }; +} + +export async function saveMachineToolTableToOpfs(interp, machineId, options = {}) { + requireToolTableSdk(interp); + const { opfsPath, wasmPath } = resolvePaths(machineId, options); + const result = interp.saveToolTable(wasmPath); + const savedText = interp.readTextFile(wasmPath); + + await saveTextFile(opfsPath, savedText, options.storage); + + return { + opfsPath, + wasmPath, + result, + savedText, + }; +} diff --git a/wasm-port/runtime/sdk/README.md b/wasm-port/runtime/sdk/README.md index 6e5df5e..15a1370 100644 --- a/wasm-port/runtime/sdk/README.md +++ b/wasm-port/runtime/sdk/README.md @@ -40,5 +40,7 @@ vendored LinuxCNC RS274NGC sources and exposes: - `runFileWithIni(path, iniPath)` - `restoreParameters(path)` - `saveParameters(path, values)` +- `loadToolTable(path)` +- `saveToolTable(path)` - `writeTextFile(path, text)` - `readTextFile(path)` diff --git a/wasm-port/runtime/sdk/src/linuxcnc-interp.js b/wasm-port/runtime/sdk/src/linuxcnc-interp.js index d4c6ed3..1844a79 100644 --- a/wasm-port/runtime/sdk/src/linuxcnc-interp.js +++ b/wasm-port/runtime/sdk/src/linuxcnc-interp.js @@ -84,5 +84,13 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) { .join("\n"); return callStringResult(mod, "lcinterp_save_parameters", path, assignments); }, + + loadToolTable(path) { + return callStringResult(mod, "lcinterp_load_tool_table", path); + }, + + saveToolTable(path) { + return callStringResult(mod, "lcinterp_save_tool_table", path); + }, }; } diff --git a/wasm-port/runtime/ui/ini-panel/app.js b/wasm-port/runtime/ui/ini-panel/app.js index 695654b..42a87f9 100644 --- a/wasm-port/runtime/ui/ini-panel/app.js +++ b/wasm-port/runtime/ui/ini-panel/app.js @@ -1,18 +1,58 @@ -import { createLinuxCncIniSdk } from "../../sdk/src/index.js"; +import { + createLinuxCncIniSdk, + createLinuxCncInterpSdk, +} from "../../sdk/src/index.js"; import { getOpfsRoot, loadTextFile, saveTextFile, } from "../../opfs/file-service.js"; +import { + loadGcodeProgram, + loadMachineTextFiles, + machineFilePaths, + saveGcodeProgram, + saveMachineTextFiles, +} from "../../opfs/machine-file-store.js"; +import { + loadMachineSessionFromOpfs, +} from "../../opfs/linuxcnc-machine-session-bridge.js"; const SAMPLE_PATH = "../../../linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini"; -const OPFS_FILE = "linuxcnc/xyzab-tdr.ini"; +const MACHINE_ID = "xyzab-tdr"; +const MACHINE_PATHS = machineFilePaths(MACHINE_ID); +const OPFS_FILE = MACHINE_PATHS.ini; const WASM_FILE = "/work/xyzab-tdr.ini"; +const SESSION_WASM_FILES = { + iniWasmPath: "/work/session-machine.ini", + parameterWasmPath: "/work/session-linuxcnc.var", + toolTableWasmPath: "/work/session-tool.tbl", +}; +const GCODE_FILENAME = "ui-session.ngc"; +const GCODE_WASM_FILE = "/work/ui-session.ngc"; +const DEFAULT_TOOL_TABLE = "T2 P7 Z3.125 D1.5 I12 J34 Q4 ;ui session tool\n"; +const DEFAULT_PARAMETERS = [ + "5161 0.0", + "5162 0.0", + "5220 1", + "5221 0.0", + "5399 0.0", + "", +].join("\n"); +const DEFAULT_GCODE = [ + "G0 X1.0 Y2.0 (Comment)", + "G1 X3.0 Y4.0 F120.0", + "", +].join("\n"); const editor = document.getElementById("ini-editor"); const logNode = document.getElementById("log"); +const eventNode = document.getElementById("canon-events"); +const eventFilterNode = document.getElementById("event-filter"); +const eventCountNode = document.getElementById("event-count"); const wasmBadge = document.getElementById("wasm-badge"); +const interpBadge = document.getElementById("interp-badge"); const opfsBadge = document.getElementById("opfs-badge"); const fields = { @@ -22,9 +62,17 @@ const fields = { joints: document.getElementById("field-joints"), coordinates: document.getElementById("field-coordinates"), parameterFile: document.getElementById("field-parameter-file"), + sessionIni: document.getElementById("field-session-ini"), + sessionParameters: document.getElementById("field-session-parameters"), + sessionToolTable: document.getElementById("field-session-tool-table"), + sessionGcode: document.getElementById("field-session-gcode"), + runStatus: document.getElementById("field-run-status"), }; let iniSdk = null; +let interpSdk = null; +let loadedSession = null; +let canonicalEventText = ""; function setBadge(node, text, className = "badge") { node.className = className; @@ -36,17 +84,40 @@ function setLog(message, isError = false) { logNode.className = `log${isError ? " danger" : ""}`; } +function setCanonicalEvents(text) { + canonicalEventText = text || ""; + renderCanonicalEvents(); +} + +function renderCanonicalEvents() { + const lines = canonicalEventText.split("\n").filter(Boolean); + const filter = eventFilterNode.value.trim().toLowerCase(); + const visibleLines = filter + ? lines.filter((line) => line.toLowerCase().includes(filter)) + : lines; + + eventNode.textContent = visibleLines.join("\n"); + eventCountNode.textContent = `${visibleLines.length} / ${lines.length}`; +} + function setField(name, value) { fields[name].textContent = value ?? "-"; } function requireIniSdk() { if (!iniSdk) { - throw new Error("WASM module is not ready yet."); + throw new Error("INI WASM module is not ready yet."); } return iniSdk; } +function requireInterpSdk() { + if (!interpSdk) { + throw new Error("Interpreter WASM module is not ready yet."); + } + return interpSdk; +} + function syncEditorToWasmFs() { requireIniSdk().writeTextFile(WASM_FILE, editor.value); } @@ -64,10 +135,19 @@ async function loadSample() { async function boot() { try { iniSdk = await createLinuxCncIniSdk(); - setBadge(wasmBadge, "WASM: ready"); + setBadge(wasmBadge, "INI WASM: ready"); } catch (error) { - setBadge(wasmBadge, "WASM: failed", "badge danger"); - setLog(`WASM init failed: ${error.message}`, true); + setBadge(wasmBadge, "INI WASM: failed", "badge danger"); + setLog(`INI WASM init failed: ${error.message}`, true); + throw error; + } + + try { + interpSdk = await createLinuxCncInterpSdk(); + setBadge(interpBadge, "Interpreter WASM: ready"); + } catch (error) { + setBadge(interpBadge, "Interpreter WASM: failed", "badge danger"); + setLog(`Interpreter WASM init failed: ${error.message}`, true); throw error; } @@ -97,6 +177,78 @@ document.getElementById("save-opfs").addEventListener("click", async () => { } }); +document.getElementById("save-machine-files").addEventListener("click", async () => { + try { + const paths = await saveMachineTextFiles(MACHINE_ID, { + ini: editor.value, + toolTable: DEFAULT_TOOL_TABLE, + parameters: DEFAULT_PARAMETERS, + }); + const gcodePath = await saveGcodeProgram(GCODE_FILENAME, DEFAULT_GCODE); + setLog( + [ + "Saved machine text files to OPFS.", + `INI: ${paths.ini}`, + `Tool table: ${paths.toolTable}`, + `Parameters: ${paths.parameters}`, + `G-code: ${gcodePath}`, + ].join("\n"), + ); + } catch (error) { + setLog(`Machine file save failed: ${error.message}`, true); + } +}); + +document.getElementById("load-session").addEventListener("click", async () => { + try { + loadedSession = await loadMachineSessionFromOpfs( + requireInterpSdk(), + MACHINE_ID, + SESSION_WASM_FILES, + ); + setField("sessionIni", loadedSession.ini.wasmPath); + setField("sessionParameters", loadedSession.parameters.wasmPath); + setField("sessionToolTable", loadedSession.toolTable.wasmPath); + setLog( + [ + "Loaded machine session into the interpreter WASM filesystem.", + `INI: ${loadedSession.ini.opfsPath} -> ${loadedSession.ini.wasmPath}`, + `Parameters: ${loadedSession.parameters.result.trim()}`, + `Tool table: ${loadedSession.toolTable.result.trim()}`, + ].join("\n"), + ); + } catch (error) { + setLog(`Machine session load failed: ${error.message}`, true); + } +}); + +document.getElementById("run-gcode").addEventListener("click", async () => { + try { + if (!loadedSession) { + throw new Error("Load a machine session before running G-code."); + } + const interp = requireInterpSdk(); + const programText = await loadGcodeProgram(GCODE_FILENAME); + interp.writeTextFile(GCODE_WASM_FILE, programText); + const result = interp.runFileWithIni(GCODE_WASM_FILE, loadedSession.ini.wasmPath); + setField("sessionGcode", GCODE_WASM_FILE); + setField("runStatus", "ok"); + setCanonicalEvents(result.trim()); + setLog( + [ + "Ran G-code through LinuxCNC interpreter WASM.", + `Program: ${GCODE_WASM_FILE}`, + `INI: ${loadedSession.ini.wasmPath}`, + result.trim(), + ].join("\n"), + ); + } catch (error) { + setField("runStatus", "failed"); + setCanonicalEvents(""); + setLog(`G-code run failed: ${error.message}`, true); + } +}); + document.getElementById("load-opfs").addEventListener("click", async () => { try { editor.value = await loadTextFile(OPFS_FILE); @@ -137,4 +289,25 @@ document.getElementById("query").addEventListener("click", async () => { } }); +document.getElementById("load-machine-files").addEventListener("click", async () => { + try { + const files = await loadMachineTextFiles(MACHINE_ID); + editor.value = files.ini; + setLog( + [ + "Loaded machine text files from OPFS.", + `INI bytes: ${files.ini.length}`, + `Tool table bytes: ${files.toolTable.length}`, + `Parameter bytes: ${files.parameters.length}`, + ].join("\n"), + ); + } catch (error) { + setLog(`Machine file load failed: ${error.message}`, true); + } +}); + +eventFilterNode.addEventListener("input", () => { + renderCanonicalEvents(); +}); + boot().catch(() => {}); diff --git a/wasm-port/runtime/ui/ini-panel/index.html b/wasm-port/runtime/ui/ini-panel/index.html index 47de24d..18f67d8 100644 --- a/wasm-port/runtime/ui/ini-panel/index.html +++ b/wasm-port/runtime/ui/ini-panel/index.html @@ -123,6 +123,32 @@ font: 13px/1.5 "SFMono-Regular", "Consolas", monospace; white-space: pre-wrap; } + .events { + border: 1px solid var(--border); + border-radius: 6px; + background: #fbfdff; + padding: 12px; + min-height: 180px; + max-height: 320px; + overflow: auto; + font: 12px/1.5 "SFMono-Regular", "Consolas", monospace; + white-space: pre-wrap; + } + .event-tools { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; + } + input { + border: 1px solid var(--border); + border-radius: 6px; + padding: 9px 10px; + font: 13px/1.4 "SFMono-Regular", "Consolas", monospace; + color: var(--text); + background: #fbfdff; + min-width: 0; + } .danger { color: var(--danger); } @media (max-width: 960px) { .grid { grid-template-columns: 1fr; } @@ -148,8 +174,12 @@ + + + + @@ -157,7 +187,8 @@

Control Panel

-
WASM: loading
+
INI WASM: loading
+
Interpreter WASM: loading
OPFS: checking
@@ -175,12 +206,29 @@
Parameter File
-
OPFS Path
-
linuxcnc/xyzab-tdr.ini
+
linuxcnc/machines/xyzab-tdr/machine.ini
WASM Path
/work/xyzab-tdr.ini
+
Session INI
+
-
+
Session Params
+
-
+
Session Tools
+
-
+
Session G-code
+
-
+
Run Status
+
-
+ +

Canonical Events

+
+ + 0 / 0 +
+

       
diff --git a/wasm-port/tests/browser/ini_panel_smoke.html b/wasm-port/tests/browser/ini_panel_smoke.html index f490c21..d17a1ac 100644 --- a/wasm-port/tests/browser/ini_panel_smoke.html +++ b/wasm-port/tests/browser/ini_panel_smoke.html @@ -29,6 +29,33 @@ } } + function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async function waitFor(predicate, label) { + const deadline = performance.now() + 8000; + while (performance.now() < deadline) { + const value = predicate(); + if (value) { + return value; + } + await delay(50); + } + throw new Error(`Timed out waiting for ${label}`); + } + + async function loadUiFrame() { + const frame = document.createElement("iframe"); + frame.src = "../../runtime/ui/ini-panel/index.html"; + document.body.appendChild(frame); + await new Promise((resolve, reject) => { + frame.addEventListener("load", resolve, { once: true }); + frame.addEventListener("error", reject, { once: true }); + }); + return frame.contentDocument; + } + try { const ini = await createLinuxCncIniSdk(); const wasmPath = "/work/browser-smoke.ini"; @@ -90,6 +117,95 @@ JOINTS = 3 await saveGcodeProgram("browser-smoke.ngc", "G0 X0 Y0\nM2\n"); assertEqual(await loadGcodeProgram("browser-smoke.ngc"), "G0 X0 Y0\nM2\n", "gcode text"); + const uiDocument = await loadUiFrame(); + await waitFor( + () => uiDocument.getElementById("wasm-badge")?.textContent.includes("ready"), + "INI panel INI WASM readiness", + ); + await waitFor( + () => uiDocument.getElementById("interp-badge")?.textContent.includes("ready"), + "INI panel interpreter WASM readiness", + ); + await waitFor( + () => uiDocument.getElementById("opfs-badge")?.textContent.includes("ready"), + "INI panel OPFS readiness", + ); + + uiDocument.getElementById("ini-editor").value = iniText; + uiDocument.getElementById("save-machine-files").click(); + await waitFor( + () => uiDocument.getElementById("log")?.textContent.includes("Saved machine text files"), + "machine file save", + ); + uiDocument.getElementById("load-session").click(); + await waitFor( + () => uiDocument.getElementById("log")?.textContent.includes("Loaded machine session"), + "machine session load", + ); + assertEqual( + uiDocument.getElementById("field-session-ini").textContent, + "/work/session-machine.ini", + "UI session INI path", + ); + assertEqual( + uiDocument.getElementById("field-session-parameters").textContent, + "/work/session-linuxcnc.var", + "UI session parameter path", + ); + assertEqual( + uiDocument.getElementById("field-session-tool-table").textContent, + "/work/session-tool.tbl", + "UI session tool table path", + ); + const uiLog = uiDocument.getElementById("log").textContent; + if (!uiLog.includes("restore_parameters=0") || !uiLog.includes("tooldata_load=0")) { + throw new Error(`UI session log missing LinuxCNC load result: ${uiLog}`); + } + uiDocument.getElementById("run-gcode").click(); + await waitFor( + () => uiDocument.getElementById("log")?.textContent.includes("Ran G-code"), + "G-code run", + ); + assertEqual( + uiDocument.getElementById("field-session-gcode").textContent, + "/work/ui-session.ngc", + "UI session G-code path", + ); + assertEqual( + uiDocument.getElementById("field-run-status").textContent, + "ok", + "UI G-code run status", + ); + const runLog = uiDocument.getElementById("log").textContent; + if ( + !runLog.includes("canon_event=STRAIGHT_TRAVERSE") || + !runLog.includes("canon_event=STRAIGHT_FEED") + ) { + throw new Error(`UI G-code run missing LinuxCNC canonical events: ${runLog}`); + } + const eventText = uiDocument.getElementById("canon-events").textContent; + if ( + !eventText.includes("canon_event=STRAIGHT_TRAVERSE") || + !eventText.includes("canon_event=STRAIGHT_FEED") + ) { + throw new Error(`UI canonical event panel missing LinuxCNC events: ${eventText}`); + } + const eventFilter = uiDocument.getElementById("event-filter"); + eventFilter.value = "STRAIGHT_FEED"; + eventFilter.dispatchEvent(new Event("input", { bubbles: true })); + const filteredEventText = uiDocument.getElementById("canon-events").textContent; + if ( + filteredEventText.includes("canon_event=STRAIGHT_TRAVERSE") || + !filteredEventText.includes("canon_event=STRAIGHT_FEED") + ) { + throw new Error(`UI canonical event filter did not preserve raw LinuxCNC lines: ${filteredEventText}`); + } + assertEqual( + uiDocument.getElementById("event-count").textContent, + "1 / 4", + "UI canonical event count", + ); + status.textContent = "browser_ini_opfs_smoke=ok"; } catch (error) { status.textContent = `browser_ini_opfs_smoke=fail ${error.stack || error.message}`; diff --git a/wasm-port/tests/browser/interp_smoke.html b/wasm-port/tests/browser/interp_smoke.html index d393777..831afbe 100644 --- a/wasm-port/tests/browser/interp_smoke.html +++ b/wasm-port/tests/browser/interp_smoke.html @@ -9,9 +9,14 @@