From 492d862e455de3366b56b2657eb07c4b7543b471 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Tue, 1 Sep 2026 23:47:34 +0800 Subject: [PATCH] Verify XYZAC panel button parity --- .../xyzac-panel-button-parity.report.json | 46 ++++++ .../xyzac-panel-button-parity-test-report.md | 152 ++++++++++++++++++ wasm-core/CMakeLists.txt | 1 + wasm-core/include/sim_io.h | 2 + wasm-core/src/sim_io.c | 32 ++++ wasm-core/tests/sim_io_test.c | 7 + web/react/src/App.tsx | 2 +- web/worker/sim-session.mjs | 12 +- web/worker/test_xyzac_3d_chips.mjs | 11 +- web/worker/test_xyzac_panel_buttons.mjs | 144 +++++++++++++++++ web/worker/test_xyzac_trt_io_authority.mjs | 9 ++ web/worker/wasm-xyzac-io.mjs | 11 +- 12 files changed, 417 insertions(+), 12 deletions(-) create mode 100644 coverage/xyzac-panel-button-parity.report.json create mode 100644 doc-rtcp/xyzac-panel-button-parity-test-report.md create mode 100644 web/worker/test_xyzac_panel_buttons.mjs diff --git a/coverage/xyzac-panel-button-parity.report.json b/coverage/xyzac-panel-button-parity.report.json new file mode 100644 index 0000000..37e4789 --- /dev/null +++ b/coverage/xyzac-panel-button-parity.report.json @@ -0,0 +1,46 @@ +{ + "schema": "xyzac-panel-button-parity.v1", + "status": "verified_with_scope_gaps", + "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", + "case": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", + "authority": "linuxcnc-wasm-worker", + "source_control_inventory": { + "menus": 80, + "toolbar": 20, + "keyboard": 10, + "total": 110, + "source_sha256": "c9f0b4dc686717b37565f85fb0ab8cc8ded9cfa0028cc0aaaccdbcf808c4c524" + }, + "button_test": { + "command": "node --experimental-strip-types web/worker/test_xyzac_panel_buttons.mjs", + "verified_paths": 94, + "expected_error_paths": 8, + "blocked_by_abi_paths": 7, + "output_sha256": "4447f37d8db6724fcd43518e8ddcd18d54e40faaedc77be9ea01abdc573226bc" + }, + "long_program": { + "program": "linuxcnc/nc_files/3D_Chips.ngc", + "source_length": 200509, + "line_count": 4711, + "preview_segments": 4684, + "fixed_tick_samples": 1500, + "execution_line": 168, + "execution_trace_entries": 166, + "motion_authority": "wasm", + "output_sha256": "493ed6c901d18fae8fa02d0dc26ee0575ce78c53557351a468fc1df622e48ea4" + }, + "native_tests": { + "wasm_build": "passed", + "ctest": "22/22 passed" + }, + "release_status": "STOP_RELEASE", + "scope_gaps": [ + "per_joint_home_mask", + "override_limits_command", + "wasm_jog_speed_and_max_velocity_controls", + "block_delete_and_optional_pause_interpreter_state", + "interactive_tool_touch_off_G10_flow", + "desktop_tool_editor_and_quit_host_boundary" + ], + "report_document": "doc-rtcp/xyzac-panel-button-parity-test-report.md" +} diff --git a/doc-rtcp/xyzac-panel-button-parity-test-report.md b/doc-rtcp/xyzac-panel-button-parity-test-report.md new file mode 100644 index 0000000..3c880f6 --- /dev/null +++ b/doc-rtcp/xyzac-panel-button-parity-test-report.md @@ -0,0 +1,152 @@ +# XYZAC TRT 面板按钮对标测试报告 + +## 结论 + +本报告针对当前选定案例 +`linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini`, +按 LinuxCNC AXIS 的命令、门控、状态和错误语义执行了一轮完整面板验收。 + +`selected_case_status: VERIFIED_WITH_SCOPE_GAPS` + +`release_status: STOP_RELEASE` + +这表示已执行并通过选定 XYZAC 案例的 WASM/Worker 控制闭环,但不宣称整个 LinuxCNC 或所有 AXIS 配置已经数值完全对标。 + +## 对标输入 + +| 作用 | LinuxCNC 原始输入 | Web/WASM 输入 | +| --- | --- | --- | +| AXIS 菜单、快捷键、状态轮询 | `linuxcnc/src/emc/usr_intf/axis/scripts/axis.py`、`linuxcnc/share/axis/tcl/axis.tcl` | `web/react/src/App.tsx`、`web/app/axis-contract.mjs` | +| 程序/暂停/恢复/停止 | `axis.py:task_run/task_step/task_pause/task_resume/task_stop` | `PROGRAM_RUN/STEP/PAUSE/RESUME/ABORT` -> `SimTask` + LinuxCNC source TP | +| JOG/HOME | `axis.py:jog_on/jog_off/home_all_joints/home_joint` | `JOG_START/JOG_STOP/HOME` -> `WasmLinuxcncTp/WasmHomeLimit` | +| MDI/Touch Off | `axis.py:send_mdi_command/touch_off_system/touch_off_tool` | `MDI_EXECUTE` -> `linuxcnc-wasm-sim_program` | +| Feed/rapid/spindle override | `axis.py:set_feedrate/set_rapidrate/set_spindlerate` | `SET_OVERRIDE` -> WASM TP/IO | +| 主轴、刹车、冷却、刀具、探针 | `axis.py:spindle/brake/mist/flood/change_tool` | `SPINDLE_COMMAND/SPINDLE_BRAKE/COOLANT_COMMAND/TOOL_*/PROBE_CONFIG` -> WASM IO | +| 预览、实时轨迹、清屏 | `linuxcnc/lib/python/rs274/glcanon.py` | `XyzacToolPreview`,只投影 WASM preview/motion trace | +| XYZAC M428/M429/M430 | `switchkins_postgui.hal`、`remap_subs/428remap.ngc` 等 | `KINEMATICS_SELECT` -> WASM kinematics/switchkins | + +源码输入哈希: + +```text +axis.py 328c038a1e8fefd952e53c8e398e3fa98b04602351c44318edd1b044e8f63ffa +glcanon.py 0dfb20f1fdc05289894f3aa638357d98e1063fbd53329c723c777399ec7b33b7 +xyzac-trt.ini f985970db293b70c055e4c41fc316d0c16037b5dc7ce07490e18f554e851f811 +source slices c9f0b4dc686717b37565f85fb0ab8cc8ded9cfa0028cc0aaaccdbcf808c4c524 +``` + +AXIS 源控件清单:菜单 80、工具栏 20、快捷键 10,共 110 条,来自 `web/workspace/xyzac-trt-source-slices.mjs`。 + +## 按钮/控件矩阵 + +下表中的“通过”包含正常路径和刻意触发的 LinuxCNC 门控拒绝;拒绝不是失败,必须返回可解释的 gate/error key。 + +| 面板区域 | Web 控件 | LinuxCNC 命令/源函数 | WASM 权威输出 | 结果 | +| --- | --- | --- | --- | --- | +| 会话 | HELLO、SESSION_ATTACH | AXIS 启动/状态轮询 | `sim_protocol`、`SimTask` | 通过 | +| Machine | Emergency Stop、Release E-stop | `General_Halt`、`emcTaskAbort` | WASM IO + Task ESTOP/RESET | 通过 | +| Machine | Power On/Off | `commands.power`、`emcTaskSetState` | `sim_io_set_power` | 通过 | +| Machine | Reset | `commands.reset` | WASM Task/IO reset | 通过 | +| Machine | HOME ALL | `home_all_joints` | `WasmHomeLimit` 五关节回零 | 通过 | +| Machine | Run Startup Sequence | release -> power -> home -> run | Worker 固定顺序 | 通过 | +| Program | Load | `open_file_guts` | WASM interpreter preview | 通过 | +| Program | Run | `task_run` / `AUTO_RUN` | WASM program + source TP | 通过 | +| Program | Pause / Resume | `task_pause` / `task_resume` | WASM interpreter + TP pause/resume | 通过 | +| Program | Stop | `task_stop` / `c.abort` | WASM TP abort,队列清零 | 通过 | +| Program | Restart | `reload_file` / first line | WASM program reload | 通过 | +| Program | Step / Seek | `task_step` / `set_first_line` | WASM interpreter | 通过 | +| JOG | X、Y、Z、A、C +/-、Stop | `jog_plus/jog_minus/jog_on/jog_off` | WASM motion queue | 通过 | +| JOG | 越界/非有限速度 | LinuxCNC motion limit/ferror | WASM 错误 key | 通过拒绝 | +| MDI | Go、历史命令 | `send_mdi_command` | WASM interpreter/canonical | 通过 | +| MDI | 运行中 MDI | `manual_ok` | `mdi_while_program_active` | 通过拒绝 | +| Override | Feed Override 50%/200% | `set_feedrate` | WASM TP feed override | 通过 | +| Override | Feed > 200% | `set_feedrate` range | `motion_override_range` | 通过拒绝 | +| Spindle | CW/CCW/Stop | `spindle_forward_toggle/spindle` | WASM spindle command | 通过 | +| Spindle | +/- | `SPINDLE_INCREASE/DECREASE` | WASM signed delta | 通过 | +| Spindle | Brake | `EMC_SPINDLE_BRAKE_ENGAGE/RELEASE` | WASM IO brake state | 通过 | +| Coolant | Mist/Flood | `mist_toggle/flood_toggle` | WASM device IO | 通过 | +| Tool | Prepare、Prepared、Change、Changed | iocontrol/tool handler | WASM IO tool state | 通过 | +| Probe | Arm、Disarm、Trip | probe canonical/HAL | WASM IO + HAL | 通过 | +| Switchkins | Identity/M429、TCP: XYZAC/M428、User K/M430 | `switchkins_postgui.hal` + remap files | WASM kinematics `active_type` | 通过 | +| Preview | Preview、DRO、路径/实时/限位/速度/偏置/范围 | `glcanon.py` draw layers | WASM pose/joints/toolpath | 通过投影 | +| Preview | 缩放、平移、旋转、原点坐标系 | AXIS view handlers | React/Three display-only state | 通过显示 | +| Diagnostics | 状态、门控、错误、恢复 | AXIS status/error channel | Worker snapshot/ledger | 通过 | + +## 本轮自动化结果 + +聚合用例:`web/worker/test_xyzac_panel_buttons.mjs` + +```json +{ + "schema": "xyzac-panel-button-parity.v1", + "source": "linuxcnc-wasm-worker", + "verified_paths": 94, + "expected_error_paths": 8, + "blocked_by_abi_paths": 7, + "selected_case_release_status": "STOP_RELEASE" +} +``` + +94 条通过路径包括: + +- Machine:HELLO、SESSION_ATTACH、LOAD_WORKSPACE、ESTOP、ESTOP_RESET、POWER、RESET、HOME ALL、启动序列; +- Program:LOAD、RUN、SIM_STEP、PAUSE、SNAPSHOT、RESUME、ABORT/STOP、RESTART、STEP、SEEK、按行恢复; +- JOG:XYZAC 五轴启动、固定 tick 采样和停止; +- MDI:正常 G-code、运行中门控和非法 G-code 错误回显; +- Feed Override:50%、200% 和 201% 拒绝; +- 主轴:CW、signed increase/decrease、Stop、Brake engage/release; +- Coolant、Tool prepare/change ack、Probe arm/disarm、ESTOP recovery; +- M428/M429/M430、VISUAL_CLEAR 和诊断快照。 + +8 条错误路径是刻意验证的拒绝:OFF 状态 Run/HOME/JOG/MDI/M428、Feed 201%、非法 MDI、运行中 MDI。它们均返回 LinuxCNC/WASM 可读原因,没有把拒绝当成成功。 + +3D_Chips 长程序补充验证: + +```text +source_length: 200509 +line_count: 4711 +preview_segments: 4684 +fixed_tick_samples: 1500 +execution_line: 168 +execution_trace_entries: 166 +motion_authority: wasm +``` + +## 当前未闭合项 + +这些项目在本轮明确记录为 `blocked_by_abi`,没有伪造通过: + +1. `Home Axis`:当前 WASM home 状态只暴露 aggregate `homed`,没有 LinuxCNC 每关节 homed mask;当前按钮调用的是统一 home。 +2. `Override Limits`:原始 AXIS `toggle_override_limits` 有独立 motion command;选定 XYZAC WASM 配置目前把 limits 作为配置所有权,尚无等价 command ABI。 +3. `Jog Speed` 和 `Max Velocity` slider:当前是 React 控件状态,尚未接入 WASM jog/max velocity command。 +4. `Block delete` 和 `Optional pause`:当前是 React toggle,尚未写入 WASM interpreter modal/control state。 +5. `Tool Touch Off`:当前按钮发送固定 `G43`;原始 AXIS 先取得用户值,再发送 `G10 L10/L11` 和 `G43`,交互输入流程尚未闭合。 +6. `Edit tool data`、`Quit`:依赖桌面进程/外部编辑器,属于浏览器 `HOST_BOUNDARY`,不在仿真 Web 范围内。 + +## 复现命令 + +```bash +scripts/build-wasm.sh +npm run build --prefix web/react +node --experimental-strip-types web/worker/test_xyzac_panel_buttons.mjs +node --experimental-strip-types web/worker/test_xyzac_button_constraints.mjs +node --experimental-strip-types web/worker/test_xyzac_workflow.mjs +node --experimental-strip-types web/worker/test_xyzac_manual_control.mjs +node --experimental-strip-types web/worker/test_xyzac_feed_override.mjs +node --experimental-strip-types web/worker/test_xyzac_trt_io_authority.mjs +node --experimental-strip-types web/worker/test_xyzac_3d_chips.mjs +bash -n run-5axis-rtcp.sh +``` + +本轮结果:WASM 构建通过,原生 CTest 22/22 通过,XYZAC 聚合按钮测试通过,3D_Chips 固定 tick 长程序测试通过。发布站点 UI 运行/暂停/恢复/停止也已在 Chromium 仿真环境执行;Safari 和真实物理断电不属于本项目范围。 + +## 证据哈希 + +```text +panel_button_test_output 4447f37d8db6724fcd43518e8ddcd18d54e40faaedc77be9ea01abdc573226bc +3d_chips_test_output 493ed6c901d18fae8fa02d0dc26ee0575ce78c53557351a468fc1df622e48ea4 +wasm_io_source 1cb066d185dd2c5bbf435e022c2518483bd44962e5a0303248757b2da51740f7 +wasm_io_c_source 09bce1430d6f71702ca0b3bf29ac50db377a6f232ce0d1a9b2c1eaac7ea2162e +panel_button_test_source 80782fc1f85f88beb48ec3dfaf638a93f2edb912e694840199dbff504477fe74 +``` + +报告对应提交前工作树保留了既有 coverage 大文件和 LinuxCNC 子模块改动;本轮只提交面板 WASM I/O 修正、step 状态修正、聚合测试和本报告。 diff --git a/wasm-core/CMakeLists.txt b/wasm-core/CMakeLists.txt index a1ea1c9..728cc41 100644 --- a/wasm-core/CMakeLists.txt +++ b/wasm-core/CMakeLists.txt @@ -303,6 +303,7 @@ if(EMSCRIPTEN) string(REPLACE "_sim_hal_set_param" "_sim_hal_set_param_scalar','_sim_hal_set_param" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_io_init" "_sim_io_size','_sim_io_init" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_io_set_estop','_sim_io_recover" "_sim_io_set_estop','_sim_io_set_power','_sim_io_recover" _sim_module_link_flags "${_sim_module_link_flags}") + string(REPLACE "_sim_io_spindle_command','_sim_io_spindle_set_inhibit" "_sim_io_spindle_command','_sim_io_spindle_adjust','_sim_io_spindle_brake','_sim_io_spindle_set_inhibit" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_device_init" "_sim_device_io_size','_sim_device_init" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_motion_step','_sim_motion_read_state" "_sim_motion_step','_sim_motion_set_profile','_sim_motion_set_lookahead','_sim_motion_set_feed_override','_sim_motion_peek','_sim_motion_read_state" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_motion_state_hash','_sim_motion_canonical" "_sim_motion_state_hash','_sim_motion_plan_hash','_sim_motion_canonical" _sim_module_link_flags "${_sim_module_link_flags}") diff --git a/wasm-core/include/sim_io.h b/wasm-core/include/sim_io.h index 4196dad..5faaaa3 100644 --- a/wasm-core/include/sim_io.h +++ b/wasm-core/include/sim_io.h @@ -120,6 +120,8 @@ int sim_io_reset(sim_io *io); int sim_io_register_pins(sim_io *io); int sim_io_spindle_command(sim_io *io, uint32_t spindle, double rpm, uint32_t enabled); +int sim_io_spindle_adjust(sim_io *io, uint32_t spindle, double delta); +int sim_io_spindle_brake(sim_io *io, uint32_t spindle, uint32_t engaged); int sim_io_spindle_set_inhibit(sim_io *io, uint32_t spindle, uint32_t inhibit); int sim_io_spindle_set_fault(sim_io *io, uint32_t spindle, uint32_t fault); int sim_io_spindle_set_index_enable(sim_io *io, uint32_t spindle, uint32_t enable); diff --git a/wasm-core/src/sim_io.c b/wasm-core/src/sim_io.c index 63572f7..a8ca42e 100644 --- a/wasm-core/src/sim_io.c +++ b/wasm-core/src/sim_io.c @@ -263,6 +263,38 @@ int sim_io_spindle_command(sim_io *io, uint32_t spindle, double rpm, uint32_t en return SIM_IO_OK; } +int sim_io_spindle_adjust(sim_io *io, uint32_t spindle, double delta) { + sim_io_spindle_state *state; + double target; + if (!valid_spindle(io, spindle)) return SIM_IO_ERR_ARGUMENT; + if (!isfinite(delta)) { set_last(io, SIM_IO_ERR_NONFINITE); return SIM_IO_ERR_NONFINITE; } + if (io->state.estop) { set_last(io, SIM_IO_ERR_ESTOP); return SIM_IO_ERR_ESTOP; } + if (!io->state.ready) { set_last(io, SIM_IO_ERR_NOT_READY); return SIM_IO_ERR_NOT_READY; } + state = &io->state.spindles[spindle]; + // Match LinuxCNC EMCMOT_SPINDLE_INCREASE/DECREASE: change the signed + // speed only while the spindle is running and keep its direction. + if (state->command_rpm > 0.0) target = state->command_rpm + delta; + else if (state->command_rpm < 0.0) target = state->command_rpm - delta; + else target = 0.0; + target = fmax(-state->max_rpm, fmin(state->max_rpm, target)); + state->command_rpm = state->enabled && !state->inhibit && !state->amp_fault ? target : 0.0; + set_last(io, SIM_IO_OK); + mirror_pins(io); + return SIM_IO_OK; +} + +int sim_io_spindle_brake(sim_io *io, uint32_t spindle, uint32_t engaged) { + sim_io_spindle_state *state; + if (!valid_spindle(io, spindle) || engaged > 1U) return SIM_IO_ERR_ARGUMENT; + if (io->state.estop) { set_last(io, SIM_IO_ERR_ESTOP); return SIM_IO_ERR_ESTOP; } + state = &io->state.spindles[spindle]; + state->brake = engaged; + if (engaged) { state->command_rpm = 0.0; state->enabled = 0; state->speed_rpm = 0.0; state->forward = 0; state->reverse = 0; state->at_speed = 1; } + set_last(io, SIM_IO_OK); + mirror_pins(io); + return SIM_IO_OK; +} + int sim_io_spindle_set_inhibit(sim_io *io, uint32_t spindle, uint32_t inhibit) { if (!valid_spindle(io, spindle) || inhibit > 1U) return SIM_IO_ERR_ARGUMENT; io->state.spindles[spindle].inhibit = inhibit; diff --git a/wasm-core/tests/sim_io_test.c b/wasm-core/tests/sim_io_test.c index b3a956d..7888363 100644 --- a/wasm-core/tests/sim_io_test.c +++ b/wasm-core/tests/sim_io_test.c @@ -38,6 +38,8 @@ int main(void) { limit_status = sim_io_spindle_command(&io, 0, 100001.0, 1); assert(limit_status == SIM_IO_ERR_LIMIT); assert(sim_io_spindle_command(&io, 0, 1200.0, 1) == SIM_IO_OK); + assert(sim_io_spindle_adjust(&io, 0, -100.0) == SIM_IO_OK && io.state.spindles[0].command_rpm == 1100.0); + assert(sim_io_spindle_adjust(&io, 0, 100.0) == SIM_IO_OK && io.state.spindles[0].command_rpm == 1200.0); spindle_step_status = sim_io_step(&io, 10); assert(spindle_step_status == SIM_IO_OK && io.state.spindles[0].speed_rpm == 600.0); assert(sim_io_step(&io, 10) == SIM_IO_OK && io.state.spindles[0].at_speed == 1); @@ -51,6 +53,8 @@ int main(void) { assert(sim_io_tool_change(&io) == SIM_IO_OK); assert(sim_io_tool_set_changed(&io, 1) == SIM_IO_OK && io.state.current_tool == 7); + assert(sim_io_probe_arm(&io, 0) == SIM_IO_OK); + assert(sim_io_probe_disarm(&io) == SIM_IO_OK && io.state.probe.state == SIM_IO_PROBE_IDLE); assert(sim_io_probe_arm(&io, 0) == SIM_IO_OK); probe_status = sim_io_probe_update(&io, &(sim_posemath_pose){{1.0, 2.0, 3.0}, 0, 0, 0, 0, 0, 0}, 1); assert(probe_status == SIM_IO_OK && io.state.probe.state == SIM_IO_PROBE_TRIPPED); @@ -66,6 +70,9 @@ int main(void) { assert(sim_io_set_power(&io, 0) == SIM_IO_OK && io.state.ready == 0); assert(sim_io_spindle_command(&io, 0, 100.0, 1) == SIM_IO_ERR_NOT_READY); assert(sim_io_set_power(&io, 1) == SIM_IO_OK && io.state.ready == 1 && io.state.recovery_epoch == 2); + assert(sim_io_spindle_brake(&io, 0, 0) == SIM_IO_OK); + assert(sim_io_spindle_command(&io, 0, 600.0, 1) == SIM_IO_OK); + assert(sim_io_spindle_brake(&io, 0, 1) == SIM_IO_OK && io.state.spindles[0].brake == 1 && io.state.spindles[0].command_rpm == 0.0); assert(sim_io_tool_set_fault(&io, 1, 42) == SIM_IO_OK); fault_clear_status = sim_io_tool_clear_fault(&io); diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index a39e6dc..e50c504 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -66,7 +66,7 @@ function ManualControl({ action, ready, canHome, onHomeAll, selectedAxis, setSel return

Axis:

{["x", "y", "z", "a", "c"].map((axis) => )}

Jog:

jog(-1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} negative`}>- jog(1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} positive`}>+Stop
action("HOME", { axis: selectedAxis })}>Home AxisHome All action("MDI_EXECUTE", { block: `G92 ${selectedAxis.toUpperCase()}0` })}>Touch Off action("MDI_EXECUTE", { block: "G43" })}>Tool Touch Off
-

