Expand WASM sim-config staging coverage

This commit is contained in:
2026-06-09 07:32:15 +08:00
parent 2658ee3b72
commit 5116b9894d
26 changed files with 5989 additions and 141 deletions

1
.gitignore vendored
View File

@@ -2,4 +2,5 @@
/.codex/
/git.txt
/text.txt
/test-results/
/linuxcnc/

View File

@@ -72,6 +72,41 @@ The current aggregate host/WASM/browser smoke command is:
wasm-port/tests/host/verify_host_smokes.sh
```
## Validation Layers
Use these layers when deciding where a LinuxCNC asset belongs. Do not widen a
later layer until the narrower layer has either passed or recorded an explicit
expected boundary.
| Layer | Scope | Entry command | Current result | Expected boundary |
| --- | --- | --- | --- | --- |
| 1 | `linuxcnc/nc_files` basic G-code smoke | `wasm-port/tests/native/verify_nc_files.sh` | `total: 107`, `pass: 101`, `expected_fail: 6`, `unexpected_fail: 0` | LinuxCNC-native entry-point/context edges only: W-axis machine context, lathe tool/cutter-comp context, upstream O-word syntax edge, and probe runtime context. |
| 2 | `linuxcnc/configs/sim` native strict harness | `wasm-port/tests/native/verify_sim_configs.sh` | `total: 159`, `pass: 151`, `expected_fail: 8`, `unexpected_fail: 0` | Native `bin/rs274` cannot provide task/user-M process execution or some INI axis-mask/runtime context. The upstream `incremental_repetition_g533.ngc` demo remains an expected upstream demo edge and must not be made pass by changing G-code semantics. |
| 3 | Standalone native runtime probes | `wasm-port/tests/native/verify_native_probes.sh` | Passes with `native probes complete` after source sync, no-standalone-semantics, fixture baseline, sim-config, and `nc_files` checks | Runtime adapters may cover filesystem, HAL/user-M, remap, parameter, tool, kinematics, TP, and WASM/browser boundary behavior, but CNC semantics must still come from vendored LinuxCNC source. |
| 4 | WASM Node/browser representative smoke | `wasm-port/tests/host/verify_host_smokes.sh` plus targeted Node/browser commands below | Current targeted checks pass with `sim_configs_wasm_node_smoke=ok`, `nc_files_wasm_node_smoke=ok`, `browser_interp_smoke=ok`, `browser_ini_opfs_smoke=ok`, and aggregate `host_wasm_opfs_browser_smokes=ok` | JS/browser code may stage files, apply executable bits, persist OPFS text, and forward paths to C ABI calls. It must not implement G-code, remap, tool-table, parameter, planner, kinematics, or user-M semantics. |
Layer 4 is made of focused smoke commands so failures can be isolated before
running the aggregate host check:
```bash
wasm-port/tests/wasm/node/verify_ini_wasm.sh
wasm-port/tests/wasm/node/verify_interp_wasm.sh
wasm-port/tests/wasm/node/verify_sim_configs_wasm.sh
wasm-port/tests/wasm/node/verify_nc_files_wasm.sh
wasm-port/tests/wasm/node/verify_tp_wasm.sh
wasm-port/tests/opfs/node/verify_file_service.sh
wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tests/browser/verify_interp_browser.sh
wasm-port/tests/host/verify_host_smokes.sh
```
Layer 4 sim-config staging uses `planSimConfigStaging()` to collect
INI-declared `[DISPLAY]OPEN_FILE`, `[EMCIO]TOOL_TABLE`,
`[RS274NGC]PARAMETER_FILE`, `[RS274NGC]SUBROUTINE_PATH`,
`[RS274NGC]USER_M_PATH`, and remap-NGC files from the vendored source
manifest. That planner is a host filesystem helper only; execution still goes
through vendored LinuxCNC interpreter/remap/tool/parameter code.
## Validation Chain
The native validation script runs these checks in order:
@@ -424,13 +459,13 @@ 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 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 plus vendored upstream `tests/interp` regression files 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_sim_configs_wasm.sh` | Validates representative vendored LinuxCNC `configs/sim` programs in Node WASM through `runSimConfigProgram()`, copying `axis/foam`, `axis/geometry`, and `axis/external_offsets` INI/G-code/user-M assets into the Emscripten filesystem before forwarding to `runFileWithIni()`, plus bridge-mill `5axis.ini`, tool table, `remap_subs/*.ngc`, and `5axisgui.ngc` before forwarding to `runFiveAxisRemapFile()`. This covers INI-driven `U/V/W` axis mask handling, real `USER_M_PATH` registration for executable `M110`/`M111`, and vendored LinuxCNC bridge-mill NGC remap execution without spawning host processes. |
| `tests/wasm/node/verify_nc_files_wasm.sh` | Validates representative vendored LinuxCNC `nc_files` examples in Node WASM by copying `arcspiral.ngc`, `hole-circle.ngc`, `factorial.ngc`, and `m6demo.ngc` into the Emscripten filesystem and forwarding to the LinuxCNC-backed `Interp::open()`/`read()`/`execute()` path. JavaScript only stages files and checks LinuxCNC output; it does not implement G-code, O-word, tool-change, or M-code behavior. |
| `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 plus vendored upstream `tests/interp` regression files through `Interp::open()`/`read()`/`execute()`, match the native canonical event plus required state readback fixtures, emit `run_step` execution-status records with LinuxCNC line number, encoded source statement, return code, and interpreter axis positions for file execution, 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_sim_configs_wasm.sh` | Validates representative vendored LinuxCNC `configs/sim` programs in Node WASM through `planSimConfigStaging()` plus `runSimConfigProgram()`. The staging planner uses INI text and `tools/source-manifest.txt` to collect the program, INI, tool table, parameter file when vendored, `SUBROUTINE_PATH` files, `USER_M_PATH` files, and remap-NGC files before forwarding to `runFileWithIni()` or `runFiveAxisRemapFile()`. This covers INI-driven `U/V/W` axis mask handling, real `USER_M_PATH` registration for executable `M110`/`M111`, all four current external-offset M111 expected-failure programs, `SUBROUTINE_PATH` staging for `opa_demo.ngc -> circles.ngc`, bridge-mill remap-subroutine staging, and vendored LinuxCNC bridge-mill NGC remap execution without spawning host processes. The same smoke includes a synthetic staging-plan assertion for `TOOL_TABLE`, `PARAMETER_FILE`, `SUBROUTINE_PATH`, and `USER_M_PATH` collection. |
| `tests/wasm/node/verify_nc_files_wasm.sh` | Validates representative vendored LinuxCNC `nc_files` examples in Node WASM by copying `3D_Chips.ngc`, `arcspiral.ngc`, `hole-circle.ngc`, `factorial.ngc`, and `m6demo.ngc` into the Emscripten filesystem and forwarding to the LinuxCNC-backed `Interp::open()`/`read()`/`execute()` path. `3D_Chips.ngc` is staged with a minimal INI-declared `tool.tbl` because the upstream program contains `T1 M6`; JavaScript only stages files and checks LinuxCNC output, including `run_step` status records, and does not implement G-code, O-word, tool-change, or M-code behavior. |
| `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, canonical-event display paths, and 5-axis remap demo action 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 plus vendored upstream `tests/interp` regression files through vendored LinuxCNC `Interp::execute()` and `Interp::open()`/`read()`/`execute()` via the exported C ABI, including direct browser SDK and OPFS-backed parameter-file restore/save, non-random/random tool-table load/save through vendored LinuxCNC source, vendored `xyzac-trt`/`xyzbc-trt` switchkins remap demo execution, representative vendored `configs/sim` `foam`, `geometry`, `external_offsets`, and bridge-mill programs through `runSimConfigProgram()`, and representative vendored `nc_files` examples through `runFile()`. |
| `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, `run_step`-backed progress/line/statement/axis display, canonical-event display paths, and 5-axis remap demo action 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 plus vendored upstream `tests/interp` regression files through vendored LinuxCNC `Interp::execute()` and `Interp::open()`/`read()`/`execute()` via the exported C ABI, including direct browser SDK and OPFS-backed parameter-file restore/save, non-random/random tool-table load/save through vendored LinuxCNC source, vendored `xyzac-trt`/`xyzbc-trt` switchkins remap demo execution, representative vendored `configs/sim` `foam`, `geometry`, `external_offsets`, and bridge-mill programs through `runSimConfigProgram()`, and representative vendored `nc_files` examples through `runFile()` or `runFileWithIni()` when the upstream file requires INI/tool-table context. |
| `tests/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM interpreter-core, WASM trajectory-planner, OPFS, and browser smoke validation with shared WASM builds. |
## Fixture Coverage
@@ -752,17 +787,21 @@ canned-cycle, state-tag motion, tool-table setup, and O-word subroutine
fixtures, plus threading/rigid tap, NURBS dispatch boundaries, and the
comparable canonical runtime edge and program-end cleanup calls. The
standalone vendored-source harness, Node WASM smoke, and browser smoke also
pin `coordinate_offsets` MDI/file-path, `feed_control_modes`, the
pin `coordinate_offsets` MDI/file-path, `feed_control_modes`, `run_step`
file-execution status output for line number, URI-encoded source statement,
return code, and current interpreter axis positions, the
`position_params` MDI/file-path, `tool_semantics`, and `canon_runtime_edges`
post-program modal, override, spindle, mist, and flood `_setup` state. Native
and WASM standalone checks also cover the user M-code registration boundary for
INI-declared `M110`/`M111` handlers while keeping process execution outside the
browser/WASM runtime. Node and browser WASM sim-config coverage now run the real
vendored `axis/geometry/xyzc.ngc` and
`axis/external_offsets/dyn_demo.ngc` programs with their executable
`M110`/`M111` files, `axis/foam/foam.ngc` with its `U/V` machine
configuration, and bridge-mill `5axisgui.ngc` with its `W` machine
coordinates. The same upstream baseline also
vendored `axis/geometry/xyzc.ngc` and the
`axis/external_offsets/dyn_demo.ngc`, `eoffsets.ngc`, `jwp_z.ngc`, and
`opa_demo.ngc` programs with their executable `M110`/`M111` files,
`opa_demo.ngc`'s `SUBROUTINE_PATH` dependency on `circles.ngc`,
`axis/foam/foam.ngc` with its `U/V` machine configuration and shared
`axis/sim.tbl` tool table staged beside the INI, and
bridge-mill `5axisgui.ngc` with its `W` machine coordinates. The same upstream baseline also
validates the current negative fixture error text and absent canonical-event
constraints. Fixtures that depend on standalone-only runtime
adapters, HAL state, upstream `rs274` output gaps such as

View File

@@ -45,7 +45,7 @@ semantic rewrites:
| 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`, `[RS274NGC]PARAMETER_FILE`, and `[EMCIO]TOOL_TABLE` when available; explicit host session file-name options take precedence over INI-derived file names, missing INI file-name values fall back to host default parameter/tool-table paths, and path validation remains owned by the OPFS path model, including rejection of traversal or nested path segments from INI-derived file names. |
| Sim-config SDK staging | `runtime/sdk/src/linuxcnc-interp.js` exposes `runSimConfigProgram()` as a host-boundary convenience only. It writes caller-provided text files into the Emscripten filesystem, applies executable bits for user M-code files, and forwards to existing LinuxCNC-backed C ABI entry points such as `runFileWithIni()` or `runFiveAxisRemapFile()`; it does not implement interpreter, axis, remap, or user M-code semantics. |
| Sim-config SDK staging | `runtime/sdk/src/sim-config-staging.js` exposes `planSimConfigStaging()` as a host-boundary file planner for representative vendored `configs/sim` programs. It reads INI text plus `tools/source-manifest.txt` to collect `[DISPLAY]OPEN_FILE`, `[EMCIO]TOOL_TABLE`, `[RS274NGC]PARAMETER_FILE`, `[RS274NGC]SUBROUTINE_PATH`, `[RS274NGC]USER_M_PATH`, and remap-NGC files for staging. `runtime/sdk/src/linuxcnc-interp.js` exposes `runSimConfigProgram()` as the execution convenience only: it writes planned text files into the Emscripten filesystem, applies executable bits for user M-code files, and forwards to existing LinuxCNC-backed C ABI entry points such as `runFileWithIni()` or `runFiveAxisRemapFile()`; neither helper implements interpreter, axis, remap, tool, parameter, or user M-code semantics. |
## Enforced Non-Drift Rules

