Verify XYZAC panel button parity
This commit is contained in:
46
coverage/xyzac-panel-button-parity.report.json
Normal file
46
coverage/xyzac-panel-button-parity.report.json
Normal file
@@ -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"
|
||||
}
|
||||
152
doc-rtcp/xyzac-panel-button-parity-test-report.md
Normal file
152
doc-rtcp/xyzac-panel-button-parity-test-report.md
Normal file
@@ -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 状态修正、聚合测试和本报告。
|
||||
@@ -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}")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -66,7 +66,7 @@ function ManualControl({ action, ready, canHome, onHomeAll, selectedAxis, setSel
|
||||
return <section className="manual-control" data-axis-feature="AXIS-MANUAL-01" aria-label="Manual Control"><div className="manual-grid">
|
||||
<div className="manual-axis-block"><h3>Axis:</h3><div className="axis-radio-grid">{["x", "y", "z", "a", "c"].map((axis) => <AxisRadio key={axis} axis={axis} selected={selectedAxis} onChange={setSelectedAxis} />)}</div></div>
|
||||
<div className="manual-jog-block"><h3>Jog:</h3><div className="jog-row"><TextButton disabled={!ready} onPointerDown={() => jog(-1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} negative`}>-</TextButton><TextButton disabled={!ready} onPointerDown={() => jog(1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} positive`}>+</TextButton><select value={increment} onChange={(event) => setIncrement(event.target.value)} aria-label="Jog increment"><option>Continuous</option><option value="0.1000">0.1000</option><option value="0.0100">0.0100</option><option value="0.0010">0.0010</option><option value="0.0001">0.0001</option></select><TextButton disabled={!ready} onClick={stopJog}>Stop</TextButton></div><div className="manual-action-row"><TextButton disabled={!canHome} onClick={() => action("HOME", { axis: selectedAxis })}>Home Axis</TextButton><TextButton disabled={!canHome} onClick={onHomeAll}>Home All</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: `G92 ${selectedAxis.toUpperCase()}0` })}>Touch Off</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: "G43" })}>Tool Touch Off</TextButton></div><label className="check-line"><input type="checkbox" checked={overrideLimits} onChange={(event) => setOverrideLimits(event.target.checked)} /> Override Limits</label></div>
|
||||
<div className="manual-spindle-block"><h3>Spindle:</h3><div className="spindle-row"><IconButton label="Spindle counter-clockwise" asset="spindle_ccw.gif" disabled={!ready} pressed={spindleOn === false} onClick={() => onSpindle(-1)} /><TextButton disabled={!ready} onClick={() => onSpindle(0)}>Stop</TextButton><IconButton label="Spindle clockwise" asset="spindle_cw.gif" disabled={!ready} pressed={spindleOn} onClick={() => onSpindle(1)} /></div><div className="spindle-row"><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>-</TextButton><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+</TextButton><label className="check-line"><input type="checkbox" checked={brake} onChange={(event) => setBrake(event.target.checked)} /> Brake</label></div></div>
|
||||
<div className="manual-spindle-block"><h3>Spindle:</h3><div className="spindle-row"><IconButton label="Spindle counter-clockwise" asset="spindle_ccw.gif" disabled={!ready} pressed={spindleOn === false} onClick={() => onSpindle(-1)} /><TextButton disabled={!ready} onClick={() => onSpindle(0)}>Stop</TextButton><IconButton label="Spindle clockwise" asset="spindle_cw.gif" disabled={!ready} pressed={spindleOn} onClick={() => onSpindle(1)} /></div><div className="spindle-row"><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>-</TextButton><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+</TextButton><label className="check-line"><input type="checkbox" checked={brake} onChange={(event) => { const engaged = event.target.checked; setBrake(engaged); void action("SPINDLE_BRAKE", { spindle: 0, engaged }); }} /> Brake</label></div></div>
|
||||
<div className="manual-coolant-block"><h3>Coolant:</h3><label className="check-line"><input type="checkbox" checked={coolant.mist} onChange={() => onCoolant("mist")} /> Mist</label><label className="check-line"><input type="checkbox" checked={coolant.flood} onChange={() => onCoolant("flood")} /> Flood</label></div>
|
||||
</div><div className="manual-device-block"><h3>Tool / Probe:</h3><div className="manual-action-row"><TextButton disabled={!ready} onClick={() => action("TOOL_PREPARE", { tool: 1, pocket: 1 })}>Prepare T1</TextButton><TextButton disabled={!ready} onClick={() => action("TOOL_CHANGE", {})}>Change</TextButton><TextButton disabled={!ready} onClick={() => action("PROBE_CONFIG", { action: "arm", probe_type: 0 })}>Arm Probe</TextButton><TextButton disabled={!ready} onClick={() => action("PROBE_CONFIG", { action: "disarm" })}>Disarm</TextButton></div><span className="device-status">Tool {devices.tool?.current ?? 0} / {devices.tool?.state ?? "idle"}; Probe {devices.probe?.state ?? "idle"}</span></div><div className="manual-sliders"><label>Jog Speed:<output>{jogSpeed}%</output><input type="range" min="6" max="100" value={jogSpeed} onChange={(event) => setJogSpeed(Number(event.target.value))} /></label><label>Max Velocity:<output>{maxVelocity}%</output><input type="range" min="6" max="100" value={maxVelocity} onChange={(event) => setMaxVelocity(Number(event.target.value))} /></label></div></section>;
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
144
web/worker/test_xyzac_panel_buttons.mjs
Normal file
144
web/worker/test_xyzac_panel_buttons.mjs
Normal file
@@ -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" }));
|
||||
@@ -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 });
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user