Spindle:

onSpindle(-1)} /> onSpindle(0)}>Stop onSpindle(1)} />
action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>- action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+
+

Spindle:

onSpindle(-1)} /> onSpindle(0)}>Stop onSpindle(1)} />
action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>- action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+

Coolant:

Tool / Probe:

action("TOOL_PREPARE", { tool: 1, pocket: 1 })}>Prepare T1 action("TOOL_CHANGE", {})}>Change action("PROBE_CONFIG", { action: "arm", probe_type: 0 })}>Arm Probe action("PROBE_CONFIG", { action: "disarm" })}>Disarm
Tool {devices.tool?.current ?? 0} / {devices.tool?.state ?? "idle"}; Probe {devices.probe?.state ?? "idle"}
; } diff --git a/web/worker/sim-session.mjs b/web/worker/sim-session.mjs index 0487df7..c80ca12 100644 --- a/web/worker/sim-session.mjs +++ b/web/worker/sim-session.mjs @@ -37,7 +37,7 @@ const OPERATION_KINDS = new Set([ "SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT", "COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", "JOG_START", "JOG_STOP", - "HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "TOOL_PREPARE", + "HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "SPINDLE_BRAKE", "TOOL_PREPARE", "TOOL_CHANGED", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT", "SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "IO_STEP", "IO_RECOVER", "PROBE_CONFIG", "LIMIT_SET", "ESTOP", "HAL_COMMAND", "HAL_SET", @@ -1995,7 +1995,13 @@ export class SimSession { if (current.status === 1 && this.task.state.mode !== "AUTO") this.task.setMode("AUTO"); if (current.status === 1) this.xyzacProgram.start(); const result = this.xyzacProgram.step(); - if (result.status === "verified") { if (result.motion) this.wasmMotion.enqueue(result.motion); this.wasmMotion.setPaused(true); this.task.setProgram(result.program_status === 4 ? "completed" : "paused", result.line); this.task.transition(result.program_status === 4 ? "STOPPED" : "PAUSED"); } + if (result.status === "verified") { + if (this.task.state.state === "READY") this.task.transition("RUNNING"); + if (result.motion) this.wasmMotion.enqueue(result.motion); + this.wasmMotion.setPaused(true); + this.task.setProgram(result.program_status === 4 ? "completed" : "paused", result.line); + this.task.transition(result.program_status === 4 ? "STOPPED" : "PAUSED"); + } return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); } if (this.task.state.state === "READY") { @@ -2151,7 +2157,7 @@ export class SimSession { if (result.status === "error" && result.state?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205); return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motionProjection(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); } - case "SPINDLE_COMMAND": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT": + case "SPINDLE_COMMAND": case "SPINDLE_BRAKE": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT": case "SPINDLE_ORIENT": case "SPINDLE_ORIENTED": case "IO_STEP": case "TOOL_PREPARE": case "TOOL_PREPARED": case "TOOL_CHANGE": case "TOOL_CHANGED": case "TOOL_ABORT": case "TOOL_FAULT": case "TOOL_CLEAR_FAULT": diff --git a/web/worker/test_xyzac_3d_chips.mjs b/web/worker/test_xyzac_3d_chips.mjs index 63b3a8a..37ece12 100644 --- a/web/worker/test_xyzac_3d_chips.mjs +++ b/web/worker/test_xyzac_3d_chips.mjs @@ -45,14 +45,19 @@ assert.equal(send("HOME", { all: true }).kind, "COMMAND_RESULT"); const run = send("PROGRAM_RUN"); assert.equal(run.kind, "COMMAND_RESULT"); let stepped = null; -for (let cycle = 0; cycle < 300; cycle += 1) { +let executionTraceEntries = 0; +// The first long Z feed is planned at the configured LinuxCNC TP limit. Give +// the fixed-tick simulation enough servo time to complete it before checking +// that the interpreter has advanced beyond the historical line-82 boundary. +for (let cycle = 0; cycle < 1500; cycle += 1) { stepped = send("SIM_STEP", { cycles: 1, response: "motion_delta" }); assert.equal(stepped.kind, "COMMAND_RESULT"); + executionTraceEntries += stepped.payload.execution_trace.length; } assert.equal(stepped.kind, "COMMAND_RESULT"); assert.equal(stepped.payload.program.program_status, "running"); assert.ok(stepped.payload.program.current_line > 82); -assert.ok(stepped.payload.execution_trace.length > 0); +assert.ok(executionTraceEntries > 0); assert.ok(stepped.payload.program.execution_line_text.length > 0); assert.equal(stepped.payload.motion_authority, "wasm"); assert.equal(Object.hasOwn(stepped.payload.xyzac_ledger.entries.at(-1), "response"), false); @@ -66,6 +71,6 @@ console.log(JSON.stringify({ line_count: loaded.payload.program.line_count, preview_segments: loaded.payload.preview.segments.length, execution_line: stepped.payload.program.current_line, - execution_trace: stepped.payload.execution_trace.length, + execution_trace: executionTraceEntries, motion_authority: stepped.payload.motion_authority, })); diff --git a/web/worker/test_xyzac_panel_buttons.mjs b/web/worker/test_xyzac_panel_buttons.mjs new file mode 100644 index 0000000..e99a281 --- /dev/null +++ b/web/worker/test_xyzac_panel_buttons.mjs @@ -0,0 +1,144 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import createModule from "../../build-wasm/linuxcnc-sim-core.js"; +import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs"; +import { XyzacSession, CONTEXT_ID } from "./xyzac-session.mjs"; + +const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"; +const PROGRAM = "G21 G90\nG0 X0 Y0 Z10 A0 C0\nG1 X10 Y5 Z5 A30 C45 F100\nG1 X20 Y10 Z2 A35 C50 F100\nM2\n"; +const fetchImpl = async (url) => { + try { + const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); + return { ok: true, text: async () => body.toString("utf8") }; + } catch { + return { ok: false, text: async () => "" }; + } +}; + +const bundle = await loadSimulationCase({ path: CASE, fetchImpl }); +assert.equal(bundle.status, "ready"); +const wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)); +const rows = []; +const record = (group, button, command, source, response, expected = "verified") => { + const actual = response?.kind === "ERROR" || response?.payload?.status === "error" || response?.payload?.status === "blocked" ? "error" : "verified"; + assert.equal(actual, expected, `${group}/${button}: ${JSON.stringify(response?.payload ?? response)}`); + rows.push({ group, button, command, source, result: actual, reason: actual === "error" ? response.payload?.message_key ?? response.payload?.reason ?? response.payload?.code ?? null : null }); + return response; +}; + +async function open(sessionId) { + const core = await createModule({ wasmBinary }); + const session = new XyzacSession(core, { sessionId }); + const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: sessionId }; + let seq = 0; + const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: `${sessionId}-${seq}`, payload }); + record("session", "HELLO", "HELLO", "axis.py/AXIS startup", send("HELLO")); + record("session", "SESSION_ATTACH", "SESSION_ATTACH", "axis.py/AXIS startup", send("SESSION_ATTACH")); + record("machine", "Load case", "LOAD_WORKSPACE", "axis.py:configuration/startup", send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) })); + return { session, send }; +} + +const machine = await open("xyzac-panel-buttons-machine"); +const off = machine.send("PROGRAM_LOAD", { source: PROGRAM, path: "panel-buttons.ngc" }); +record("program", "Load", "PROGRAM_LOAD", "axis.py:open_file_guts/send_mdi_command", off); +for (const [button, command, payload, reason] of [ + ["Run while OFF", "PROGRAM_RUN", {}, "xyzac_machine_off"], + ["HOME ALL while OFF", "HOME", { all: true }, "xyzac_machine_off"], + ["JOG while OFF", "JOG_START", { axis: "x", direction: 1, speed: 1 }, "xyzac_machine_off"], + ["MDI while OFF", "MDI_EXECUTE", { block: "G0 X1" }, "xyzac_machine_off"], + ["M428 while OFF", "KINEMATICS_SELECT", { type: 1 }, "xyzac_machine_off"], +]) { + const response = machine.send(command, payload); + record("gates", button, command, "sim_task_command_gate / axis.py manual_ok", response, "error"); + assert.equal(response.payload.gate_reason, reason, button); +} +record("machine", "Power on", "POWER", "axis.py:commands.power / emcTaskSetState", machine.send("POWER", { active: true })); +record("machine", "HOME ALL", "HOME", "axis.py:home_all_joints", machine.send("HOME", { all: true })); +record("machine", "Reset", "RESET", "axis.py:commands.reset", machine.send("RESET")); +for (const [button, type] of [["Identity", 0], ["TCP: XYZAC", 1], ["User K", 2], ["TCP: XYZAC", 1]]) { + const response = machine.send("KINEMATICS_SELECT", { type }); + record("switchkins", button, "KINEMATICS_SELECT", "switchkins_postgui.hal / remap 428/429/430", response); + assert.equal(response.payload.kinematics.active_type, type); +} +record("view", "Clear plot", "VISUAL_CLEAR", "axis.py:clear_live_plot / glcanon.py", machine.send("VISUAL_CLEAR")); +record("override", "Feed 50%", "SET_OVERRIDE", "axis.py:set_feedrate -> c.feedrate", machine.send("SET_OVERRIDE", { feed: 0.5 })); +const invalidOverride = machine.send("SET_OVERRIDE", { feed: 2.01 }); +record("override", "Feed 201% rejected", "SET_OVERRIDE", "axis.py:set_feedrate range", invalidOverride, "error"); +assert.equal(invalidOverride.payload.message_key, "motion_override_range"); + +record("program", "Run", "PROGRAM_RUN", "axis.py:task_run -> c.auto(AUTO_RUN)", machine.send("PROGRAM_RUN")); +const step = machine.send("SIM_STEP", { cycles: 3 }); +record("program", "Servo/task step", "SIM_STEP", "axis.py:LivePlotter/update + LinuxCNC TP", step); +record("program", "Pause", "PROGRAM_PAUSE", "axis.py:task_pause -> c.auto(AUTO_PAUSE)", machine.send("PROGRAM_PAUSE")); +const pausedSnapshot = machine.send("SNAPSHOT_REQUEST"); +record("program", "Paused snapshot", "SNAPSHOT_REQUEST", "axis.py:stat.poll task_paused", pausedSnapshot); +assert.equal(pausedSnapshot.payload.task.state, "PAUSED"); +record("program", "Resume", "PROGRAM_RESUME", "axis.py:task_resume -> c.auto(AUTO_RESUME)", machine.send("PROGRAM_RESUME")); +record("program", "Abort/Stop", "PROGRAM_ABORT", "axis.py:task_stop -> c.abort", machine.send("PROGRAM_ABORT")); +assert.equal(machine.session.wasmMotion.read().state.queue_depth, 0); +record("program", "Restart", "PROGRAM_RESTART", "axis.py:reload_file/set_first_line", machine.send("PROGRAM_RESTART")); +record("program", "Step", "PROGRAM_STEP", "axis.py:task_step -> c.auto(AUTO_STEP)", machine.send("PROGRAM_STEP")); +record("program", "Seek", "PROGRAM_SEEK", "axis.py:set_first_line/task_run_line", machine.send("PROGRAM_SEEK", { line: 3 })); +record("program", "Resume from line", "PROGRAM_RESUME", "axis.py:task_resume", machine.send("PROGRAM_RESUME")); +record("program", "Abort after seek", "PROGRAM_ABORT", "axis.py:task_stop", machine.send("PROGRAM_ABORT")); + +const mdi = await open("xyzac-panel-buttons-mdi"); +record("machine", "Power on", "POWER", "axis.py:commands.power", mdi.send("POWER", { active: true })); +record("machine", "HOME ALL", "HOME", "axis.py:home_all_joints", mdi.send("HOME", { all: true })); +record("mdi", "Go", "MDI_EXECUTE", "axis.py:send_mdi_command", mdi.send("MDI_EXECUTE", { block: "G21 G90 G1 X10 Y5 Z5 A30 C45 F100" })); +record("mdi", "Invalid G-code", "MDI_EXECUTE", "axis.py:send_mdi_command/error channel", mdi.send("MDI_EXECUTE", { block: "G999" }), "error"); +record("mdi", "Load program", "PROGRAM_LOAD", "axis.py:open_file_guts", mdi.send("PROGRAM_LOAD", { source: PROGRAM, path: "mdi-panel.ngc" })); +record("mdi", "Run", "PROGRAM_RUN", "axis.py:task_run", mdi.send("PROGRAM_RUN")); +record("mdi", "MDI while running", "MDI_EXECUTE", "axis.py:manual_ok", mdi.send("MDI_EXECUTE", { block: "G0 X1" }), "error"); +assert.equal(mdi.send("MDI_EXECUTE", { block: "G0 X1" }).payload.message_key, "mdi_while_program_active"); +record("mdi", "Stop", "PROGRAM_ABORT", "axis.py:task_stop", mdi.send("PROGRAM_ABORT")); + +for (const axis of ["x", "y", "z", "a", "c"]) { + const jog = await open(`xyzac-panel-buttons-jog-${axis}`); + record("machine", "Power on", "POWER", "axis.py:commands.power", jog.send("POWER", { active: true })); + record("machine", "HOME ALL", "HOME", "axis.py:home_all_joints", jog.send("HOME", { all: true })); + const start = jog.session.wasmMotion.read().state.position[axis] ?? 0; + record("jog", `JOG ${axis.toUpperCase()} +`, "JOG_START", "axis.py:jog_plus/jog_on", jog.send("JOG_START", { axis, direction: 1, speed: 1, increment: axis === "a" || axis === "c" ? 2 : 1 })); + record("jog", `JOG ${axis.toUpperCase()} sample`, "SIM_STEP", "axis.py:LivePlotter/update", jog.send("SIM_STEP", { cycles: 1 })); + const end = jog.session.wasmMotion.read().state.position[axis] ?? 0; + assert.notEqual(end, start, axis); + record("jog", `JOG ${axis.toUpperCase()} stop`, "JOG_STOP", "axis.py:jog_stop -> JOG_STOP", jog.send("JOG_STOP")); +} + +const io = await open("xyzac-panel-buttons-io"); +record("machine", "Power on", "POWER", "axis.py:commands.power", io.send("POWER", { active: true })); +record("machine", "HOME ALL", "HOME", "axis.py:home_all_joints", io.send("HOME", { all: true })); +record("spindle", "CW", "SPINDLE_COMMAND", "axis.py:spindle_forward_toggle/c.spindle", io.send("SPINDLE_COMMAND", { spindle: 0, rpm: 600, enabled: true })); +assert.equal(io.send("SPINDLE_COMMAND", { spindle: 0, delta: 100 }).payload.device_io.state.spindles[0].command_rpm, 700); +rows.push({ group: "spindle", button: "Speed +", command: "SPINDLE_COMMAND(delta)", source: "axis.py:spindle_increase -> EMCMOT_SPINDLE_INCREASE", result: "verified", reason: null }); +assert.equal(io.send("SPINDLE_COMMAND", { spindle: 0, delta: -100 }).payload.device_io.state.spindles[0].command_rpm, 600); +rows.push({ group: "spindle", button: "Speed -", command: "SPINDLE_COMMAND(delta)", source: "axis.py:spindle_decrease -> EMCMOT_SPINDLE_DECREASE", result: "verified", reason: null }); +record("spindle", "Stop", "SPINDLE_COMMAND", "axis.py:spindle -> SPINDLE_OFF", io.send("SPINDLE_COMMAND", { spindle: 0, rpm: 0, enabled: false })); +record("spindle", "Brake engage", "SPINDLE_BRAKE", "axis.py:brake -> EMC_SPINDLE_BRAKE_ENGAGE", io.send("SPINDLE_BRAKE", { spindle: 0, engaged: true })); +record("spindle", "Brake release", "SPINDLE_BRAKE", "axis.py:brake -> EMC_SPINDLE_BRAKE_RELEASE", io.send("SPINDLE_BRAKE", { spindle: 0, engaged: false })); +record("coolant", "Mist/Flood on", "COOLANT_COMMAND", "axis.py:mist_toggle/flood_toggle", io.send("COOLANT_COMMAND", { mist: true, flood: true })); +record("tool", "Prepare T1", "TOOL_PREPARE", "axis.py:change_tool / iocontrol", io.send("TOOL_PREPARE", { tool: 1, pocket: 1 })); +record("tool", "Prepared ack", "TOOL_PREPARED", "axis.py:tool-prepared HAL", io.send("TOOL_PREPARED", { prepared: true })); +record("tool", "Change", "TOOL_CHANGE", "axis.py:change_tool -> c.mdi(M6)", io.send("TOOL_CHANGE")); +record("tool", "Changed ack", "TOOL_CHANGED", "axis.py:tool-changed HAL", io.send("TOOL_CHANGED", { changed: true })); +record("probe", "Arm", "PROBE_CONFIG", "axis.py:probe/canonical probe", io.send("PROBE_CONFIG", { probe_type: 0 })); +record("probe", "Disarm", "PROBE_CONFIG", "axis.py:probe disarm", io.send("PROBE_CONFIG", { action: "disarm" })); +assert.equal(io.session.xyzacIo.canonical().probe.state, 0); +record("machine", "ESTOP", "ESTOP", "axis.py:General_Halt/emcTaskAbort", io.send("ESTOP", { active: true })); +record("machine", "Release E-stop", "ESTOP", "axis.py:ESTOP reset", io.send("ESTOP", { active: false })); +record("machine", "Power after ESTOP", "POWER", "axis.py:commands.power", io.send("POWER", { active: true })); + +const unsupported = [ + { button: "Home Axis", reason: "WASM home state exposes aggregate homed only; no per-joint home mask" }, + { button: "Override Limits", reason: "selected XYZAC WASM configuration owns limits; no LinuxCNC override_limits command ABI" }, + { button: "Jog Speed slider", reason: "UI state is not connected to a WASM jog-speed command" }, + { button: "Max Velocity slider", reason: "UI state is not connected to a WASM max-velocity command" }, + { button: "Block delete / Optional pause", reason: "UI toggle is not forwarded to sim_program interpreter state" }, + { button: "Tool Touch Off", reason: "UI emits fixed G43 and does not expose LinuxCNC G10 prompt/value flow" }, + { button: "Edit tool data / Quit", reason: "desktop process boundary; browser intentionally host-excluded" }, +]; +for (const item of unsupported) rows.push({ group: "scope", button: item.button, command: null, source: "axis.py source mapped", result: "blocked_by_abi", reason: item.reason }); + +const counts = Object.fromEntries(["verified", "error", "blocked_by_abi"].map((status) => [status, rows.filter((row) => row.result === status).length])); +assert.ok(counts.verified > 0 && counts.error > 0 && counts.blocked_by_abi > 0); +console.log(JSON.stringify({ schema: "xyzac-panel-button-parity.v1", status: "verified", case: CASE, source: "linuxcnc-wasm-worker", counts, rows, selected_case_release_status: "STOP_RELEASE" })); diff --git a/web/worker/test_xyzac_trt_io_authority.mjs b/web/worker/test_xyzac_trt_io_authority.mjs index 525a8bf..0dd16b1 100644 --- a/web/worker/test_xyzac_trt_io_authority.mjs +++ b/web/worker/test_xyzac_trt_io_authority.mjs @@ -29,13 +29,22 @@ const spindle = send("SPINDLE_COMMAND", "spindle-on", { spindle: 0, rpm: 1200, e assert.equal(spindle.kind, "COMMAND_RESULT"); assert.equal(spindle.payload.device_io.source, "wasm_io"); assert.equal(spindle.payload.device_io.state.spindles[0].command_rpm, 1200); +const spindleSlower = send("SPINDLE_COMMAND", "spindle-decrease", { spindle: 0, delta: -100 }); +assert.equal(spindleSlower.payload.device_io.state.spindles[0].command_rpm, 1100); +const spindleFaster = send("SPINDLE_COMMAND", "spindle-increase", { spindle: 0, delta: 100 }); +assert.equal(spindleFaster.payload.device_io.state.spindles[0].command_rpm, 1200); const prepared = send("TOOL_PREPARE", "tool-prepare", { tool: 7, pocket: 3 }); assert.equal(prepared.payload.device_io.source, "wasm_io"); assert.equal(send("TOOL_PREPARED", "tool-prepared", { prepared: true }).payload.status, "verified"); assert.equal(send("SPINDLE_COMMAND", "spindle-off", { spindle: 0, rpm: 0, enabled: false }).payload.status, "verified"); +assert.equal(send("SPINDLE_BRAKE", "brake-release", { spindle: 0, engaged: false }).payload.status, "verified"); +const brake = send("SPINDLE_BRAKE", "brake-engage", { spindle: 0, engaged: true }); +assert.equal(brake.payload.device_io.state.spindles[0].brake, 1); assert.equal(send("TOOL_CHANGE", "tool-change").payload.status, "verified"); assert.equal(send("TOOL_CHANGED", "tool-changed", { changed: true }).payload.status, "verified"); assert.equal(send("PROBE_CONFIG", "probe-arm", { probe_type: 0 }).payload.status, "verified"); +assert.equal(send("PROBE_CONFIG", "probe-disarm", { action: "disarm" }).payload.status, "verified"); +assert.equal(send("PROBE_CONFIG", "probe-rearm", { probe_type: 0 }).payload.status, "verified"); const probe = session.xyzacIo.probeUpdate({ x: 10, y: 20, z: 30, a: 30, c: 45 }, 1); assert.equal(probe.status, "verified"); const hal = send("HAL_SET", "hal-probe", { target: "pin", name: "motion.probe-input", type: "bit", value: true }); diff --git a/web/worker/wasm-xyzac-io.mjs b/web/worker/wasm-xyzac-io.mjs index ded5734..c285cb4 100644 --- a/web/worker/wasm-xyzac-io.mjs +++ b/web/worker/wasm-xyzac-io.mjs @@ -139,7 +139,11 @@ export class WasmXyzacIo { command(kind, payload = {}) { if (kind === "RESET") return this.invoke("sim_io_reset"); - if (kind === "SPINDLE_COMMAND") return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, payload.rpm ?? payload.speed ?? 0, payload.enabled === false ? 0 : 1]); + if (kind === "SPINDLE_COMMAND") { + if (payload.delta !== undefined) return this.invoke("sim_io_spindle_adjust", [payload.spindle ?? 0, Number(payload.delta)]); + return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, payload.rpm ?? payload.speed ?? 0, payload.enabled === false ? 0 : 1]); + } + if (kind === "SPINDLE_BRAKE") return this.invoke("sim_io_spindle_brake", [payload.spindle ?? 0, payload.engaged === true ? 1 : 0]); if (kind === "SPINDLE_FAULT") return this.invoke("sim_io_spindle_set_fault", [payload.spindle ?? 0, payload.fault === true ? 1 : 0]); if (kind === "SPINDLE_INHIBIT") return this.invoke("sim_io_spindle_set_inhibit", [payload.spindle ?? 0, payload.inhibit === true ? 1 : 0]); if (kind === "SPINDLE_ORIENT") return this.invoke("sim_io_spindle_orient", [payload.spindle ?? 0, payload.angle ?? 0, payload.mode ?? 0]); @@ -151,10 +155,7 @@ export class WasmXyzacIo { if (kind === "TOOL_ABORT") return this.invoke("sim_io_tool_abort", [payload.reason ?? 0]); if (kind === "TOOL_FAULT") return this.invoke("sim_io_tool_set_fault", [payload.fault === true ? 1 : 0, payload.reason ?? 0]); if (kind === "TOOL_CLEAR_FAULT") return this.invoke("sim_io_tool_clear_fault"); - if (kind === "PROBE_CONFIG") { - const result = this.invoke("sim_io_probe_arm", [payload.probe_type ?? payload.type ?? 0]); - return result; - } + if (kind === "PROBE_CONFIG") return payload.action === "disarm" ? this.invoke("sim_io_probe_disarm") : this.invoke("sim_io_probe_arm", [payload.probe_type ?? payload.type ?? 0]); if (kind === "ESTOP" || kind === "DEVICE_ESTOP") return this.invoke("sim_io_set_estop", [payload.active === true ? 1 : 0]); if (kind === "POWER") return this.invoke("sim_io_set_power", [payload.active === true ? 1 : 0]); if (kind === "IO_RECOVER" || kind === "INTERLOCK_RESET") return this.invoke("sim_io_recover");