From 19687c3268d76d25173801576b1d12f39e59fc75 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Mon, 8 Jun 2026 14:16:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E8=A7=84=E5=88=92=E6=8C=81=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 trajectory planner WASM 构建和 Node smoke,覆盖线段、圆弧、队列规划探针,并纳入 host 聚合验证。 --- wasm-port/docs/compatibility-validation.md | 22 +- wasm-port/docs/source-reuse-map.md | 17 +- .../core/linuxcnc_wrap/linuxcnc_tp_wasm.c | 442 ++++++++++++++++++ wasm-port/tests/host/verify_host_smokes.sh | 2 + wasm-port/tests/wasm/node/verify_tp_wasm.mjs | 83 ++++ wasm-port/tests/wasm/node/verify_tp_wasm.sh | 9 + wasm-port/tools/build_tp_wasm.sh | 108 +++++ 7 files changed, 671 insertions(+), 12 deletions(-) create mode 100644 wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c create mode 100644 wasm-port/tests/wasm/node/verify_tp_wasm.mjs create mode 100755 wasm-port/tests/wasm/node/verify_tp_wasm.sh create mode 100755 wasm-port/tools/build_tp_wasm.sh diff --git a/wasm-port/docs/compatibility-validation.md b/wasm-port/docs/compatibility-validation.md index e0fca6e..f7bd154 100644 --- a/wasm-port/docs/compatibility-validation.md +++ b/wasm-port/docs/compatibility-validation.md @@ -24,6 +24,12 @@ The current WASM interpreter-core smoke validation command is: wasm-port/tests/wasm/node/verify_interp_wasm.sh ``` +The current WASM trajectory-planner smoke validation command is: + +```bash +wasm-port/tests/wasm/node/verify_tp_wasm.sh +``` + The current OPFS host-boundary validation command is: ```bash @@ -110,6 +116,13 @@ negative G-code fixtures into the Emscripten filesystem and verifies their LinuxCNC-produced error text through the `Interp::open()`/`read()`/`execute()` file path. +The WASM trajectory-planner smoke script builds +`build/wasm/tp/linuxcnc_tp.js` and `linuxcnc_tp.wasm` from vendored LinuxCNC +TP, TC, TC queue, spherical-arc, blendmath, S-curve, Ruckig-wrapper, C Ruckig +support, emcpose, and posemath source files. It loads the module in Node and +calls the exported TP probe C ABI to validate the same LinuxCNC linear, arc, +and queued-line planner calls covered by the native `linuxcnc_tp_api_probe`. + The OPFS host-boundary script validates the JavaScript file-service adapter with a Node mock of the browser File System Access handles. It covers nested directory creation, text save/load, missing file behavior, invalid relative @@ -172,9 +185,9 @@ override INI-derived names before OPFS text is copied into the LinuxCNC-backed WASM filesystem, and that absent INI file-name values use the default OPFS parameter/tool-table paths in a real browser session. -The aggregate host smoke script builds the INI and interpreter-core WASM -artifacts once, then runs the Node WASM smokes, the Node OPFS mock smoke, and -the Chromium browser smokes. +The aggregate host smoke script builds the INI, interpreter-core, and +trajectory-planner WASM artifacts once, then runs the Node WASM smokes, the +Node OPFS mock smoke, and the Chromium browser smokes. ## Source Coverage @@ -224,10 +237,11 @@ 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, including LinuxCNC-backed boolean conversion and machine-session file-name string lookup. | | `tests/wasm/node/verify_interp_wasm.sh` | Validates the initial interpreter-core WASM module can be built from vendored LinuxCNC interpreter source, loaded through the interpreter JS SDK, run the first fixture group through `Interp::execute()` and selected file fixtures through `Interp::open()`/`read()`/`execute()`, match the native canonical event plus required state readback fixtures, and run parameter-file restore/save through vendored LinuxCNC `Interp::restore_parameters()` and `Interp::save_parameters()`. | +| `tests/wasm/node/verify_tp_wasm.sh` | Validates a standalone trajectory-planner WASM module can be built from vendored LinuxCNC TP/TC/Ruckig support source, loaded in Node, and run the same linear, arc, and queued-line planner probe paths covered by the native TP harness. | | `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter, path model, session snapshot store including custom filenames and envelope/path rejection paths, machine file store, G-code text store including filename rejection paths, OPFS-to-WASM parameter/tool-table bridges, and grouped machine-session loading without moving file persistence, parameter semantics, or tool-table semantics into the WASM core. | | `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, INI/interpreter WASM module loading, LinuxCNC-backed INI machine-session file-name string lookup, OPFS text-file round trip, generic session snapshot round trip plus custom filename and envelope/path rejection paths, machine file text round trip, G-code text round trip plus filename rejection paths, and the INI panel UI's machine-session load with default OPFS parameter/tool-table file mapping, G-code run, and canonical-event display paths in a real browser runtime. | | `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 direct browser SDK and OPFS-backed parameter-file restore/save plus non-random/random 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. | +| `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 diff --git a/wasm-port/docs/source-reuse-map.md b/wasm-port/docs/source-reuse-map.md index 7a4154e..99ed2db 100644 --- a/wasm-port/docs/source-reuse-map.md +++ b/wasm-port/docs/source-reuse-map.md @@ -36,14 +36,14 @@ Current validation is intentionally mechanical: | INI parsing | `src/emc/ini/inifile.cc`, `inifile.h`, `inifile.hh` | Copy unchanged | Native file IO remains LinuxCNC-style in the vendored parser; browser OPFS integration remains outside this layer; `runtime/sdk/src/index.js` exports the INI SDK wrapper around the generated WASM C ABI, including LinuxCNC `iniFindBool()` for machine-session flags and LinuxCNC string lookup for INI-declared `[RS274NGC]PARAMETER_FILE` and `[EMCIO]TOOL_TABLE` machine file names | Vendor byte sync, `linuxcnc_ini_probe`, `linuxcnc_inifile_source_probe`, `tests/wasm/node/verify_ini_wasm.sh` boolean plus file-name lookup assertions, `tests/browser/verify_ini_panel_browser.sh`, `tests/host/verify_host_smokes.sh` | | RTAPI compatibility headers | `src/rtapi/rtapi_*.h` in the manifest | Copy unchanged plus standalone shim include path | `runtime/core/shims/rtapi.h` supplies the minimal standalone RTAPI surface needed by vendored code | Vendor byte sync, compile coverage through dependent source probes | | Canon/NML-facing interpreter types | `src/emc/nml_intf/canon*.hh`, `emctool.h`, `interp_return.hh`, `motion_types.h`, `emcpose.*`, `emcpos.h`, `debugflags.h`, `src/emc/linuxcnc.h` | Copy unchanged plus narrow standalone status shim | NML transport is not ported; `runtime/core/shims/nml_intf/emc.hh` exposes only the `emcStatus` machine-units status edge currently needed by vendored interpreter conversion and initialization code | Vendor byte sync, dependent source probes, `linuxcnc_emc_status_probe`, `linuxcnc_tp_api_probe`, interpreter harnesses, `tests/wasm/node/verify_interp_wasm.sh` and `tests/browser/verify_interp_browser.sh` `Interp::init()` machine-unit assertions | -| Motion state headers | `src/emc/motion/state_tag.h`, `emcmotcfg.h`, `simple_tp.h`, `motion.h`, `mot_priv.h`, `axis.h` | Copy unchanged | Realtime motion process is not ported; standalone probes seed the small motion status/config state required by TP calls | Vendor byte sync, `linuxcnc_tp_api_probe` | +| Motion state headers | `src/emc/motion/state_tag.h`, `emcmotcfg.h`, `simple_tp.h`, `motion.h`, `mot_priv.h`, `axis.h` | Copy unchanged | Realtime motion process is not ported; standalone probes seed the small motion status/config state required by TP calls | Vendor byte sync, `linuxcnc_tp_api_probe`, `tests/wasm/node/verify_tp_wasm.sh` | | Identity/trivial kinematics | `src/emc/kinematics/kinematics.h`, `cubic.h`, `kins_util.c`, `trivkins.c` | Copy unchanged | HAL component lifecycle and RTAPI module metadata are replaced by standalone shims; forward/inverse mapping behavior remains LinuxCNC source | Vendor byte sync, per-file source probes, `linuxcnc_kinematics_probe` | | 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` | | 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` | -| Trajectory planner | `src/emc/tp/tp.c`, `tc.c`, `tcq.c`, `spherical_arc.c`, `blendmath.c`, `sp_scurve.c`, `ruckig_wrapper.c`, plus matching `*.h` files | Copy unchanged | Native realtime scheduling and motion process state are replaced by standalone probe setup | Vendor byte sync, per-file source probes, `linuxcnc_tp_api_probe` | -| Ruckig C planner support | Selected `src/emc/tp/cruckig/*.c` and `*.h` files in the manifest | Copy unchanged | Used as LinuxCNC planner support code through vendored TP sources | Vendor byte sync, per-file source probes | -| Posemath | `src/libnml/posemath/posemath.cc`, `_posemath.c`, `gomath.c`, `sincos.c`, and matching headers | Copy unchanged | `gomath.c` is compiled as C; `rtapi.h` shim is C/C++ compatible for this boundary | Vendor byte sync, per-file source probes, TP probe | +| Trajectory planner | `src/emc/tp/tp.c`, `tc.c`, `tcq.c`, `spherical_arc.c`, `blendmath.c`, `sp_scurve.c`, `ruckig_wrapper.c`, plus matching `*.h` files | Copy unchanged | Native realtime scheduling and motion process state are replaced by standalone probe setup; the WASM TP probe uses the same deterministic status/config boundary and calls vendored TP APIs through a narrow C ABI | Vendor byte sync, per-file source probes, `linuxcnc_tp_api_probe`, `tests/wasm/node/verify_tp_wasm.sh` | +| Ruckig C planner support | Selected `src/emc/tp/cruckig/*.c` and `*.h` files in the manifest | Copy unchanged | Used as LinuxCNC planner support code through vendored TP sources | Vendor byte sync, per-file source probes, `tests/wasm/node/verify_tp_wasm.sh` | +| Posemath | `src/libnml/posemath/posemath.cc`, `_posemath.c`, `gomath.c`, `sincos.c`, and matching headers | Copy unchanged | `gomath.c` is compiled as C; `rtapi.h` shim is C/C++ compatible for this boundary | Vendor byte sync, per-file source probes, TP native and WASM probes | | 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; single-axis rotary indexer lock/unlock dispatch remains vendored LinuxCNC `issue_straight_index()` behavior | Vendor byte sync, per-file source probes, canonical fixture suite, no standalone `Interp::convert_g()` guard, `linuxcnc_indexer_harness`, `tests/wasm/node/verify_interp_wasm.sh` and `tests/browser/verify_interp_browser.sh` rotary-indexer assertions | | 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`, `tests/wasm/node/verify_interp_wasm.sh` and `tests/browser/verify_interp_browser.sh` `Interp::synch()` tool-slot assertions plus `Interp::init_named_parameters()`/`find_named_param()` named-parameter lookup assertions | @@ -71,14 +71,15 @@ Current validation is intentionally mechanical: userspace genser flows are not yet established. - Cutter compensation positive motion and negative interpreter paths are fixture-covered through vendored `interp_convert.cc` and `interp_queue.cc`. -- Browser/WASM C ABI and JS SDK layers are now present for the INI parser and - the current interpreter-core smoke scope. The interpreter SDK is a thin +- Browser/WASM C ABI and JS SDK layers are now present for the INI parser, the + current interpreter-core smoke scope, and a trajectory-planner TP probe. 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` and non-random/random 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. + tool-table semantics. The TP WASM probe calls vendored LinuxCNC planner APIs + directly and does not expose a browser planner SDK yet; 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, diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c new file mode 100644 index 0000000..0aab14d --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c @@ -0,0 +1,442 @@ +#include +#include +#include +#include + +#include + +#include "emc/motion/motion.h" +#include "emc/nml_intf/motion_types.h" +#include "emc/tp/tp.h" + +static emcmot_status_t status; +static emcmot_config_t config; + +static void dio_write(int index, char value) +{ + (void)index; + (void)value; +} + +static void aio_write(int index, double value) +{ + (void)index; + (void)value; +} + +static void set_rotary_unlock(int joint, int unlock) +{ + (void)joint; + (void)unlock; +} + +static int get_rotary_unlock(int joint) +{ + (void)joint; + return 1; +} + +static double axis_vel_limit(int axis) +{ + return axis >= 0 && axis < EMCMOT_MAX_AXIS ? 10.0 : 0.0; +} + +static double axis_acc_limit(int axis) +{ + return axis >= 0 && axis < EMCMOT_MAX_AXIS ? 20.0 : 0.0; +} + +static void init_motion_state(void) +{ + memset(&status, 0, sizeof(status)); + memset(&config, 0, sizeof(config)); + + config.numJoints = 3; + config.numSpindles = 1; + config.numDIO = 4; + config.numAIO = 4; + config.arcBlendOptDepth = 0; + config.arcBlendEnable = 0; + config.arcBlendFallbackEnable = 0; + config.arcBlendGapCycles = 4; + config.arcBlendRampFreq = 20.0; + config.arcBlendTangentKinkRatio = 0.1; + config.maxFeedScale = 1.0; + + status.net_feed_scale = 1.0; + status.feed_scale = 1.0; + status.rapid_scale = 1.0; + status.enables_new = FS_ENABLED | SS_ENABLED; + status.enables_queued = status.enables_new; + status.spindle_status[0].at_speed = 1; + status.spindle_status[0].direction = 1; + status.spindleSync = 0; + status.jerk = 0.0; + status.planner_type = 0; + + tpMotData(&status, &config); + tpMotFunctions( + dio_write, + aio_write, + set_rotary_unlock, + get_rotary_unlock, + axis_vel_limit, + axis_acc_limit); +} + +static int pose_near(const EmcPose *actual, const EmcPose *expected) +{ + return fabs(actual->tran.x - expected->tran.x) < 1e-9 && + fabs(actual->tran.y - expected->tran.y) < 1e-9 && + fabs(actual->tran.z - expected->tran.z) < 1e-9; +} + +static void append(char *out, size_t size, size_t *offset, const char *text) +{ + if (*offset >= size) { + return; + } + const int written = snprintf(out + *offset, size - *offset, "%s", text); + if (written > 0) { + *offset += (size_t)written; + } +} + +static void append_linear_probe(char *out, size_t size, size_t *offset) +{ + init_motion_state(); + + TP_STRUCT tp; + EmcPose start; + EmcPose end; + struct state_tag_t tag; + memset(&tp, 0, sizeof(tp)); + memset(&start, 0, sizeof(start)); + memset(&end, 0, sizeof(end)); + memset(&tag, 0, sizeof(tag)); + + end.tran.x = 1.0; + + const int create_rc = tpCreate(&tp, TP_DEFAULT_QUEUE_SIZE, 1); + const int set_cycle_rc = tpSetCycleTime(&tp, 0.001); + const int set_pos_rc = tpSetPos(&tp, &start); + const int set_vmax_rc = tpSetVmax(&tp, 1.0, 1.0); + const int set_vlimit_rc = tpSetVlimit(&tp, 1.0); + const int set_amax_rc = tpSetAmax(&tp, 10.0); + const int set_term_rc = tpSetTermCond(&tp, TC_TERM_COND_STOP, 0.0); + const int add_line_rc = tpAddLine( + &tp, + end, + EMC_MOTION_TYPE_FEED, + 1.0, + 1.0, + 10.0, + 100.0, + status.enables_new, + 0, + -1, + tag); + + int cycle_rc = 0; + int cycles = 0; + for (; cycles < 10000 && !tpIsDone(&tp); ++cycles) { + cycle_rc = tpRunCycle(&tp, 1000000); + if (cycle_rc < 0) { + break; + } + } + + EmcPose final_pos; + memset(&final_pos, 0, sizeof(final_pos)); + const int get_pos_rc = tpGetPos(&tp, &final_pos); + + char line[1024]; + snprintf(line, sizeof(line), + "tp_create=%d\n" + "tp_set_cycle_time=%d\n" + "tp_set_pos=%d\n" + "tp_set_vmax=%d\n" + "tp_set_vlimit=%d\n" + "tp_set_amax=%d\n" + "tp_set_term_cond=%d\n" + "tp_add_line=%d\n" + "tp_cycle_rc=%d\n" + "tp_cycles=%d\n" + "tp_get_pos=%d\n" + "tp_done_after_line=%d\n" + "tp_queue_depth=%d\n" + "tp_final_pos=%.12g,%.12g,%.12g\n", + create_rc, + set_cycle_rc, + set_pos_rc, + set_vmax_rc, + set_vlimit_rc, + set_amax_rc, + set_term_rc, + add_line_rc, + cycle_rc, + cycles, + get_pos_rc, + tpIsDone(&tp), + tpQueueDepth(&tp), + final_pos.tran.x, + final_pos.tran.y, + final_pos.tran.z); + append(out, size, offset, line); +} + +static void append_arc_probe(char *out, size_t size, size_t *offset) +{ + init_motion_state(); + + TP_STRUCT tp; + EmcPose start; + EmcPose end; + PmCartesian center; + PmCartesian normal; + struct state_tag_t tag; + memset(&tp, 0, sizeof(tp)); + memset(&start, 0, sizeof(start)); + memset(&end, 0, sizeof(end)); + memset(¢er, 0, sizeof(center)); + memset(&normal, 0, sizeof(normal)); + memset(&tag, 0, sizeof(tag)); + + start.tran.x = 1.0; + end.tran.y = 1.0; + normal.z = 1.0; + + const int create_rc = tpCreate(&tp, TP_DEFAULT_QUEUE_SIZE, 2); + const int set_cycle_rc = tpSetCycleTime(&tp, 0.001); + const int set_pos_rc = tpSetPos(&tp, &start); + const int set_vmax_rc = tpSetVmax(&tp, 1.0, 1.0); + const int set_vlimit_rc = tpSetVlimit(&tp, 1.0); + const int set_amax_rc = tpSetAmax(&tp, 10.0); + const int set_term_rc = tpSetTermCond(&tp, TC_TERM_COND_STOP, 0.0); + const int add_circle_rc = tpAddCircle( + &tp, + end, + center, + normal, + 0, + EMC_MOTION_TYPE_ARC, + 1.0, + 1.0, + 10.0, + 100.0, + status.enables_new, + 0, + tag); + + int cycle_rc = 0; + int cycles = 0; + for (; cycles < 10000 && !tpIsDone(&tp); ++cycles) { + cycle_rc = tpRunCycle(&tp, 1000000); + if (cycle_rc < 0) { + break; + } + } + + EmcPose final_pos; + memset(&final_pos, 0, sizeof(final_pos)); + const int get_pos_rc = tpGetPos(&tp, &final_pos); + const int near_end = pose_near(&final_pos, &end); + + char line[1024]; + snprintf(line, sizeof(line), + "tp_arc_create=%d\n" + "tp_arc_set_cycle_time=%d\n" + "tp_arc_set_pos=%d\n" + "tp_arc_set_vmax=%d\n" + "tp_arc_set_vlimit=%d\n" + "tp_arc_set_amax=%d\n" + "tp_arc_set_term_cond=%d\n" + "tp_add_circle=%d\n" + "tp_arc_cycle_rc=%d\n" + "tp_arc_cycles=%d\n" + "tp_arc_get_pos=%d\n" + "tp_done_after_arc=%d\n" + "tp_arc_queue_depth=%d\n" + "tp_arc_final_pos_near_end=%d\n" + "tp_arc_final_pos=%.12g,%.12g,%.12g\n", + create_rc, + set_cycle_rc, + set_pos_rc, + set_vmax_rc, + set_vlimit_rc, + set_amax_rc, + set_term_rc, + add_circle_rc, + cycle_rc, + cycles, + get_pos_rc, + tpIsDone(&tp), + tpQueueDepth(&tp), + near_end, + final_pos.tran.x, + final_pos.tran.y, + final_pos.tran.z); + append(out, size, offset, line); +} + +static void append_queued_lines_probe(char *out, size_t size, size_t *offset) +{ + init_motion_state(); + + TP_STRUCT tp; + EmcPose start; + EmcPose end1; + EmcPose end2; + struct state_tag_t tag; + memset(&tp, 0, sizeof(tp)); + memset(&start, 0, sizeof(start)); + memset(&end1, 0, sizeof(end1)); + memset(&end2, 0, sizeof(end2)); + memset(&tag, 0, sizeof(tag)); + + end1.tran.x = 1.0; + end2.tran.x = 1.0; + end2.tran.y = 1.0; + + const int create_rc = tpCreate(&tp, TP_DEFAULT_QUEUE_SIZE, 3); + const int set_cycle_rc = tpSetCycleTime(&tp, 0.001); + const int set_pos_rc = tpSetPos(&tp, &start); + const int set_vmax_rc = tpSetVmax(&tp, 1.0, 1.0); + const int set_vlimit_rc = tpSetVlimit(&tp, 1.0); + const int set_amax_rc = tpSetAmax(&tp, 10.0); + const int set_term_rc = tpSetTermCond(&tp, TC_TERM_COND_STOP, 0.0); + const int add_line1_rc = tpAddLine( + &tp, + end1, + EMC_MOTION_TYPE_FEED, + 1.0, + 1.0, + 10.0, + 100.0, + status.enables_new, + 0, + -1, + tag); + const int depth_after_line1 = tpQueueDepth(&tp); + const int add_line2_rc = tpAddLine( + &tp, + end2, + EMC_MOTION_TYPE_FEED, + 1.0, + 1.0, + 10.0, + 100.0, + status.enables_new, + 0, + -1, + tag); + const int depth_after_line2 = tpQueueDepth(&tp); + + int cycle_rc = 0; + int cycles = 0; + for (; cycles < 20000 && !tpIsDone(&tp); ++cycles) { + cycle_rc = tpRunCycle(&tp, 1000000); + if (cycle_rc < 0) { + break; + } + } + + EmcPose final_pos; + memset(&final_pos, 0, sizeof(final_pos)); + const int get_pos_rc = tpGetPos(&tp, &final_pos); + const int near_end = pose_near(&final_pos, &end2); + + char line[1024]; + snprintf(line, sizeof(line), + "tp_queue_create=%d\n" + "tp_queue_set_cycle_time=%d\n" + "tp_queue_set_pos=%d\n" + "tp_queue_set_vmax=%d\n" + "tp_queue_set_vlimit=%d\n" + "tp_queue_set_amax=%d\n" + "tp_queue_set_term_cond=%d\n" + "tp_queue_add_line1=%d\n" + "tp_queue_depth_after_line1=%d\n" + "tp_queue_add_line2=%d\n" + "tp_queue_depth_after_line2=%d\n" + "tp_queue_cycle_rc=%d\n" + "tp_queue_cycles=%d\n" + "tp_queue_get_pos=%d\n" + "tp_done_after_queued_lines=%d\n" + "tp_queue_final_depth=%d\n" + "tp_queue_final_pos_near_end=%d\n" + "tp_queue_final_pos=%.12g,%.12g,%.12g\n", + create_rc, + set_cycle_rc, + set_pos_rc, + set_vmax_rc, + set_vlimit_rc, + set_amax_rc, + set_term_rc, + add_line1_rc, + depth_after_line1, + add_line2_rc, + depth_after_line2, + cycle_rc, + cycles, + get_pos_rc, + tpIsDone(&tp), + tpQueueDepth(&tp), + near_end, + final_pos.tran.x, + final_pos.tran.y, + final_pos.tran.z); + append(out, size, offset, line); +} + +EMSCRIPTEN_KEEPALIVE +char *lctp_run_probe(void) +{ + char *out = (char *)malloc(8192); + if (!out) { + return NULL; + } + out[0] = '\0'; + size_t offset = 0; + + TP_STRUCT tp; + TC_STRUCT tc; + EmcPose pose; + memset(&tp, 0, sizeof(tp)); + memset(&tc, 0, sizeof(tc)); + memset(&pose, 0, sizeof(pose)); + + pose.tran.x = 1.0; + pose.tran.y = 2.0; + pose.tran.z = 3.0; + + char line[512]; + snprintf(line, sizeof(line), + "sizeof_TP_STRUCT=%zu\n" + "sizeof_TC_STRUCT=%zu\n" + "tp_default_queue_size=%d\n" + "tp_err_ok=%d\n" + "tc_linear=%d\n" + "tc_circular=%d\n" + "pose_xyz=%.12g,%.12g,%.12g\n", + sizeof(tp), + sizeof(tc), + TP_DEFAULT_QUEUE_SIZE, + TP_ERR_OK, + TC_LINEAR, + TC_CIRCULAR, + pose.tran.x, + pose.tran.y, + pose.tran.z); + append(out, 8192, &offset, line); + append_linear_probe(out, 8192, &offset); + append_arc_probe(out, 8192, &offset); + append_queued_lines_probe(out, 8192, &offset); + return out; +} + +EMSCRIPTEN_KEEPALIVE +void lctp_free_string(char *value) +{ + free(value); +} diff --git a/wasm-port/tests/host/verify_host_smokes.sh b/wasm-port/tests/host/verify_host_smokes.sh index dcd7d6a..3d7c0c3 100755 --- a/wasm-port/tests/host/verify_host_smokes.sh +++ b/wasm-port/tests/host/verify_host_smokes.sh @@ -5,8 +5,10 @@ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" "$ROOT_DIR/tools/build_ini_panel.sh" "$ROOT_DIR/tools/build_wasm_core.sh" +"$ROOT_DIR/tools/build_tp_wasm.sh" SKIP_INI_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_ini_wasm.sh" SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_interp_wasm.sh" +SKIP_TP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_tp_wasm.sh" "$ROOT_DIR/tests/opfs/node/verify_file_service.sh" SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh" SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_interp_browser.sh" diff --git a/wasm-port/tests/wasm/node/verify_tp_wasm.mjs b/wasm-port/tests/wasm/node/verify_tp_wasm.mjs new file mode 100644 index 0000000..2d4af25 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_tp_wasm.mjs @@ -0,0 +1,83 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; +import assert from "node:assert/strict"; + +import createLinuxCncTpModule from "../../../build/wasm/tp/linuxcnc_tp.js"; + +function verifyExpectedOutput(fixtureName, output, expectedText) { + const expectedLines = expectedText.split("\n").filter(Boolean); + + for (const expectedLine of expectedLines) { + assert.equal( + output.includes(expectedLine), + true, + `${fixtureName}: ${expectedLine}`, + ); + } +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = resolve(__dirname, "../../.."); +const wasmPath = resolve(rootDir, "build/wasm/tp/linuxcnc_tp.wasm"); + +const tp = await createLinuxCncTpModule({ + wasmBinary: readFileSync(wasmPath), + print() {}, + printErr(message) { + console.error(message); + }, +}); + +const resultPtr = tp._lctp_run_probe(); +assert.notEqual(resultPtr, 0); +try { + const output = tp.UTF8ToString(resultPtr); + verifyExpectedOutput( + "tp_wasm_probe", + output, + [ + "tp_default_queue_size=32", + "tp_err_ok=0", + "tc_linear=1", + "tc_circular=2", + "pose_xyz=1,2,3", + "tp_create=0", + "tp_set_cycle_time=0", + "tp_set_pos=0", + "tp_set_vmax=0", + "tp_set_vlimit=0", + "tp_set_amax=0", + "tp_set_term_cond=0", + "tp_add_line=0", + "tp_cycle_rc=4", + "tp_get_pos=0", + "tp_done_after_line=1", + "tp_queue_depth=0", + "tp_final_pos=1,0,0", + "tp_arc_create=0", + "tp_add_circle=0", + "tp_arc_cycle_rc=4", + "tp_arc_get_pos=0", + "tp_done_after_arc=1", + "tp_arc_queue_depth=0", + "tp_arc_final_pos_near_end=1", + "tp_queue_create=0", + "tp_queue_add_line1=0", + "tp_queue_depth_after_line1=1", + "tp_queue_add_line2=0", + "tp_queue_depth_after_line2=2", + "tp_queue_cycle_rc=4", + "tp_queue_get_pos=0", + "tp_done_after_queued_lines=1", + "tp_queue_final_depth=0", + "tp_queue_final_pos_near_end=1", + "tp_queue_final_pos=1,1,0", + ].join("\n"), + ); +} finally { + tp._lctp_free_string(resultPtr); +} + +console.log("tp_wasm_node_smoke=ok"); diff --git a/wasm-port/tests/wasm/node/verify_tp_wasm.sh b/wasm-port/tests/wasm/node/verify_tp_wasm.sh new file mode 100755 index 0000000..0bc0651 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_tp_wasm.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" + +if [[ "${SKIP_TP_BUILD:-0}" != "1" ]]; then + "$ROOT_DIR/tools/build_tp_wasm.sh" +fi +node "$ROOT_DIR/tests/wasm/node/verify_tp_wasm.mjs" diff --git a/wasm-port/tools/build_tp_wasm.sh b/wasm-port/tools/build_tp_wasm.sh new file mode 100755 index 0000000..f25275e --- /dev/null +++ b/wasm-port/tools/build_tp_wasm.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +VENDOR_DIR="$ROOT_DIR/vendor/linuxcnc" +WRAP_DIR="$ROOT_DIR/runtime/core/linuxcnc_wrap" +SHIM_DIR="$ROOT_DIR/runtime/core/shims" +INCLUDE_DIR="$ROOT_DIR/runtime/core/include" +OUT_DIR="$ROOT_DIR/build/wasm/tp" +OBJ_DIR="$OUT_DIR/obj" + +source "$ROOT_DIR/tools/wasm_incremental_build_lib.sh" + +mkdir -p "$OUT_DIR" "$OBJ_DIR" + +TARGET="linuxcnc_tp_wasm" +: > "$OUT_DIR/$TARGET.stdout.log" +: > "$OUT_DIR/$TARGET.stderr.log" + +COMMON_FLAGS=( + -O2 + -D_GNU_SOURCE + -DM_PI=3.14159265358979323846 + -I"$WRAP_DIR" + -I"$SHIM_DIR" + -I"$INCLUDE_DIR" + -I"$VENDOR_DIR/src" + -I"$VENDOR_DIR/src/rtapi" + -I"$VENDOR_DIR/src/emc" + -I"$VENDOR_DIR/src/emc/nml_intf" + -I"$VENDOR_DIR/src/emc/motion" + -I"$VENDOR_DIR/src/emc/tp" + -I"$VENDOR_DIR/src/emc/kinematics" + -I"$VENDOR_DIR/src/libnml/posemath" +) + +C_FLAGS=( + -std=gnu11 + "${COMMON_FLAGS[@]}" +) + +CXX_FLAGS=( + -std=c++20 + -fpermissive + "${COMMON_FLAGS[@]}" +) + +SOURCES=( + "$VENDOR_DIR/src/emc/tp/tp.c" + "$VENDOR_DIR/src/emc/tp/tc.c" + "$VENDOR_DIR/src/emc/tp/tcq.c" + "$VENDOR_DIR/src/emc/tp/spherical_arc.c" + "$VENDOR_DIR/src/emc/tp/blendmath.c" + "$VENDOR_DIR/src/emc/tp/sp_scurve.c" + "$VENDOR_DIR/src/emc/tp/ruckig_wrapper.c" + "$VENDOR_DIR/src/emc/tp/cruckig/block.c" + "$VENDOR_DIR/src/emc/tp/cruckig/brake.c" + "$VENDOR_DIR/src/emc/tp/cruckig/calculator.c" + "$VENDOR_DIR/src/emc/tp/cruckig/cruckig.c" + "$VENDOR_DIR/src/emc/tp/cruckig/input_parameter.c" + "$VENDOR_DIR/src/emc/tp/cruckig/output_parameter.c" + "$VENDOR_DIR/src/emc/tp/cruckig/profile.c" + "$VENDOR_DIR/src/emc/tp/cruckig/roots.c" + "$VENDOR_DIR/src/emc/tp/cruckig/trajectory.c" + "$VENDOR_DIR/src/emc/tp/cruckig/position_first_step1.c" + "$VENDOR_DIR/src/emc/tp/cruckig/position_first_step2.c" + "$VENDOR_DIR/src/emc/tp/cruckig/position_second_step1.c" + "$VENDOR_DIR/src/emc/tp/cruckig/position_second_step2.c" + "$VENDOR_DIR/src/emc/tp/cruckig/position_third_step1.c" + "$VENDOR_DIR/src/emc/tp/cruckig/position_third_step2.c" + "$VENDOR_DIR/src/emc/tp/cruckig/velocity_second_step1.c" + "$VENDOR_DIR/src/emc/tp/cruckig/velocity_second_step2.c" + "$VENDOR_DIR/src/emc/tp/cruckig/velocity_third_step1.c" + "$VENDOR_DIR/src/emc/tp/cruckig/velocity_third_step2.c" + "$VENDOR_DIR/src/emc/nml_intf/emcpose.c" + "$VENDOR_DIR/src/libnml/posemath/posemath.cc" + "$VENDOR_DIR/src/libnml/posemath/_posemath.c" + "$VENDOR_DIR/src/libnml/posemath/sincos.c" + "$WRAP_DIR/linuxcnc_tp_wasm.c" +) + +OBJECTS=() +for source in "${SOURCES[@]}"; do + obj="$(source_object_path "$source" "$ROOT_DIR" "$OBJ_DIR")" + OBJECTS+=("$obj") + case "$source" in + *.cc|*.cpp|*.cxx) + compile_wasm_object "$TARGET" "$source" "$obj" "$OUT_DIR" "${CXX_FLAGS[@]}" + ;; + *) + compile_wasm_object "$TARGET" "$source" "$obj" "$OUT_DIR" "${C_FLAGS[@]}" + ;; + esac +done + +link_wasm_module \ + "$TARGET" \ + "$OUT_DIR/linuxcnc_tp.js" \ + "$OUT_DIR" \ + -O2 \ + "${OBJECTS[@]}" \ + -s MODULARIZE=1 \ + -s EXPORT_ES6=1 \ + -s ENVIRONMENT=web,node \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s NO_EXIT_RUNTIME=1 \ + -s EXPORTED_FUNCTIONS='["_malloc","_free","_lctp_run_probe","_lctp_free_string"]' \ + -s EXPORTED_RUNTIME_METHODS='["UTF8ToString"]'