View File

@@ -48,19 +48,20 @@ wasm-port/tests/host/verify_host_smokes.sh
| Reject standalone CNC semantics | Done | `tools/verify_no_standalone_cnc_semantics.sh` guards project-owned `Interp::...` definitions outside documented runtime-edge stubs. |
| Maintain source reuse map | Done | Current sim-config, remap, interpreter, kinematics, TP, user-M, tool, named-parameter, and OPFS runtime boundaries are recorded in `docs/source-reuse-map.md`; keep updating it whenever LinuxCNC source, test assets, or runtime boundaries are added. |
| Maintain compatibility validation notes | Done | Current native, Node WASM, browser, OPFS, and host aggregate checks are recorded in `docs/compatibility-validation.md`; keep updating it whenever a validation command or fixture class is added. |
| Document validation layers | Done | `docs/compatibility-validation.md` now records Layer 1 `nc_files` basic smoke, Layer 2 `configs/sim` native strict harness, Layer 3 standalone native runtime probes, and Layer 4 WASM Node/browser representative smokes with entry commands, current results, expected boundaries, and the no-LinuxCNC-semantics rule. |
| Build LinuxCNC test inventory | In progress | `linuxcnc/tests/remap` has been inventoried by dependency class; the first broader `linuxcnc/tests` pass now records full-process, HAL/realtime, linuxcncrsh, Python-binding, and external-component families in the blocked table. |
| Prefer pure interpreter tests first | In progress | Continue with tests that can run through `Interp::open()`, `read()`, and `execute()` without realtime hardware or GUI. |
| Add upstream-output comparison | In progress | Use upstream `../linuxcnc/bin/rs274` or original `test.sh` output where practical, then compare against native/WASM/browser output. |
| Promote tests through three layers | In progress | Add each selected test first to native harnesses, then WASM Node, then browser smoke where meaningful. |
| Record blocked tests | In progress | First broader family-level blocked batch is recorded below; continue expanding it as each `linuxcnc/tests` family is inspected. |
| Add aggregate smoke coverage | Done | `tests/host/verify_host_smokes.sh` now runs stable Node WASM, sim-config WASM, `nc_files` WASM, OPFS, and browser smoke checks with shared builds. Current run passed with `host_wasm_opfs_browser_smokes=ok`. |
| Add `linuxcnc/nc_files` basic examples | Done | `tests/native/verify_nc_files.sh` now runs the basic/example subset of upstream `linuxcnc/nc_files` through LinuxCNC `bin/rs274`; current native baseline is 107 total, 101 pass, 6 expected context/entry-point edges, 0 unexpected failures. Exploratory `--all` inventory is 247 total, 219 pass, 28 expected context/entry-point edges, 0 unexpected failures. A representative vendored subset now passes Node WASM and browser execution through LinuxCNC-backed `Interp::open()`/`read()`/`execute()`. |
| Add `linuxcnc/nc_files` basic examples | Done | `tests/native/verify_nc_files.sh` now runs the basic/example subset of upstream `linuxcnc/nc_files` through LinuxCNC `bin/rs274`; current native baseline is 107 total, 101 pass, 6 expected context/entry-point edges, 0 unexpected failures. Exploratory `--all` inventory is 247 total, 219 pass, 28 expected context/entry-point edges, 0 unexpected failures. A representative vendored subset now passes Node WASM and browser execution through LinuxCNC-backed `Interp::open()`/`read()`/`execute()`, with `3D_Chips.ngc` using an INI-declared tool-table context for its upstream `T1 M6` line. |
## LinuxCNC Test Asset Queue
| LinuxCNC test path | Status | Current target | Notes |
| --- | --- | --- | --- |
| `nc_files` basic/example programs | Done | Native, WASM Node, browser | Native coverage is tracked by `wasm-port/tests/native/verify_nc_files.sh`; complete programs execute strictly, macro/library files are wrapped with temporary `M2` for load/parse validation, and six known LinuxCNC-native context edges are classified explicitly. `--all` also inventories probe/plasmac/NURBS files and currently classifies all 28 non-passing files as expected runtime-context or entry-point edges. The representative vendored WASM/browser subset covers `arcspiral.ngc`, `hole-circle.ngc`, `factorial.ngc`, and `m6demo.ngc` through LinuxCNC-backed file execution only. |
| `nc_files` basic/example programs | Done | Native, WASM Node, browser | Native coverage is tracked by `wasm-port/tests/native/verify_nc_files.sh`; complete programs execute strictly, macro/library files are wrapped with temporary `M2` for load/parse validation, and six known LinuxCNC-native context edges are classified explicitly. `--all` also inventories probe/plasmac/NURBS files and currently classifies all 28 non-passing files as expected runtime-context or entry-point edges. The representative vendored WASM/browser subset covers `3D_Chips.ngc`, `arcspiral.ngc`, `hole-circle.ngc`, `factorial.ngc`, and `m6demo.ngc` through LinuxCNC-backed file execution, with `3D_Chips.ngc` staged through `runFileWithIni()` and an INI-declared `tool.tbl` instead of being judged without its required tool context. |
| `tests/remap/duplicate-o-word` | Done | Native, WASM Node, browser | Vendored under `wasm-port/vendor/linuxcnc/tests/remap/duplicate-o-word`; validates duplicate O-word behavior through vendored LinuxCNC remap/O-word/file execution paths. |
| `tests/remap/fail/args.0` | Done | Native, WASM Node, browser | Vendored under `wasm-port/vendor/linuxcnc/tests/remap/fail/args.0`; validates required `P`/`Q` remap arguments when both are present through vendored LinuxCNC remap argument handling. |
| `tests/remap/fail/args.1` | Done | Native, WASM Node, browser | Vendored under `wasm-port/vendor/linuxcnc/tests/remap/fail/args.1`; validates LinuxCNC missing-`Q` remap error text and upstream-style continue-after-error flow. |
@@ -92,7 +93,7 @@ wasm-port/tests/host/verify_host_smokes.sh
| `tests/interp/abort-hot-comment` | Done | Native, WASM Node, browser | Vendored under `wasm-port/vendor/linuxcnc/tests/interp/abort-hot-comment`; validates LinuxCNC `(ABORT,...)` hot-comment parameter expansion for numbered, named, and INI named parameters through vendored file execution paths and `INI_FILE_NAME`. |
| `configs/sim/axis/foam/foam.ngc` | Done | Native, WASM Node, browser | Vendored under `wasm-port/vendor/linuxcnc/configs/sim/axis/foam`; validates LinuxCNC INI-driven `U/V` axis mask handling through `axis_foam.ini` and vendored file execution paths. |
| `configs/sim/axis/geometry/xyzc.ngc` | Done | Native, WASM Node, browser | Vendored under `wasm-port/vendor/linuxcnc/configs/sim/axis/geometry`; validates LinuxCNC `USER_M_PATH` registration with the real executable `M110` while process execution remains a deterministic standalone `USER_M_COMMAND` boundary. |
| `configs/sim/axis/external_offsets/dyn_demo.ngc` | Done | Native, WASM Node, browser | Vendored under `wasm-port/vendor/linuxcnc/configs/sim/axis/external_offsets`; validates LinuxCNC `USER_M_PATH` registration with the real executable `M111` against an upstream sim program while process execution remains a deterministic standalone boundary. |
| `configs/sim/axis/external_offsets/*.ngc` M111 demos | Done | Native, WASM Node, browser | Vendored under `wasm-port/vendor/linuxcnc/configs/sim/axis/external_offsets`; validates `dyn_demo.ngc`, `eoffsets.ngc`, `jwp_z.ngc`, and `opa_demo.ngc` with their corresponding INI files, real executable `M111`, shared `eoffset.tbl`, and `opa_demo.ngc`'s `circles.ngc` subroutine while process execution remains a deterministic standalone boundary. |
| More `tests/remap/*` interpreter-only cases | Planned | Native first | Select cases that do not require Python behavior beyond documented runtime-edge stubs unless the required LinuxCNC source path is ported. |
| More `tests/interp/*` interpreter-only cases | Planned | Native first | Select upstream interpreter cases that can run through vendored file execution without Python, HAL, realtime motion, or GUI. |
| `tests/rs274ngc/*` candidates | Planned | Native first | Use upstream `rs274` as the baseline where output is comparable. |

View File

@@ -36,6 +36,9 @@ remap_subroutine: PASS 65, FAIL 0
Remaining failures are explicit LinuxCNC-native baseline edges:
- `axis/external_offsets/*`: 4 failures from sim-only user M-codes `M111`.
The native harness now resolves the corresponding INI by
`[DISPLAY]OPEN_FILE`, so `eoffsets.ngc`, `jwp_z.ngc`, and `opa_demo.ngc`
are no longer judged through `dynamic_offsets.ini`.
- `axis/geometry/xyzc.ngc`: 1 failure from sim-only user M-code `M110`.
- `axis/foam/foam.ngc`: 1 failure from missing `U/V` axis support in the
LinuxCNC `bin/rs274` entry point.
@@ -392,9 +395,11 @@ ABI paths. They cover:
WASM.
- `axis/geometry/xyzc.ngc` with `xyzc.ini` and real executable `M110`, verifying
`USER_M_PATH` registration in WASM.
- `axis/external_offsets/dyn_demo.ngc` with `dynamic_offsets.ini` and real
executable `M111`, verifying the same user-M boundary against an upstream sim
program.
- `axis/external_offsets/dyn_demo.ngc`, `eoffsets.ngc`, `jwp_z.ngc`, and
`opa_demo.ngc` with their corresponding INI files, real executable `M111`,
shared `eoffset.tbl`, and `opa_demo.ngc`'s `circles.ngc` subroutine,
verifying the same user-M and `SUBROUTINE_PATH` boundaries against upstream
sim programs.
Completion checks:
@@ -457,7 +462,8 @@ programs.
3. Update `tools/source-manifest.txt` and `tools/verify_vendor_sync.sh` if new
LinuxCNC files are copied into `wasm-port/vendor/linuxcnc`. Done for the
representative `axis/foam`, `axis/geometry`, and `axis/external_offsets`
sim-config files; no `verify_vendor_sync.sh` logic change was needed.
sim-config files, including the remaining external-offset M111 programs and
`circles.ngc`; no `verify_vendor_sync.sh` logic change was needed.
4. Preserve the latest result summary as a machine-readable artifact, but do
not commit generated logs unless they are intentionally used as fixtures.

View File

@@ -41,8 +41,8 @@ Current validation is intentionally mechanical:
| Switchable 5-axis bridge kinematics | `src/emc/kinematics/5axiskins.c`, `switchkins.c`, `switchkins.h`, `userkfuncs.c`, plus `src/rtapi/rtapi_ctype.h` | Copy unchanged | HAL pin allocation, HAL component lifecycle, and RTAPI module metadata are standalone runtime edges; switchable 5-axis forward/inverse behavior remains LinuxCNC source | Vendor byte sync, per-file source probes, `linuxcnc_5axis_kinematics_probe` |
| TRT table-rotary kinematics | `src/emc/kinematics/trtfuncs.c`, `xyzac-trt-kins.c`, `xyzbc-trt-kins.c` | Copy unchanged | HAL pin allocation and switchkins lifecycle stay runtime boundaries; XYZAC/XYZBC TRT forward/inverse behavior remains LinuxCNC source | Vendor byte sync, per-file source probes, `linuxcnc_xyzac_trt_kinematics_probe`, `linuxcnc_xyzbc_trt_kinematics_probe` |
| Five-axis switchkins machine configs and M428/M429/M430 remaps | `configs/sim/axis/vismach/5axis/bridgemill/*`, `table-dual-rotary/*`, and `table-rotary-tilting/*` selected INI, HAL, tool-table, XML, demo, and `remap_subs/*.ngc` files plus `src/emc/rs274ngc/interp_remap.cc` | Copy unchanged | `M428`, `M429`, and `M430` remain LinuxCNC `REMAP` entries that call LinuxCNC NGC subroutines using `M68`, `M66`, `_hal[motion.switchkins-type]`, and the INI/HAL `motion.analog-out-03 => motion.switchkins-type` link; standalone remap descriptor parsing routes through vendored `Interp::parse_remap()` and `find_ngc_file()`, and NGC remap/file execution routes through vendored LinuxCNC O-word and `open()`/`read()`/`execute()` paths while HAL synchronization stays a runtime adapter boundary | Vendor byte sync, `linuxcnc_5axis_remap_asset_probe`, `linuxcnc_remap_parse_harness`, `linuxcnc_remap_hal_sync_harness`, `linuxcnc_5axis_remap_execute_harness`, `tests/wasm/node/verify_interp_wasm.sh`, `tests/wasm/node/verify_sim_configs_wasm.sh`, `tests/browser/verify_interp_browser.sh`, `tests/browser/verify_ini_panel_browser.sh` |
| Representative sim-config machine programs | `configs/sim/axis/foam/*`, `configs/sim/axis/geometry/M110`, `xyzc.ini`, `xyzc.ngc`, `configs/sim/axis/external_offsets/M111`, `dyn_demo.ngc`, `dynamic_offsets.ini`, `eoffset.tbl`, and `configs/sim/axis/sim.tbl` | Copy unchanged | The `foam`, `xyzc`, and dynamic external-offset sample programs remain LinuxCNC sim-config assets. The standalone boundary only copies INI, tool table, G-code, and executable user-M files into the native or WASM filesystem, then calls vendored LinuxCNC INI parsing and `Interp::open()`/`read()`/`execute()` through the existing machine-config adapter. The JS SDK `runSimConfigProgram()` is only the host file-staging helper for this boundary: it writes caller-provided text files to the Emscripten filesystem, applies executable bits, and forwards to LinuxCNC-backed C ABI entry points. `M110`/`M111` process execution stays a runtime boundary represented by deterministic `USER_M_COMMAND` canonical events | Vendor byte sync, `tests/native/verify_sim_configs.sh`, `tests/wasm/node/verify_sim_configs_wasm.sh`, `tests/browser/verify_interp_browser.sh` |
| Representative `nc_files` examples | `nc_files/arcspiral.ngc`, `nc_files/factorial.ngc`, `nc_files/hole-circle.ngc`, `nc_files/m6demo.ngc` | Copy unchanged | These upstream `linuxcnc/nc_files` examples are copied byte-for-byte into the WASM vendor tree after the native `nc_files` harness classifies the broader directory. Node and browser tests only stage the original `.ngc` text in the Emscripten filesystem and call the LinuxCNC-backed `Interp::open()`, `read()`, and `execute()` path through the existing SDK; no G-code, O-word, tool-change, or M-code behavior is implemented in JavaScript | Vendor byte sync, `tests/native/verify_nc_files.sh`, `tests/wasm/node/verify_nc_files_wasm.sh`, `tests/browser/verify_interp_browser.sh`, `tests/host/verify_host_smokes.sh` |
| Representative sim-config machine programs | `configs/sim/axis/foam/*`, `configs/sim/axis/geometry/M110`, `xyzc.ini`, `xyzc.ngc`, `configs/sim/axis/external_offsets/M111`, `dyn_demo.ngc`, `dynamic_offsets.ini`, `eoffsets.ngc`, `eoffsets.ini`, `jwp_z.ngc`, `jwp_z.ini`, `opa_demo.ngc`, `opa.ini`, `circles.ngc`, `eoffset.tbl`, and `configs/sim/axis/sim.tbl` | Copy unchanged | The `foam`, `xyzc`, and external-offset sample programs remain LinuxCNC sim-config assets. The standalone boundary only copies INI, tool table, G-code, executable user-M files, and `SUBROUTINE_PATH` files into the native or WASM filesystem, then calls vendored LinuxCNC INI parsing and `Interp::open()`/`read()`/`execute()` through the existing machine-config adapter. The native sim-config harness now prefers `[DISPLAY]OPEN_FILE` when several INI files live beside a program, so each program is judged with its corresponding machine context. The JS SDK `runSimConfigProgram()` is only the host file-staging helper for this boundary: it writes caller-provided text files to the Emscripten filesystem, applies executable bits, and forwards to LinuxCNC-backed C ABI entry points. `M110`/`M111` process execution stays a runtime boundary represented by deterministic `USER_M_COMMAND` canonical events | Vendor byte sync, `tests/native/verify_sim_configs.sh`, `tests/wasm/node/verify_sim_configs_wasm.sh`, `tests/browser/verify_interp_browser.sh` |
| Representative `nc_files` examples | `nc_files/3D_Chips.ngc`, `nc_files/arcspiral.ngc`, `nc_files/factorial.ngc`, `nc_files/hole-circle.ngc`, `nc_files/m6demo.ngc` | Copy unchanged | These upstream `linuxcnc/nc_files` examples are copied byte-for-byte into the WASM vendor tree after the native `nc_files` harness classifies the broader directory. Node and browser tests only stage the original `.ngc` text and, for `3D_Chips.ngc`, the minimal INI-declared tool table context required by its upstream `T1 M6` line in the Emscripten filesystem, then call the LinuxCNC-backed `Interp::open()`, `read()`, and `execute()` path through the existing SDK; no G-code, O-word, tool-change, or M-code behavior is implemented in JavaScript | Vendor byte sync, `tests/native/verify_nc_files.sh`, `tests/wasm/node/verify_nc_files_wasm.sh`, `tests/browser/verify_interp_browser.sh`, `tests/host/verify_host_smokes.sh` |
| LinuxCNC remap regression fixtures | `tests/remap/duplicate-o-word/*`, `tests/remap/fail/args.0/*`, `tests/remap/fail/args.1/*`, `tests/remap/fail/args.2/*`, `tests/remap/fail/body-ngc/*`, `tests/remap/m30-interaction/*`, `tests/remap/nested-remaps-oword/*`, `tests/remap/posargs.0/*`, `tests/remap/sequencing/*`, selected NGC-only `tests/remap/remap-io/test-ngc.ini` plus `io_*.ngc`, and `src/emc/rs274ngc/interp_remap.cc` plus `interp_o_word.cc` | Copy unchanged | The upstream duplicate O-word, NGC-only remap failure, M30/remap-level interaction, nested O-word remap, positional-argument remap, G/M remap sequencing, and remap-IO NGC subroutine regressions remain LinuxCNC REMAP/O-word/file or MDI execution tests. The standalone boundary only copies the upstream test files into native or WASM filesystems, reads LinuxCNC INI `SUBROUTINE_PATH`, `REMAP`, and `OWORD_NARGS` entries, and calls vendored `Interp::parse_remap()`, `open()`, `read()`, and `execute()` or feeds the upstream remap-IO MDI sequence into vendored `Interp::execute()`; the continue-on-error runner path only mirrors LinuxCNC `rs274 -n 0` test execution and does not implement duplicate-label, O-word, nested remap, M30, positional-argument, failure, sequencing, M62-M68, M66 input, or remap semantics | Vendor byte sync, `linuxcnc_duplicate_oword_remap_harness`, `tests/wasm/node/verify_interp_wasm.sh`, `tests/browser/verify_interp_browser.sh` |
| LinuxCNC interpreter regression fixtures | `tests/interp/do-while-break/*`, `tests/interp/oword-bug315/*`, `tests/interp/oword-bug315-p2/*`, `tests/interp/exists/*`, `tests/interp/return-value/*`, `tests/interp/subs-follow-main/*`, `tests/interp/fractional-linenumbers/*`, `tests/interp/namedparam-bug424/*`, selected `tests/interp/rotation/*` pure interpreter cases, `tests/interp/iniparam/*`, `tests/interp/iniparam-failassign/*`, `tests/interp/m19/*`, `tests/interp/magic_comments/param_format_printing/*`, selected `tests/interp/m98m99/*` pure interpreter cases, `tests/interp/sub-call-from-sub/*`, `tests/interp/sequence-number/*`, `tests/interp/nested-sub-error/*`, `tests/interp/nested-sub-in-file-error/*`, `tests/interp/abort-hot-comment/*`, plus vendored interpreter/O-word sources including `interp_o_word.cc`, `interp_read.cc`, `interp_execute.cc`, `interp_find.cc`, `interp_namedparams.cc`, and `rs274ngc_pre.cc` | Copy unchanged | The upstream do/while/break, O-word bug315, `EXISTS[]`, subroutine return-value, subs-after-main, fractional line-number, named-parameter, selected rotation/G53/G28 absolute-position and endpoint, INI named-parameter lookup/read-only protection, M19 spindle-orient offset/timeout handling, magic-comment parameter formatting, selected Fanuc `M98/M99` and O-expression call regressions, external-subroutine-call, external-subroutine line-number, nested-subroutine-definition rejection, blocked forward-seek-to-later-numbered-sub, and `(ABORT,...)` hot-comment parameter-expansion regressions remain LinuxCNC interpreter file-execution tests. The standalone boundary only vendors the original upstream assets, copies `*.ngc`, `test.ini`, `test.tbl`, `subs/*.ngc`, and referenced external subroutine `.ngc` files into native or WASM filesystems where applicable, sets the LinuxCNC `INI_FILE_NAME` runtime edge for `_ini[...]`, applies LinuxCNC INI machine settings such as `[RS274NGC]ORIENT_OFFSET`, reads LinuxCNC INI `SUBROUTINE_PATH`, captures LinuxCNC `(PRINT,...)` output through the existing stdout/Emscripten `print` boundary, and calls vendored `Interp::open()`, `read()`, and `execute()`; it does not implement O-word loop, break, subroutine, dynamic call, `M98/M99`, `EXISTS[]`, named-parameter, absolute-position named-parameter, G28/G53 endpoint, INI-variable, read-only parameter, line-number, spindle-speed, spindle-orient, magic-comment formatting, subroutine lookup, ABORT hot-comment, error, or branch semantics | Vendor byte sync, `linuxcnc_interp_minimal_harness`, `tests/wasm/node/verify_interp_wasm.sh`, `tests/browser/verify_interp_browser.sh` |
| Additional non-switchable kinematics | `src/emc/kinematics/corexykins.c`, `rotatekins.c`, `rosekins.c`, `maxkins.c`, `lineardeltakins.c`, `lineardeltakins-common.h`, `rotarydeltakins.c`, `rotarydeltakins-common.h`, `scorbot-kins.c`, `tripodkins.c`, `scarakins.c`, `pumakins.c`, `pumakins.h`, `genhexkins.c`, `genhexkins.h`, `genserfuncs.c`, `genserkins.c`, `genserkins.h`, `ugenserkins.c`, `pentakins.c`, `pentakins.h`, `cubic.c` | Copy unchanged | HAL pin allocation, HAL parameter allocation, HAL component lifecycle, RTAPI module metadata, Go math C/C++ linkage, switchkins iterative-forward warmup, and userspace test-program process entry remain standalone runtime edges; forward/inverse behavior remains LinuxCNC source where the module exposes it | Vendor byte sync, per-file source probes, `linuxcnc_corexy_kinematics_probe`, `linuxcnc_rotate_kinematics_probe`, `linuxcnc_rose_kinematics_probe`, `linuxcnc_max_kinematics_probe`, `linuxcnc_lineardelta_kinematics_probe`, `linuxcnc_rotarydelta_kinematics_probe`, `linuxcnc_scorbot_kinematics_probe`, `linuxcnc_tripod_kinematics_probe`, `linuxcnc_scara_kinematics_probe`, `linuxcnc_puma_kinematics_probe`, `linuxcnc_genser_kinematics_probe`, `linuxcnc_genhex_kinematics_probe`, `linuxcnc_pentakins_kinematics_probe` |

View File

@@ -158,6 +158,61 @@ std::vector<std::string> load_program(int argc, char **argv)
return lines;
}
bool is_uri_unreserved(char value)
{
return (value >= 'A' && value <= 'Z') ||
(value >= 'a' && value <= 'z') ||
(value >= '0' && value <= '9') ||
value == '-' || value == '_' || value == '.' || value == '~';
}
void append_percent_encoded_text(std::ostream &output, const char *text)
{
constexpr char hex[] = "0123456789ABCDEF";
for (const char *cursor = text ? text : ""; *cursor; ++cursor) {
char value = *cursor;
switch (*cursor) {
case '\r':
case '\n':
case '\t':
value = ' ';
break;
default:
break;
}
if (is_uri_unreserved(value)) {
output << value;
} else {
const auto byte = static_cast<unsigned char>(value);
output << '%' << hex[(byte >> 4) & 0x0f] << hex[byte & 0x0f];
}
}
}
void append_run_step(std::ostream &output,
const Interp &interp,
const char *phase,
int step,
int rc)
{
output << "run_step phase=" << phase
<< " step=" << step
<< " rc=" << rc
<< " line=" << interp._setup.sequence_number
<< " x=" << interp._setup.current_x
<< " y=" << interp._setup.current_y
<< " z=" << interp._setup.current_z
<< " a=" << interp._setup.AA_current
<< " b=" << interp._setup.BB_current
<< " c=" << interp._setup.CC_current
<< " u=" << interp._setup.u_current
<< " v=" << interp._setup.v_current
<< " w=" << interp._setup.w_current
<< " statement_uri=";
append_percent_encoded_text(output, interp._setup.linetext);
output << "\n";
}
} // namespace
int main(int argc, char **argv)
@@ -228,6 +283,7 @@ int main(int argc, char **argv)
}
++file_read_count;
std::cout << "file_read_" << file_read_count << "=" << file_read_rc << "\n";
append_run_step(std::cout, file_interp, "read", file_read_count, file_read_rc);
if ((file_read_rc != INTERP_OK) && (file_read_rc != INTERP_EXECUTE_FINISH)) {
if (file_read_rc > INTERP_MIN_ERROR) {
char error_buf[LINELEN] = {0};
@@ -244,6 +300,7 @@ int main(int argc, char **argv)
const int file_execute_rc = file_interp.execute();
++file_execute_count;
std::cout << "file_execute_" << file_execute_count << "=" << file_execute_rc << "\n";
append_run_step(std::cout, file_interp, "execute", file_execute_count, file_execute_rc);
if (file_execute_rc > INTERP_MIN_ERROR) {
char error_buf[LINELEN] = {0};
file_interp.error_text(file_execute_rc, error_buf, sizeof(error_buf));

View File

@@ -264,6 +264,61 @@ void append_tool_table_state(std::ostringstream &output)
output << "tool_index_for_tool_2=" << standalone::find_tool_index_for_tool(2) << "\n";
}
bool is_uri_unreserved(char value)
{
return (value >= 'A' && value <= 'Z') ||
(value >= 'a' && value <= 'z') ||
(value >= '0' && value <= '9') ||
value == '-' || value == '_' || value == '.' || value == '~';
}
void append_percent_encoded_text(std::ostringstream &output, const char *text)
{
constexpr char hex[] = "0123456789ABCDEF";
for (const char *cursor = text ? text : ""; *cursor; ++cursor) {
char value = *cursor;
switch (*cursor) {
case '\r':
case '\n':
case '\t':
value = ' ';
break;
default:
break;
}
if (is_uri_unreserved(value)) {
output << value;
} else {
const auto byte = static_cast<unsigned char>(value);
output << '%' << hex[(byte >> 4) & 0x0f] << hex[byte & 0x0f];
}
}
}
void append_run_step(std::ostringstream &output,
const Interp &interp,
const char *phase,
int step,
int rc)
{
output << "run_step phase=" << phase
<< " step=" << step
<< " rc=" << rc
<< " line=" << interp._setup.sequence_number
<< " x=" << interp._setup.current_x
<< " y=" << interp._setup.current_y
<< " z=" << interp._setup.current_z
<< " a=" << interp._setup.AA_current
<< " b=" << interp._setup.BB_current
<< " c=" << interp._setup.CC_current
<< " u=" << interp._setup.u_current
<< " v=" << interp._setup.v_current
<< " w=" << interp._setup.w_current
<< " statement_uri=";
append_percent_encoded_text(output, interp._setup.linetext);
output << "\n";
}
void append_error_text(std::ostringstream &output, Interp &interp, const char *prefix, int rc)
{
if (rc > INTERP_MIN_ERROR) {
@@ -539,6 +594,7 @@ char *run_file_with_ini_internal(const char *path, const char *ini_path, bool co
++file_read_count;
output << "file_read_" << file_read_count << "=" << read_rc << "\n";
append_run_step(output, interp, "read", file_read_count, read_rc);
if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) {
if (read_rc > INTERP_MIN_ERROR) {
char error_buf[LINELEN] = {0};
@@ -555,6 +611,7 @@ char *run_file_with_ini_internal(const char *path, const char *ini_path, bool co
const int execute_rc = interp.execute();
++file_execute_count;
output << "file_execute_" << file_execute_count << "=" << execute_rc << "\n";
append_run_step(output, interp, "execute", file_execute_count, execute_rc);
if (execute_rc > INTERP_MIN_ERROR) {
char error_buf[LINELEN] = {0};
interp.error_text(execute_rc, error_buf, sizeof(error_buf));

View File

@@ -25,6 +25,7 @@ Use `src/index.js` for stable imports:
import {
createLinuxCncIniSdk,
createLinuxCncInterpSdk,
planSimConfigStaging,
} from "./src/index.js";
```
@@ -85,6 +86,15 @@ into the Emscripten filesystem, applies executable bits for user M-code files,
then forwards to `runFileWithIni()` by default or to `runFiveAxisRemapFile()`
when `executionMode: "fiveAxisRemap"` is explicitly requested.
`planSimConfigStaging()` is the companion file-staging planner for
representative LinuxCNC `configs/sim` programs. Callers provide the vendored
source manifest text, machine relative path, INI file name, and INI text. The
planner reads only INI file references and the manifest, then returns the INI,
`[DISPLAY]OPEN_FILE`, `[EMCIO]TOOL_TABLE`, `[RS274NGC]PARAMETER_FILE`,
`[RS274NGC]SUBROUTINE_PATH`, `[RS274NGC]USER_M_PATH`, and remap-NGC files that
should be copied into the Emscripten filesystem. It does not implement G-code,
tool, parameter, remap, or user-M semantics.
`runFileWithIniContinueOnError()` uses the same LinuxCNC-backed file execution
path but keeps the runner loop going after LinuxCNC reports an error, matching
upstream `rs274 -n 0` regression tests such as `tests/interp/oword-unwind`.

View File

@@ -1,2 +1,3 @@
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
export { planSimConfigStaging } from "./sim-config-staging.js";

View File

@@ -0,0 +1,252 @@
function cleanManifest(manifestText) {
return manifestText
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"));
}
function normalizeRel(path) {
const parts = [];
for (const part of path.split("/")) {
if (!part || part === ".") {
continue;
}
if (part === "..") {
parts.pop();
continue;
}
parts.push(part);
}
return parts.join("/");
}
function dirname(path) {
const clean = normalizeRel(path);
const index = clean.lastIndexOf("/");
return index === -1 ? "" : clean.slice(0, index);
}
function basename(path) {
const clean = normalizeRel(path);
const index = clean.lastIndexOf("/");
return index === -1 ? clean : clean.slice(index + 1);
}
function stripIniComment(line) {
const hash = line.indexOf("#");
const semicolon = line.indexOf(";");
const indexes = [hash, semicolon].filter((index) => index >= 0);
if (indexes.length === 0) {
return line;
}
return line.slice(0, Math.min(...indexes));
}
function parseIni(iniText) {
const values = new Map();
let section = "";
for (const rawLine of iniText.split("\n")) {
const sectionMatch = rawLine.match(/^\s*\[([^\]]+)\]/);
if (sectionMatch) {
section = sectionMatch[1].trim().toUpperCase();
continue;
}
const line = stripIniComment(rawLine);
const equals = line.indexOf("=");
if (equals === -1 || !section) {
continue;
}
const key = line.slice(0, equals).trim().toUpperCase();
const value = line.slice(equals + 1).trim();
if (!key || !value) {
continue;
}
const mapKey = `${section}.${key}`;
const existing = values.get(mapKey) ?? [];
existing.push(value);
values.set(mapKey, existing);
}
return values;
}
function firstIniValue(values, section, key) {
return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`)?.[0] ?? null;
}
function allIniValues(values, section, key) {
return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`) ?? [];
}
function splitSearchPath(value) {
return value.split(":").map((entry) => entry.trim()).filter(Boolean);
}
function sourceRelFor(machineRel, path) {
return normalizeRel(`configs/sim/${machineRel}/${path}`);
}
function targetPathFor(wasmDir, targetRel) {
return `${wasmDir}/${normalizeRel(targetRel)}`;
}
function isUserMCodePath(path) {
return /^M1\d\d$/i.test(basename(path));
}
function isSubroutinePath(path) {
return path.toLowerCase().endsWith(".ngc");
}
function findUpwardByBasename(manifestSet, sourceDir, fileName) {
let current = sourceDir;
while (current.startsWith("configs/sim")) {
const candidate = normalizeRel(`${current}/${fileName}`);
if (manifestSet.has(candidate)) {
return candidate;
}
if (current === "configs/sim") {
break;
}
current = dirname(current);
}
return null;
}
function sourceForReference(manifestSet, sourceDir, reference) {
if (!reference || reference.startsWith("/")) {
return null;
}
const sourceRel = normalizeRel(`${sourceDir}/${reference}`);
if (manifestSet.has(sourceRel)) {
return sourceRel;
}
return findUpwardByBasename(manifestSet, sourceDir, basename(reference));
}
function addPlannedFile(plan, manifestSet, sourceRel, targetRel, options = {}) {
if (!sourceRel || !manifestSet.has(sourceRel)) {
if (options.required) {
throw new Error(`missing vendored sim-config file: ${sourceRel}`);
}
return;
}
const existing = plan.get(sourceRel);
const executable = options.executable || isUserMCodePath(sourceRel);
if (existing) {
existing.executable ||= executable;
return;
}
plan.set(sourceRel, {
sourceRel,
path: targetPathFor(options.wasmDir, targetRel),
executable,
});
}
function addDirectoryFiles(plan, manifestEntries, sourceDir, targetDir, predicate, options) {
const prefix = sourceDir ? `${sourceDir}/` : "";
for (const sourceRel of manifestEntries) {
if (!sourceRel.startsWith(prefix)) {
continue;
}
const childRel = sourceRel.slice(prefix.length);
if (childRel.includes("/") || !predicate(sourceRel)) {
continue;
}
addPlannedFile(plan, new Set(manifestEntries), sourceRel, normalizeRel(`${targetDir}/${childRel}`), options);
}
}
function remapNgcNames(iniValues) {
const names = [];
for (const remap of allIniValues(iniValues, "RS274NGC", "REMAP")) {
const match = remap.match(/(?:^|\s)ngc=([^\s]+)/i);
if (match) {
names.push(`${match[1]}.ngc`);
}
}
return names;
}
export function planSimConfigStaging({
manifestText,
machineRel,
iniFile,
iniText,
programFile = null,
wasmDir = `/work/sim/${machineRel}`,
}) {
const manifestEntries = cleanManifest(manifestText);
const manifestSet = new Set(manifestEntries);
const iniValues = parseIni(iniText);
const plan = new Map();
const sourceDir = sourceRelFor(machineRel, dirname(iniFile));
const iniSourceRel = sourceRelFor(machineRel, iniFile);
const programReference = programFile ?? firstIniValue(iniValues, "DISPLAY", "OPEN_FILE");
addPlannedFile(plan, manifestSet, iniSourceRel, iniFile, { wasmDir, required: true });
if (programReference) {
const programSourceRel = sourceForReference(manifestSet, sourceDir, programReference);
addPlannedFile(plan, manifestSet, programSourceRel, programReference, {
wasmDir,
required: true,
});
}
for (const [section, key] of [
["EMCIO", "TOOL_TABLE"],
["RS274NGC", "PARAMETER_FILE"],
]) {
const reference = firstIniValue(iniValues, section, key);
if (!reference) {
continue;
}
const sourceRel = sourceForReference(manifestSet, sourceDir, reference);
addPlannedFile(plan, manifestSet, sourceRel, reference, { wasmDir });
}
const subroutineDirs = splitSearchPath(
firstIniValue(iniValues, "RS274NGC", "SUBROUTINE_PATH") ?? "",
);
for (const dirEntry of subroutineDirs) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceSubdir = normalizeRel(`${sourceDir}/${dirEntry}`);
const targetSubdir = normalizeRel(dirEntry);
addDirectoryFiles(plan, manifestEntries, sourceSubdir, targetSubdir, isSubroutinePath, {
wasmDir,
});
}
for (const dirEntry of splitSearchPath(firstIniValue(iniValues, "RS274NGC", "USER_M_PATH") ?? "")) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
const targetUserMDir = normalizeRel(dirEntry);
addDirectoryFiles(plan, manifestEntries, sourceUserMDir, targetUserMDir, isUserMCodePath, {
wasmDir,
executable: true,
});
}
for (const remapName of remapNgcNames(iniValues)) {
for (const dirEntry of subroutineDirs) {
const sourceRel = sourceForReference(manifestSet, normalizeRel(`${sourceDir}/${dirEntry}`), remapName);
addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${dirEntry}/${remapName}`), {
wasmDir,
});
}
}
return {
wasmDir,
iniPath: targetPathFor(wasmDir, iniFile),
programPath: programReference ? targetPathFor(wasmDir, programReference) : null,
files: [...plan.values()],
};
}

View File

@@ -179,6 +179,52 @@ function parseRunMotion(resultText, programText) {
const axes = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 };
const snapshots = [];
const sourceLines = programLineMap(programText);
const motionByLine = new Map();
for (const line of resultText.split("\n")) {
const motion = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/);
if (!motion) {
continue;
}
const sourceLine = readCanonicalNumber(line, "line");
if (Number.isFinite(sourceLine)) {
motionByLine.set(sourceLine, motion[1]);
}
}
for (const line of resultText.split("\n")) {
if (!line.startsWith("run_step phase=execute ")) {
continue;
}
for (const axis of Object.keys(axes)) {
const value = readCanonicalNumber(line, axis);
if (value !== null && Number.isFinite(value)) {
axes[axis] = value;
}
}
const sourceLine = readCanonicalNumber(line, "line");
const statementIndex = line.indexOf(" statement_uri=");
const statementValue =
statementIndex >= 0 ? line.slice(statementIndex + " statement_uri=".length).trim() : "";
let statement = "";
try {
statement = statementValue ? decodeURIComponent(statementValue) : "";
} catch {
statement = statementValue;
}
snapshots.push({
type: motionByLine.get(sourceLine) ?? "EXECUTE",
line: sourceLine,
statement: statement || (Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-"),
axes: { ...axes },
});
}
if (snapshots.length > 0) {
return snapshots;
}
for (const line of resultText.split("\n")) {
const motion = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/);

View File

@@ -10,6 +10,7 @@
import {
createLinuxCncIniSdk,
createLinuxCncInterpSdk,
planSimConfigStaging,
} from "../../runtime/sdk/src/index.js";
import {
saveMachineParametersToOpfs,
@@ -101,21 +102,27 @@
}
}
async function loadSimMachineFiles(machineRel, files, executableFiles = []) {
const sourceDir = `../../vendor/linuxcnc/configs/sim/${machineRel}`;
const wasmDir = `/work/browser-sim/${machineRel}`;
const executableSet = new Set(executableFiles);
const machineFiles = [];
for (const filename of files) {
machineFiles.push({
path: `${wasmDir}/${filename}`,
text: await fetchText(`${sourceDir}/${filename}`),
executable: executableSet.has(filename),
async function loadSimMachineFiles(machineRel, iniFile, programFile = null) {
const iniText = await fetchText(
`../../vendor/linuxcnc/configs/sim/${machineRel}/${iniFile}`,
);
const plan = planSimConfigStaging({
manifestText: await fetchText("../../tools/source-manifest.txt"),
machineRel,
iniFile,
iniText,
programFile,
wasmDir: `/work/browser-sim/${machineRel}`,
});
}
return { wasmDir, files: machineFiles };
return {
...plan,
files: await Promise.all(plan.files.map(async (file) => ({
path: file.path,
text: await fetchText(`../../vendor/linuxcnc/${file.sourceRel}`),
executable: file.executable,
}))),
};
}
async function writeFiveAxisTrtMachineFiles(interp) {
@@ -275,6 +282,36 @@
return interp.runFile(wasmPath);
}
async function runVendorNcFileWithToolTable(interp, filename) {
const wasmDir = "/work/browser-nc-files-context";
const wasmPath = `${wasmDir}/${filename}`;
const iniPath = `${wasmDir}/nc_files_context.ini`;
await writeFetchedTextFile(
interp,
`../../vendor/linuxcnc/nc_files/${filename}`,
wasmPath,
);
interp.writeTextFile(
`${wasmDir}/tool.tbl`,
"T1 P1 D10.000000 Z+0.000000 ; nc_files representative context tool\n",
);
interp.writeTextFile(
iniPath,
[
"[RS274NGC]",
"SUBROUTINE_PATH = .",
"",
"[TRAJ]",
"COORDINATES = X Y Z A B C U V W",
"",
"[EMCIO]",
"TOOL_TABLE = tool.tbl",
"",
].join("\n"),
);
return interp.runFileWithIni(wasmPath, iniPath);
}
async function verifyRejects(fixtureName, operation, expectedPattern) {
try {
await operation();
@@ -507,6 +544,18 @@
);
}
verifyExpectedOutput(
"browser_minimal_linear_run_steps",
interp.runFile("/work/minimal_linear.ngc"),
[
"run_step phase=read step=1 rc=0 line=1",
"run_step phase=execute step=1 rc=0 line=1 x=1 y=2 z=0",
"statement_uri=G0%20X1.0%20Y2.0%20%28Comment%29",
"run_step phase=execute step=2 rc=0 line=2 x=3 y=4 z=0",
"statement_uri=G1%20X3.0%20Y4.0%20F120.0",
].join("\n"),
);
const namedParamPath = `/work/${INTERP_INI_FIXTURE}.ngc`;
interp.writeTextFile(namedParamPath, namedParamProgramText);
verifyExpectedOutput(
@@ -564,6 +613,28 @@
].join("\n"),
);
verifyExpectedOutput(
"browser_nc_files_3d_chips_context",
await runVendorNcFileWithToolTable(interp, "3D_Chips.ngc"),
[
"file_open=0",
"file_read_count=4706",
"file_execute_count=4706",
"file_saw_error=0",
"run_step phase=execute step=18 rc=2 line=18",
"statement_uri=N50T%23%3Ctoolno%3EM6",
"run_step phase=execute step=19 rc=0 line=19",
"statement_uri=N60M8",
"canon_event=CHANGE_TOOL",
"canon_event=START_SPINDLE_CLOCKWISE spindle=0",
"canon_event=STRAIGHT_TRAVERSE line=21 x=0 y=0 z=10",
"canon_event=STRAIGHT_FEED line=23 x=53 y=-56.128 z=-25.372",
"canon_event=PROGRAM_END",
"absent=file_error_text=",
"absent=Requested tool 1 not found",
].join("\n"),
);
verifyExpectedOutput(
"browser_nc_files_arcspiral",
await runVendorNcFile(interp, "arcspiral.ngc"),
@@ -622,16 +693,13 @@
].join("\n"),
);
const foam = await loadSimMachineFiles("axis/foam", [
"axis_foam.ini",
"foam.ngc",
]);
const foam = await loadSimMachineFiles("axis/foam", "axis_foam.ini", "foam.ngc");
verifyExpectedOutput(
"browser_sim_axis_foam_uv",
interp.runSimConfigProgram({
files: foam.files,
programPath: `${foam.wasmDir}/foam.ngc`,
iniPath: `${foam.wasmDir}/axis_foam.ini`,
programPath: foam.programPath,
iniPath: foam.iniPath,
}),
[
"file_open=0",
@@ -644,21 +712,15 @@
const bridgeMillSim = await loadSimMachineFiles(
"axis/vismach/5axis/bridgemill",
[
"5axis.ini",
"5axis.tbl",
"5axisgui.ngc",
"remap_subs/428remap.ngc",
"remap_subs/429remap.ngc",
"remap_subs/430remap.ngc",
],
);
verifyExpectedOutput(
"browser_sim_axis_bridgemill_w",
interp.runSimConfigProgram({
files: bridgeMillSim.files,
programPath: `${bridgeMillSim.wasmDir}/5axisgui.ngc`,
iniPath: `${bridgeMillSim.wasmDir}/5axis.ini`,
programPath: bridgeMillSim.programPath,
iniPath: bridgeMillSim.iniPath,
executionMode: "fiveAxisRemap",
}),
[
@@ -682,19 +744,15 @@
const geometry = await loadSimMachineFiles(
"axis/geometry",
[
"M110",
"xyzc.ini",
"xyzc.ngc",
],
["M110"],
);
verifyExpectedOutput(
"browser_sim_axis_geometry_xyzc_user_m110",
interp.runSimConfigProgram({
files: geometry.files,
programPath: `${geometry.wasmDir}/xyzc.ngc`,
iniPath: `${geometry.wasmDir}/xyzc.ini`,
programPath: geometry.programPath,
iniPath: geometry.iniPath,
}),
[
"file_open=0",
@@ -706,30 +764,67 @@
].join("\n"),
);
for (const externalOffsetCase of [
{
name: "dyn_demo",
program: "dyn_demo.ngc",
ini: "dynamic_offsets.ini",
readCount: 833,
firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=21 x=0 y=0 z=0",
firstFeed: "canon_event=STRAIGHT_FEED line=26 x=9 y=0 z=1.5",
},
{
name: "eoffsets",
program: "eoffsets.ngc",
ini: "eoffsets.ini",
readCount: 35,
firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=24 x=0 y=0 z=0",
firstFeed: "canon_event=STRAIGHT_FEED line=27 x=2.25 y=2.64 z=0",
},
{
name: "jwp_z",
program: "jwp_z.ngc",
ini: "jwp_z.ini",
readCount: 35,
firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=17 x=0 y=0 z=0.5",
firstFeed: "canon_event=STRAIGHT_FEED line=24 x=2.5 y=3.3 z=0",
},
{
name: "opa_demo",
program: "opa_demo.ngc",
ini: "opa.ini",
readCount: 58,
firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=13 x=1 y=0 z=0",
firstFeed: "canon_event=STRAIGHT_FEED line=35 x=1 y=0 z=0",
},
]) {
const externalOffsets = await loadSimMachineFiles(
"axis/external_offsets",
[
"M111",
"dyn_demo.ngc",
"dynamic_offsets.ini",
],
["M111"],
externalOffsetCase.ini,
externalOffsetCase.program,
);
verifyExpectedOutput(
"browser_sim_axis_external_offsets_user_m111",
`browser_sim_axis_external_offsets_${externalOffsetCase.name}_user_m111`,
interp.runSimConfigProgram({
files: externalOffsets.files,
programPath: `${externalOffsets.wasmDir}/dyn_demo.ngc`,
iniPath: `${externalOffsets.wasmDir}/dynamic_offsets.ini`,
programPath: externalOffsets.programPath,
iniPath: externalOffsets.iniPath,
}),
[
"file_open=0",
`file_read_count=${externalOffsetCase.readCount}`,
`file_execute_count=${externalOffsetCase.readCount}`,
"file_saw_error=0",
"canon_event=USER_M_COMMAND code=M111",
externalOffsetCase.firstTraverse,
externalOffsetCase.firstFeed,
"canon_event=PROGRAM_END",
"absent=Unknown m code used: M111",
"absent=Bad character",
"absent=Cannot use axis values",
].join("\n"),
);
}
const fiveAxisTrtDir = await writeFiveAxisTrtMachineFiles(interp);
verifyExpectedOutput(

View File

@@ -81,9 +81,33 @@ find_nearest_ini() {
return 1
}
find_ini_by_open_file() {
local ngc="$1"
local ngc_dir="$2"
local ini open_file resolved
while IFS= read -r ini; do
open_file="$(ini_value "$ini" DISPLAY OPEN_FILE || true)"
[[ -z "$open_file" ]] && continue
if [[ "$open_file" = /* ]]; then
resolved="$open_file"
else
resolved="$(realpath -m "$(dirname "$ini")/$open_file")"
fi
if [[ "$resolved" == "$(realpath -m "$ngc")" ]]; then
printf '%s\n' "$ini"
return 0
fi
done < <(find "$ngc_dir" -maxdepth 1 -type f -name '*.ini' | sort)
return 1
}
find_sim_ini() {
local rel="$1"
local ngc_dir="$2"
local ngc="$3"
if find_ini_by_open_file "$ngc" "$ngc_dir"; then
return 0
fi
case "$rel" in
axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/*)
printf '%s\n' "$SIM_DIR/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini"
@@ -289,7 +313,7 @@ while IFS= read -r ngc; do
continue
fi
ini="$(find_sim_ini "$rel" "$(dirname "$ngc")" || true)"
ini="$(find_sim_ini "$rel" "$(dirname "$ngc")" "$ngc" || true)"
if [[ -z "$ini" ]]; then
printf '%s\tunsupported_runtime_edge\tmissing-ini\n' "$rel" >> "$BUILD_DIR/skipped.tsv"
skip=$((skip + 1))

View File

@@ -446,6 +446,18 @@ for (const fixtureName of INTERP_FILE_FIXTURES) {
verifyExpectedOutput(fixtureName, interp.runFile(programPath), expectedEvents);
}
verifyExpectedOutput(
"minimal_linear_run_steps",
interp.runFile("/work/minimal_linear.ngc"),
[
"run_step phase=read step=1 rc=0 line=1",
"run_step phase=execute step=1 rc=0 line=1 x=1 y=2 z=0",
"statement_uri=G0%20X1.0%20Y2.0%20%28Comment%29",
"run_step phase=execute step=2 rc=0 line=2 x=3 y=4 z=0",
"statement_uri=G1%20X3.0%20Y4.0%20F120.0",
].join("\n"),
);
const namedParamFilePath = `/work/${INTERP_INI_FIXTURE}.ngc`;
interp.writeTextFile(namedParamFilePath, namedParamProgramText);
verifyExpectedOutput(

View File

@@ -47,6 +47,54 @@ function runVendorNcFile(filename) {
return interp.runFile(wasmPath);
}
function runVendorNcFileWithToolTable(filename) {
const wasmDir = "/work/nc-files-context";
const wasmPath = `${wasmDir}/${filename}`;
const iniPath = `${wasmDir}/nc_files_context.ini`;
interp.writeTextFile(wasmPath, readFileSync(resolve(ncFilesDir, filename), "utf8"));
interp.writeTextFile(
`${wasmDir}/tool.tbl`,
"T1 P1 D10.000000 Z+0.000000 ; nc_files representative context tool\n",
);
interp.writeTextFile(
iniPath,
[
"[RS274NGC]",
"SUBROUTINE_PATH = .",
"",
"[TRAJ]",
"COORDINATES = X Y Z A B C U V W",
"",
"[EMCIO]",
"TOOL_TABLE = tool.tbl",
"",
].join("\n"),
);
return interp.runFileWithIni(wasmPath, iniPath);
}
verifyExpectedOutput(
"nc_files_3d_chips_context_wasm",
runVendorNcFileWithToolTable("3D_Chips.ngc"),
[
"file_open=0",
"file_read_count=4706",
"file_execute_count=4706",
"file_saw_error=0",
"run_step phase=execute step=18 rc=2 line=18",
"statement_uri=N50T%23%3Ctoolno%3EM6",
"run_step phase=execute step=19 rc=0 line=19",
"statement_uri=N60M8",
"canon_event=CHANGE_TOOL",
"canon_event=START_SPINDLE_CLOCKWISE spindle=0",
"canon_event=STRAIGHT_TRAVERSE line=21 x=0 y=0 z=10",
"canon_event=STRAIGHT_FEED line=23 x=53 y=-56.128 z=-25.372",
"canon_event=PROGRAM_END",
"absent=file_error_text=",
"absent=Requested tool 1 not found",
].join("\n"),
);
verifyExpectedOutput(
"nc_files_arcspiral_wasm",
runVendorNcFile("arcspiral.ngc"),

View File

@@ -3,7 +3,10 @@ import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";
import { createLinuxCncInterpSdk } from "../../../runtime/sdk/src/index.js";
import {
createLinuxCncInterpSdk,
planSimConfigStaging,
} from "../../../runtime/sdk/src/index.js";
function verifyExpectedOutput(fixtureName, output, expectedText) {
const expectedLines = expectedText.split("\n").filter(Boolean);
@@ -32,6 +35,7 @@ const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "build/wasm/core/linuxcnc_interp.wasm");
const vendorRoot = resolve(rootDir, "vendor/linuxcnc");
const manifestText = readFileSync(resolve(rootDir, "tools/source-manifest.txt"), "utf8");
const interp = await createLinuxCncInterpSdk({
wasmBinary: readFileSync(wasmPath),
@@ -41,35 +45,72 @@ const interp = await createLinuxCncInterpSdk({
},
});
function vendorFile(sourceRel, wasmPath) {
const sourcePath = resolve(vendorRoot, sourceRel);
const syntheticPlan = planSimConfigStaging({
manifestText: [
"configs/sim/example/test.ini",
"configs/sim/example/test.ngc",
"configs/sim/example/tool.tbl",
"configs/sim/example/linuxcnc.var",
"configs/sim/example/subs/helper.ngc",
"configs/sim/example/M123",
].join("\n"),
machineRel: "example",
iniFile: "test.ini",
iniText: [
"[DISPLAY]",
"OPEN_FILE = test.ngc",
"[RS274NGC]",
"PARAMETER_FILE = linuxcnc.var",
"SUBROUTINE_PATH = subs",
"USER_M_PATH = .",
"[EMCIO]",
"TOOL_TABLE = tool.tbl",
].join("\n"),
});
assert.deepEqual(
syntheticPlan.files.map((file) => [file.sourceRel, file.executable]).sort(),
[
["configs/sim/example/M123", true],
["configs/sim/example/linuxcnc.var", false],
["configs/sim/example/subs/helper.ngc", false],
["configs/sim/example/test.ini", false],
["configs/sim/example/test.ngc", false],
["configs/sim/example/tool.tbl", false],
],
"synthetic sim-config staging plan",
);
function simMachine(machineRel, iniFile, programFile = null) {
const iniText = readFileSync(
resolve(vendorRoot, `configs/sim/${machineRel}/${iniFile}`),
"utf8",
);
const plan = planSimConfigStaging({
manifestText,
machineRel,
iniFile,
iniText,
programFile,
wasmDir: `/work/sim/${machineRel}`,
});
return {
path: wasmPath,
text: readFileSync(sourcePath, "utf8"),
executable: (statSync(sourcePath).mode & 0o111) !== 0,
...plan,
files: plan.files.map((file) => ({
path: file.path,
text: readFileSync(resolve(vendorRoot, file.sourceRel), "utf8"),
executable:
file.executable || (statSync(resolve(vendorRoot, file.sourceRel)).mode & 0o111) !== 0,
})),
};
}
function simMachine(machineRel, files) {
const wasmDir = `/work/sim/${machineRel}`;
return {
wasmDir,
files: files.map((file) =>
vendorFile(`configs/sim/${machineRel}/${file}`, `${wasmDir}/${file}`),
),
};
}
const foam = simMachine("axis/foam", [
"axis_foam.ini",
"foam.ngc",
]);
const foam = simMachine("axis/foam", "axis_foam.ini", "foam.ngc");
verifyExpectedOutput(
"sim_axis_foam_uv_wasm",
interp.runSimConfigProgram({
files: foam.files,
programPath: `${foam.wasmDir}/foam.ngc`,
iniPath: `${foam.wasmDir}/axis_foam.ini`,
programPath: foam.programPath,
iniPath: foam.iniPath,
}),
[
"file_open=0",
@@ -80,20 +121,17 @@ verifyExpectedOutput(
].join("\n"),
);
const bridgeMill = simMachine("axis/vismach/5axis/bridgemill", [
const bridgeMill = simMachine(
"axis/vismach/5axis/bridgemill",
"5axis.ini",
"5axis.tbl",
"5axisgui.ngc",
"remap_subs/428remap.ngc",
"remap_subs/429remap.ngc",
"remap_subs/430remap.ngc",
]);
);
verifyExpectedOutput(
"sim_axis_bridgemill_w_wasm",
interp.runSimConfigProgram({
files: bridgeMill.files,
programPath: `${bridgeMill.wasmDir}/5axisgui.ngc`,
iniPath: `${bridgeMill.wasmDir}/5axis.ini`,
programPath: bridgeMill.programPath,
iniPath: bridgeMill.iniPath,
executionMode: "fiveAxisRemap",
}),
[
@@ -115,17 +153,13 @@ verifyExpectedOutput(
].join("\n"),
);
const geometry = simMachine("axis/geometry", [
"M110",
"xyzc.ini",
"xyzc.ngc",
]);
const geometry = simMachine("axis/geometry", "xyzc.ini", "xyzc.ngc");
verifyExpectedOutput(
"sim_axis_geometry_xyzc_user_m110_wasm",
interp.runSimConfigProgram({
files: geometry.files,
programPath: `${geometry.wasmDir}/xyzc.ngc`,
iniPath: `${geometry.wasmDir}/xyzc.ini`,
programPath: geometry.programPath,
iniPath: geometry.iniPath,
}),
[
"file_open=0",
@@ -137,25 +171,66 @@ verifyExpectedOutput(
].join("\n"),
);
const externalOffsets = simMachine("axis/external_offsets", [
"M111",
"dyn_demo.ngc",
"dynamic_offsets.ini",
]);
for (const externalOffsetCase of [
{
name: "dyn_demo",
program: "dyn_demo.ngc",
ini: "dynamic_offsets.ini",
readCount: 833,
firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=21 x=0 y=0 z=0",
firstFeed: "canon_event=STRAIGHT_FEED line=26 x=9 y=0 z=1.5",
},
{
name: "eoffsets",
program: "eoffsets.ngc",
ini: "eoffsets.ini",
readCount: 35,
firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=24 x=0 y=0 z=0",
firstFeed: "canon_event=STRAIGHT_FEED line=27 x=2.25 y=2.64 z=0",
},
{
name: "jwp_z",
program: "jwp_z.ngc",
ini: "jwp_z.ini",
readCount: 35,
firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=17 x=0 y=0 z=0.5",
firstFeed: "canon_event=STRAIGHT_FEED line=24 x=2.5 y=3.3 z=0",
},
{
name: "opa_demo",
program: "opa_demo.ngc",
ini: "opa.ini",
readCount: 58,
firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=13 x=1 y=0 z=0",
firstFeed: "canon_event=STRAIGHT_FEED line=35 x=1 y=0 z=0",
},
]) {
const externalOffsets = simMachine(
"axis/external_offsets",
externalOffsetCase.ini,
externalOffsetCase.program,
);
verifyExpectedOutput(
"sim_axis_external_offsets_user_m111_wasm",
`sim_axis_external_offsets_${externalOffsetCase.name}_user_m111_wasm`,
interp.runSimConfigProgram({
files: externalOffsets.files,
programPath: `${externalOffsets.wasmDir}/dyn_demo.ngc`,
iniPath: `${externalOffsets.wasmDir}/dynamic_offsets.ini`,
programPath: externalOffsets.programPath,
iniPath: externalOffsets.iniPath,
}),
[
"file_open=0",
`file_read_count=${externalOffsetCase.readCount}`,
`file_execute_count=${externalOffsetCase.readCount}`,
"file_saw_error=0",
"canon_event=USER_M_COMMAND code=M111",
externalOffsetCase.firstTraverse,
externalOffsetCase.firstFeed,
"canon_event=PROGRAM_END",
"absent=Unknown m code used: M111",
"absent=Bad character",
"absent=Cannot use axis values",
].join("\n"),
);
}
console.log("sim_configs_wasm_node_smoke=ok");

View File

@@ -346,9 +346,16 @@ tests/interp/abort-hot-comment/test.ngc
tests/interp/abort-hot-comment/test.sh
configs/sim/axis/sim.tbl
configs/sim/axis/external_offsets/M111
configs/sim/axis/external_offsets/circles.ngc
configs/sim/axis/external_offsets/dyn_demo.ngc
configs/sim/axis/external_offsets/dynamic_offsets.ini
configs/sim/axis/external_offsets/eoffset.tbl
configs/sim/axis/external_offsets/eoffsets.ini
configs/sim/axis/external_offsets/eoffsets.ngc
configs/sim/axis/external_offsets/jwp_z.ini
configs/sim/axis/external_offsets/jwp_z.ngc
configs/sim/axis/external_offsets/opa.ini
configs/sim/axis/external_offsets/opa_demo.ngc
configs/sim/axis/foam/axis_foam.ini
configs/sim/axis/foam/foam.ngc
configs/sim/axis/geometry/M110
@@ -403,6 +410,7 @@ configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.txt
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml
nc_files/3D_Chips.ngc
nc_files/arcspiral.ngc
nc_files/factorial.ngc
nc_files/hole-circle.ngc

View File

@@ -0,0 +1,43 @@
(info: Multiple circles at radius R, use 0 for current val)
;Notes:
; 1) z axis not altered (remains at current z)
; 2) for inside polygon cutting (fnum==0):
; start with rfraction negative
; and gradually increase rfraction to zero as program runs
; multiple circles
; 3) for outside polygon cutting (fnum==1):
; start with rfraction positive
; and gradually decrease to zero as program runs multiple
; circles
;
; Alternate method for polygons:
; 1) use #<radius>=0 and jog to safe starting position
; 2) use #<revs>=1, run program for one revolution
; 3) Run program repeatedly and manually jog x (radius)
; before each program start to increase/decrease
; current radius for inside/outside polygon respectively
o<circles> sub
#<radius> = #1 (= 1 R 0==>current)
#<aspeed> = #2 (= 8 aspeed: RPM)
#<lspeed> = #3 (= 10 lspeed: units/sec)
#<revs> = #4 (= 1 +/- Revolutions)
#<c> = #<_c> ;starting value
#<ctarget> = [#<c> + #<revs> * 360]
#<arate> = [#<aspeed> * 360]
m111 ;clear prior info messages
o<if1> if [#<radius> EQ 0]
#<ref_radius> = #<_x> ;use current x
o<if1> else
#<ref_radius> = #<radius>
f#<lspeed> g1 x#<ref_radius>
o<if1> endif
(debug, ref_radius=#<ref_radius>)
m68 e0 q#<ref_radius> ; set ref_radius for eoffsets
f#<arate> g1 c#<ctarget> ; circles at arate
o<circles> endsub

View File

@@ -0,0 +1,104 @@
# Notes:
# 1) [AXIS_n]OFFSET_AV_RATIO= controls external offsets a,v
# Allowed values are 0.0 <= OFFSET_AV_RATIO <= 0.9
# Value of 0.0 disables external offsets
# Disallowed values are superseded with msg to stdout
# 2) Immediate homing herein
# ([JOINT_*]HOME_SEQUENCE=0, other HOME_* items omitted)
# 3) uncomment [JOINT_0]HOME_SEARCH_VEL,HOME_LATCH_VEL
# to demonstrate non-zero joint.0.motor-offset
[APPLICATIONS]
APP = sim_pin --title "MPG Simulator" \
e:xcounts \
e:ycounts \
e:zcounts
# make sure [xyz]:ecounts are zero if machine off:
APP = eoffsets_monitor.tcl
[HAL]
HALUI = halui
HALFILE = LIB:basic_sim.tcl -no_use_hal_manualtoolchange
HALFILE = eoffsets.hal
POSTGUI_HALFILE = eoffsets_panel.hal
[EMC]
MACHINE = External Offsets Demo
VERSION = 1.1
[DISPLAY]
PYVCP = eoffsets_panel.xml
DISPLAY = axis
POSITION_OFFSET = RELATIVE
POSITION_FEEDBACK = ACTUAL
MAX_LINEAR_VELOCITY = 2
OPEN_FILE = ./eoffsets.ngc
TKPKG = Ngcgui 1.0
NGCGUI_FONT = Helvetica -12 normal
NGCGUI_SUBFILE = queuebuster.ngc
[TASK]
TASK = milltask
CYCLE_TIME = 0.001
[RS274NGC]
SUBROUTINE_PATH = .
USER_M_PATH = .
PARAMETER_FILE = sim.var
[EMCIO]
TOOL_TABLE = eoffset.tbl
[EMCMOT]
EMCMOT = motmod
SERVO_PERIOD = 1000000
[TRAJ]
COORDINATES = XYZ
LINEAR_UNITS = inch
ANGULAR_UNITS = degree
[KINS]
JOINTS = 3
KINEMATICS = trivkins coordinates=XYZ
[AXIS_X]
OFFSET_AV_RATIO = 0.2
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
MIN_LIMIT = -0.1
MAX_LIMIT = 10
[AXIS_Y]
OFFSET_AV_RATIO = 0.2
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
MIN_LIMIT = -0.1
MAX_LIMIT = 5
[AXIS_Z]
OFFSET_AV_RATIO = 0.2
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
MIN_LIMIT = -1
MAX_LIMIT = 1
[JOINT_0]
TYPE = LINEAR
HOME_SEQUENCE = 0
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
[JOINT_1]
TYPE = LINEAR
HOME_SEQUENCE = 0
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
[JOINT_2]
TYPE = LINEAR
HOME_SEQUENCE = 0
MAX_VELOCITY = 2
MAX_ACCELERATION = 20

View File

@@ -0,0 +1,35 @@
#<xmax> = 9
#<ymax> = 4
#<zdepth> = 0
#<feed> = 20
#<x1> = [0.25 * #<xmax>]
#<y1> = [0.66 * #<ymax>]
#<z1> = 0
#<x2> = [0.50 * #<xmax>]
#<y2> = 0
#<z2> = -.5
#<x3> = [0.75 * #<xmax>]
#<y3> = 0
#<z3> = 0
#<x4> = #<x3>
#<y4> = #<y1>
#<z4> = 0
m111 ;clear notifications
g61
g0x0y0z0
f #<feed>
g1 x #<x1> y #<y1> z #<z1>
g1 x #<x2> y #<y2> z #<z2>
g1 x #<x3> y #<y3> z #<z3>
g1 x #<x4> y #<y4> z #<z4>
g1 x #<xmax> y #<ymax>
g0x0y0z0
m111 ;clear notifications
m2

View File

@@ -0,0 +1,86 @@
# Notes:
# 1) [AXIS_n]OFFSET_AV_RATIO= controls external offsets a,v
# Allowed values are 0.0 <= OFFSET_AV_RATIO <= 0.9
# Value of 0.0 disables external offsets
# Disallowed values are superseded with msg to stdout
# 2) Immediate homing herein
# ([JOINT_*]HOME_SEQUENCE=0, other HOME_* items omitted)
# 3) uncomment [JOINT_0]HOME_SEARCH_VEL,HOME_LATCH_VEL
# to demonstrate non-zero joint.0.motor-offset
[HAL]
HALUI = halui
HALFILE = LIB:basic_sim.tcl
POSTGUI_HALFILE = jwp_z_panel.hal
[EMC]
MACHINE = Jog-While-Pause Z Demo
VERSION = 1.1
[DISPLAY]
PYVCP = jwp_z_panel.xml
DISPLAY = axis
POSITION_OFFSET = RELATIVE
POSITION_FEEDBACK = ACTUAL
MAX_LINEAR_VELOCITY = 2
OPEN_FILE = ./jwp_z.ngc
[TASK]
TASK = milltask
CYCLE_TIME = 0.001
[RS274NGC]
USER_M_PATH = .
PARAMETER_FILE = sim.var
[EMCMOT]
EMCMOT = motmod
SERVO_PERIOD = 1000000
[TRAJ]
COORDINATES = XYZ
LINEAR_UNITS = inch
ANGULAR_UNITS = degree
[KINS]
JOINTS = 3
KINEMATICS = trivkins coordinates=XYZ
[AXIS_X]
MIN_LIMIT = -1
MAX_LIMIT = 11
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
[AXIS_Y]
MIN_LIMIT = -1
MAX_LIMIT = 5
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
[AXIS_Z]
# Note: low OFFSET_AV_RATIO:
OFFSET_AV_RATIO = 0.05
MIN_LIMIT = -1
MAX_LIMIT = 1
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
[JOINT_0]
TYPE = LINEAR
HOME_SEQUENCE = 0
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
[JOINT_1]
TYPE = LINEAR
HOME_SEQUENCE = 0
MAX_VELOCITY = 2
MAX_ACCELERATION = 20
[JOINT_2]
TYPE = LINEAR
HOME_SEQUENCE = 0
MAX_VELOCITY = 2
MAX_ACCELERATION = 20

View File

@@ -0,0 +1,35 @@
#<xmax> = 10
#<ymax> = 5
#<zsafe> = 0.5
#<feed> = 100
#<x1> = [0.25 * #<xmax>]
#<y1> = [0.66 * #<ymax>]
#<x2> = [0.50 * #<xmax>]
#<y2> = [0.33 * #<ymax>]
#<x3> = [0.75 * #<xmax>]
#<y3> = [0.33 * #<ymax>]
m111 ;clear notifications
g61
g0 x0 y0 z#<zsafe>
(debug, Enter offsets only when paused, s to continue)
m0
m111
f #<feed>
g0 z0
g1 x #<x1> y #<y1>
g1 x #<x2> y #<y2>
g1 x #<x3> y #<y3>
g1 x #<xmax> y #<ymax>
g0 z#<zsafe>
(debug, Program End: Clear offsets, s to continue)
m0
g0 x0 y0
m111 ;clear notifications
m2

View File

@@ -0,0 +1,87 @@
[APPLICATIONS]
DELAY = 3
# For assessing system accel/vel:
# (particularly when using func3 square wave modulation)
APP = sim_pin ini.x.max_acceleration ini.x.max_velocity
[HAL]
HALUI = halui
HALFILE = LIB:basic_sim.tcl
HALFILE = opa.hal
POSTGUI_HALFILE = opa_panel.hal
[EMC]
MACHINE = External Offset(X) per Angle(C) Demo
VERSION = 1.1
[DISPLAY]
PYVCP = opa_panel.xml
DISPLAY = axis
OPEN_FILE = opa_demo.ngc
GEOMETRY = !CXZ
POSITION_OFFSET = RELATIVE
POSITION_FEEDBACK = ACTUAL
MAX_LINEAR_VELOCITY = 2
MAX_ANGULAR_VELOCITY = 60
TKPKG = Ngcgui 1.0
NGCGUI_FONT = Helvetica -12 normal
NGCGUI_SUBFILE = circles.ngc
[TASK]
TASK = milltask
CYCLE_TIME = 0.001
[RS274NGC]
SUBROUTINE_PATH = .
USER_M_PATH = .
PARAMETER_FILE = sim.var
[EMCIO]
TOOL_TABLE = eoffset.tbl
[EMCMOT]
EMCMOT = motmod
SERVO_PERIOD = 1000000
[TRAJ]
COORDINATES = XZC
LINEAR_UNITS = inch
ANGULAR_UNITS = degree
[KINS]
JOINTS = 3
KINEMATICS = trivkins coordinates=XZC
[AXIS_X]
OFFSET_AV_RATIO = 0.5
MAX_VELOCITY = 5
# increase MAX_ACCELERATION to improve
# waveform tracking:
MAX_ACCELERATION = 50
[AXIS_Z]
MAX_VELOCITY = 5
MAX_ACCELERATION = 10
[AXIS_C]
MAX_VELOCITY = 60
MAX_ACCELERATION = 600
[JOINT_0]
TYPE = LINEAR
HOME_SEQUENCE = 0
MAX_VELOCITY = 5
MAX_ACCELERATION = 50
[JOINT_1]
TYPE = LINEAR
HOME_SEQUENCE = 0
MAX_VELOCITY = 5
MAX_ACCELERATION = 10
[JOINT_2]
TYPE = ANGULAR
HOME_SEQUENCE = 0
MAX_VELOCITY = 60
MAX_ACCELERATION = 600

View File

@@ -0,0 +1,15 @@
; opa_demo.ngc:
; invoke subroutine circles.ngc for multiple circles (#<revs>)
; with:
; x = #<radius>
; z = #<_z>
#<radius> = 1
#<z> = #<_z> ; current value
#<aspeed> = 8 ; angular speed (RPM)
#<lspeed> = 10 ; linear speed (units/minute)
#<revs> = 100
g0 x#<radius> z#<z>
o<circles> call [#<radius>][#<aspeed>][#<lspeed>][#<revs>]
m2

File diff suppressed because it is too large Load Diff