Use native LinuxCNC homing and consolidate web panel status
This commit is contained in:
53
doc-rtcp/xyzac-native-homing-status-validation.md
Normal file
53
doc-rtcp/xyzac-native-homing-status-validation.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Native Homing and Status Validation
|
||||||
|
|
||||||
|
Date: 2026-09-09. Local URL: http://127.0.0.1:5175/.
|
||||||
|
This records local execution, not deployment or a full LinuxCNC port.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
- The selected XYZAC case compiles the pinned `homing.c` and `simple_tp.c`.
|
||||||
|
- HAL allocation and ideal motor feedback are adapted; the homing state
|
||||||
|
machine is not rewritten in JavaScript.
|
||||||
|
- `task.state` is machine state; interpreter state is `task.interpState`.
|
||||||
|
- `motion.traj.mode` is FREE/COORD/TELEOP; `motion_type` is separate.
|
||||||
|
- `home_state` comes from homemod's state underlying `joint.N.home-state`.
|
||||||
|
- The monitor displays task mode/state, interpreter state, motion mode/type
|
||||||
|
and Homing. Per-joint status and history remain in the snapshot; their table
|
||||||
|
and history list were removed from the visible panel at the user's request.
|
||||||
|
- Numeric Home Joint replaces the world-axis-to-joint alias. Original control
|
||||||
|
ID `manual-home-axis` is retained; `manual-home-joint-select` selects 0..4.
|
||||||
|
- Home acceptance is asynchronous. Completion is determined by homemod,
|
||||||
|
not by the coordinated planner queue.
|
||||||
|
|
||||||
|
## Verified
|
||||||
|
|
||||||
|
- Native C test: `ctest --test-dir build-native -R sim_native_homing_test`.
|
||||||
|
- WASM/Worker: `node web/worker/test_native_homing.mjs`.
|
||||||
|
- All five joints: Jog then native no-search re-home resets joint coordinates
|
||||||
|
while preserving simulated physical motor position and updating motor offset.
|
||||||
|
- A test INI with HOME=5 executes the original free_tp final move; intermediate
|
||||||
|
positions are nonzero, homed stays false until completion, coordinated TP
|
||||||
|
queue remains empty.
|
||||||
|
- Partial home remains FREE; the native all-homed edge switches to TELEOP.
|
||||||
|
- AUTO+PAUSED rejects switching to MANUAL; abort and E-stop cancel homemod
|
||||||
|
without marking interrupted joints homed; subsequent re-home works.
|
||||||
|
- Native state save/restore preserves positions and offsets.
|
||||||
|
- Existing Step, WAITING/MDI Pause/Resume, rerun, touch-off, source widget gates
|
||||||
|
and Live Plot lifecycle regression tests pass with asynchronous Home setup.
|
||||||
|
- Source Tcl oracle: 576 scenarios, 7488 assertions. Control inventory: 83 IDs.
|
||||||
|
- Visible Chrome tests: `scripts/test_xyzac_home_reset_browser.mjs` and
|
||||||
|
`scripts/test_xyzac_panel_buttons_browser.mjs` with `HEADED=1`.
|
||||||
|
- Desktop/mobile screenshots and canvas pixel checks pass. General browser
|
||||||
|
audit covers 51 buttons with no page errors.
|
||||||
|
- React type checking and production build pass.
|
||||||
|
|
||||||
|
Screenshots: `/tmp/axis-native-home-desktop.png`,
|
||||||
|
`/tmp/axis-native-home-mobile.png`.
|
||||||
|
|
||||||
|
## Remaining Boundaries
|
||||||
|
|
||||||
|
Task/interpreter status still uses the existing WASM adapter. Physical switch,
|
||||||
|
encoder and unlock hardware have not been validated; the selected INI uses
|
||||||
|
zero search/latch velocities. Full Joint/World jogging is not exposed.
|
||||||
|
General recovery retains the existing planner/HAL endpoint differences and
|
||||||
|
large RECOVER envelope stack limitation documented in the source-flow contract.
|
||||||
@@ -45,12 +45,12 @@ digraph LinuxCncAxisWebControlLogic {
|
|||||||
|
|
||||||
source [label="LinuxCNC 源程序\naxis.py · axis.tcl\nemctaskmain.cc · HAL/INI", fillcolor="#f7f1df", color="#b08a2e"];
|
source [label="LinuxCNC 源程序\naxis.py · axis.tcl\nemctaskmain.cc · HAL/INI", fillcolor="#f7f1df", color="#b08a2e"];
|
||||||
contract [label="源程序锁定契约\nSOURCE_EXACT · SOURCE_ADAPTER\nSOURCE_BLOCKED · WEB_EXTENSION", fillcolor="#fff5c9", color="#b08a2e", penwidth=2.2];
|
contract [label="源程序锁定契约\nSOURCE_EXACT · SOURCE_ADAPTER\nSOURCE_BLOCKED · WEB_EXTENSION", fillcolor="#fff5c9", color="#b08a2e", penwidth=2.2];
|
||||||
gesture [label="浏览器控件手势\ndata-axis-control\n82 个文档控制 ID", fillcolor="#edf6f8", color="#3c8091"];
|
gesture [label="浏览器控件手势\ndata-axis-control\n83 个文档控制 ID", fillcolor="#edf6f8", color="#3c8091"];
|
||||||
react [label="React handler\n只发意图,不是状态权威", fillcolor="#edf6f8", color="#3c8091"];
|
react [label="React handler\n只发意图,不是状态权威", fillcolor="#edf6f8", color="#3c8091"];
|
||||||
operation [label="AxisOperationController\noperation_id · ledger\nWeb 通道暂停仍接收合法连续 seq", fillcolor="#eef2fb", color="#526da8"];
|
operation [label="AxisOperationController\noperation_id · ledger\nWeb 通道暂停仍接收合法连续 seq", fillcolor="#eef2fb", color="#526da8"];
|
||||||
gate [label="SimSession command gate\n最终门控与稳定拒绝原因", fillcolor="#fcebdc", color="#bd6a28", penwidth=2.2];
|
gate [label="SimSession command gate\n最终门控与稳定拒绝原因", fillcolor="#fcebdc", color="#bd6a28", penwidth=2.2];
|
||||||
wasm [label="LinuxCNC/WASM 权威\nTask · Interpreter · TP\nIO · Home/Limit · Kinematics", fillcolor="#e9f5e8", color="#43844b", penwidth=2.2];
|
wasm [label="LinuxCNC/WASM 权威\nTask · Interpreter · TP\nIO · Home/Limit · Kinematics", fillcolor="#e9f5e8", color="#43844b", penwidth=2.2];
|
||||||
snapshot [label="Canonical snapshot\naxis_state · ui_gates / gates · motion\nprogram · devices · visual", fillcolor="#e9f5e8", color="#43844b"];
|
snapshot [label="Canonical snapshot\naxis_state · ui_gates / gates · motion\nprogram · devices · visual · source_status", fillcolor="#e9f5e8", color="#43844b"];
|
||||||
projection [label="React projection\n按钮 enabled/pressed\nDRO · Preview · Diagnostics", fillcolor="#edf6f8", color="#3c8091"];
|
projection [label="React projection\n按钮 enabled/pressed\nDRO · Preview · Diagnostics", fillcolor="#edf6f8", color="#3c8091"];
|
||||||
|
|
||||||
{ rank=same; source; contract; gesture; react; operation; }
|
{ rank=same; source; contract; gesture; react; operation; }
|
||||||
@@ -106,7 +106,10 @@ digraph LinuxCncAxisWebControlLogic {
|
|||||||
|
|
||||||
estop -> off [label="ESTOP active=false"];
|
estop -> off [label="ESTOP active=false"];
|
||||||
off -> on_unhomed [label="POWER active=true"];
|
off -> on_unhomed [label="POWER active=true"];
|
||||||
on_unhomed -> idle [label="HOME all=true"];
|
homing [label="MANUAL · HOMING\\ntraj.mode=FREE · interpState=IDLE\\nhoming.c + simple_tp.c\\njoint homing / homed / home-state", fillcolor="#e9f5e8"];
|
||||||
|
on_unhomed -> homing [label="HOME all/joint"];
|
||||||
|
homing -> idle [label="全部 homed -> TELEOP"];
|
||||||
|
homing -> on_unhomed [label="部分回零 / 中止", style=dashed];
|
||||||
idle -> running [label="PROGRAM_RUN"];
|
idle -> running [label="PROGRAM_RUN"];
|
||||||
running -> paused [label="PROGRAM_PAUSE"];
|
running -> paused [label="PROGRAM_PAUSE"];
|
||||||
paused -> running [label="PROGRAM_RESUME"];
|
paused -> running [label="PROGRAM_RESUME"];
|
||||||
@@ -135,7 +138,7 @@ digraph LinuxCncAxisWebControlLogic {
|
|||||||
];
|
];
|
||||||
|
|
||||||
manual_controls [
|
manual_controls [
|
||||||
label="手动控制\n\nmanual-axis-x / y / z / a / c (本地选择)\nmanual-jog-increment (本地选择)\nmanual-jog-negative / positive (JOG_START -> release JOG_STOP)\nmanual-home-axis (BOTH: world-axis 拒绝)\nmanual-touch-off (TOUCH_OFF / G10 L20)\nmanual-tool-touch-off (blocked_by_abi_tool_offsets)\nmanual-override-limits (blocked_by_abi)\nmanual-spindle-ccw / stop / cw (SPINDLE_COMMAND)\nmanual-spindle-decrease / increase\n (SPINDLE_COMMAND -> release SPINDLE_CONSTANT)\nmanual-spindle-brake (SPINDLE_BRAKE)\nmanual-coolant-mist / flood (COOLANT_COMMAND)",
|
label="手动控制\n\nmanual-axis-x / y / z / a / c (本地选择)\nmanual-jog-increment (本地选择)\nmanual-jog-negative / positive (JOG_START -> release JOG_STOP)\nmanual-home-joint-select (数字关节选择)\nmanual-home-axis (HOME joint; 原生 homemod)\nmanual-touch-off (TOUCH_OFF / G10 L20)\nmanual-tool-touch-off (blocked_by_abi_tool_offsets)\nmanual-override-limits (blocked_by_abi)\nmanual-spindle-ccw / stop / cw (SPINDLE_COMMAND)\nmanual-spindle-decrease / increase\n (SPINDLE_COMMAND -> release SPINDLE_CONSTANT)\nmanual-spindle-brake (SPINDLE_BRAKE)\nmanual-coolant-mist / flood (COOLANT_COMMAND)",
|
||||||
fontname="Noto Sans Mono CJK SC",
|
fontname="Noto Sans Mono CJK SC",
|
||||||
fillcolor="#e9f5e8",
|
fillcolor="#e9f5e8",
|
||||||
color="#43844b"
|
color="#43844b"
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ fingerprinted so the audit fails on an unreviewed source drift:
|
|||||||
| `lib/python/glnav.py` | `b2d811a21954966655a40f107c2af217cc7d57bae0dc308dc4698f5859ed3f11` | Orthographic/perspective presets, rotation, camera framing |
|
| `lib/python/glnav.py` | `b2d811a21954966655a40f107c2af217cc7d57bae0dc308dc4698f5859ed3f11` | Orthographic/perspective presets, rotation, camera framing |
|
||||||
| `src/emc/task/emctask.cc` | pinned by submodule commit | Machine state edges and observed `ESTOP`/`ESTOP_RESET`/`ON` state |
|
| `src/emc/task/emctask.cc` | pinned by submodule commit | Machine state edges and observed `ESTOP`/`ESTOP_RESET`/`ON` state |
|
||||||
| `src/emc/task/emctaskmain.cc` | pinned by submodule commit | Task command acceptance, homing enforcement, run/pause/resume/abort and task-plan reopen/close |
|
| `src/emc/task/emctaskmain.cc` | pinned by submodule commit | Task command acceptance, homing enforcement, run/pause/resume/abort and task-plan reopen/close |
|
||||||
|
| `src/emc/motion/homing.c` | `ee73d38f2feb8d5523b9a733cb17d635d3904667aaa219f8a02d7fbffee1ba39` | Native joint homing and sequence state machines, compiled into WASM |
|
||||||
|
| `src/emc/motion/simple_tp.c` | `5a0026f4d5bdd9d1bc55290c6ea9c399abd664633b69413c307f7353953d2603` | Native free-mode joint motion during homing |
|
||||||
| `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` plus `remap_subs/{428,429,430}remap.ngc` | pinned by submodule commit | Selected axes, limits, speeds, startup file and switchkins remaps |
|
| `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` plus `remap_subs/{428,429,430}remap.ngc` | pinned by submodule commit | Selected axes, limits, speeds, startup file and switchkins remaps |
|
||||||
|
|
||||||
The current local LinuxCNC tree contains unrelated heartbeat instrumentation
|
The current local LinuxCNC tree contains unrelated heartbeat instrumentation
|
||||||
@@ -155,7 +157,7 @@ families because their cardinality comes from the selected INI.
|
|||||||
| Optional Stop and Block Delete | `toggle_optional_stop`, `toggle_block_delete` | Visible `SOURCE_BLOCKED`; block delete's source wait, MANUAL switch and preview reload must not be approximated by a local checkbox |
|
| Optional Stop and Block Delete | `toggle_optional_stop`, `toggle_block_delete` | Visible `SOURCE_BLOCKED`; block delete's source wait, MANUAL switch and preview reload must not be approximated by a local checkbox |
|
||||||
| MDI history clear/copy/paste/select/double-click/Up/Down | `clear_mdi_history`, `mdi_history_hist2clip`, `mdi_history_clip2hist`, `mdi_history_butt_1`, `mdi_history_double_butt_1`, `mdi_up_cmd`, `mdi_down_cmd` | Select/Up/Down/local history: `SOURCE_ADAPTER`; clipboard/file persistence variants are browser/host boundaries |
|
| MDI history clear/copy/paste/select/double-click/Up/Down | `clear_mdi_history`, `mdi_history_hist2clip`, `mdi_history_clip2hist`, `mdi_history_butt_1`, `mdi_history_double_butt_1`, `mdi_up_cmd`, `mdi_down_cmd` | Select/Up/Down/local history: `SOURCE_ADAPTER`; clipboard/file persistence variants are browser/host boundaries |
|
||||||
| Calibration, HAL Configuration, HAL Meter, HAL Scope, LinuxCNC Status, Debug Level | external `exec` commands in Machine menu | Desktop-process boundary; not rendered and never mapped to Diagnostics as though it were the same action |
|
| Calibration, HAL Configuration, HAL Meter, HAL Scope, LinuxCNC Status, Debug Level | external `exec` commands in Machine menu | Desktop-process boundary; not rendered and never mapped to Diagnostics as though it were the same action |
|
||||||
| Home all/joint and Unhome all/joint | `home_all_joints`, `home_joint_number`, `unhome_all_joints`, `unhome_joint_number` | Home all: `SOURCE_ADAPTER`; Web Home Axis maps the fixed XYZAC axis selection to a numeric joint and retains the source KINEMATICS_BOTH restriction; individual joint selection and Unhome remain `SOURCE_BLOCKED` |
|
| Home all/joint and Unhome all/joint | `home_all_joints`, `home_joint_number`, `unhome_all_joints`, `unhome_joint_number` | Home All and numeric Home Joint: `SOURCE_ADAPTER`, native homemod execution. World-axis requests reject for BOTH. Unhome remains unavailable. |
|
||||||
| Zero P1..P9 and G92 coordinate systems | `clear_offset` | `SOURCE_BLOCKED`; Touch Off is not an alias for clearing a coordinate system |
|
| Zero P1..P9 and G92 coordinate systems | `clear_offset` | `SOURCE_BLOCKED`; Touch Off is not an alias for clearing a coordinate system |
|
||||||
| Tool touch-off target workpiece/fixture | `toggle_tto_g11` selecting `G10 L10`/`G10 L11` | `SOURCE_BLOCKED` with Tool Touch Off; no local target state is exposed |
|
| Tool touch-off target workpiece/fixture | `toggle_tto_g11` selecting `G10 L10`/`G10 L11` | `SOURCE_BLOCKED` with Tool Touch Off; no local target state is exposed |
|
||||||
| Top/Z2/Side/Front/Y2/Perspective, zoom and rotate | `set_view_*`, `zoomin`, `zoomout`, rotate Tcl command | `SOURCE_ADAPTER`; Y2 is toolbar-only in the native layout |
|
| Top/Z2/Side/Front/Y2/Perspective, zoom and rotate | `set_view_*`, `zoomin`, `zoomout`, rotate Tcl command | `SOURCE_ADAPTER`; Y2 is toolbar-only in the native layout |
|
||||||
@@ -163,9 +165,9 @@ families because their cardinality comes from the selected INI.
|
|||||||
| Show program, rapids, alpha, live plot, tool, extents, offsets, limits, velocity, DTG | `toggle_show_*`, `toggle_program_alpha` | Program/live/extents/offsets/limits/velocity have adapters; rapids, alpha, tool toggle and DTG are not rendered |
|
| Show program, rapids, alpha, live plot, tool, extents, offsets, limits, velocity, DTG | `toggle_show_*`, `toggle_program_alpha` | Program/live/extents/offsets/limits/velocity have adapters; rapids, alpha, tool toggle and DTG are not rendered |
|
||||||
| Grid Off/custom/INI increments, large DRO font, PyVCP, Joint/World | `set_grid_size`, `set_grid_size_custom`, `toggle_dro_large_font`, `toggle_show_pyvcppanel`, `set_teleop_mode` | Not rendered; Joint/World is `SOURCE_BLOCKED`, presentation-only settings are not aliased |
|
| Grid Off/custom/INI increments, large DRO font, PyVCP, Joint/World | `set_grid_size`, `set_grid_size_custom`, `toggle_dro_large_font`, `toggle_show_pyvcppanel`, `set_teleop_mode` | Not rendered; Joint/World is `SOURCE_BLOCKED`, presentation-only settings are not aliased |
|
||||||
| Clear Live Plot | `clear_live_plot` -> `LivePlotter.clear` -> `positionlogger.clear` | `SOURCE_ADAPTER`; this is the only native live-trace deletion action |
|
| Clear Live Plot | `clear_live_plot` -> `LivePlotter.clear` -> `positionlogger.clear` | `SOURCE_ADAPTER`; this is the only native live-trace deletion action |
|
||||||
| Axis/joint selection and jog increment | `ja_rbutton`, `jogincrement`, `activate_ja_widget` | XYZAC axis and increment selection: `SOURCE_ADAPTER`; native joint selection is `SOURCE_BLOCKED` |
|
| Axis/joint selection and jog increment | `ja_rbutton`, `jogincrement`, `activate_ja_widget` | XYZAC axis/increment selection and dedicated numeric homing-joint selection: `SOURCE_ADAPTER`; full joint-mode jogging remains unavailable |
|
||||||
| Jog negative/positive and key jog | `jog_minus`, `jog_plus`, `jog_on`, `jog_off` | `SOURCE_ADAPTER`; press/release semantics and Manual-tab guard apply |
|
| Jog negative/positive and key jog | `jog_minus`, `jog_plus`, `jog_on`, `jog_off` | `SOURCE_ADAPTER`; press/release semantics and Manual-tab guard apply |
|
||||||
| Home Axis, Touch Off, Tool Touch Off, Override Limits | corresponding `zerohome`/`override` widgets | Home Axis: fixed XYZAC axis-to-joint `SOURCE_ADAPTER` with source mode transaction; Touch Off: selected-coordinate adapter; Tool Touch Off/Override Limits: visible `SOURCE_BLOCKED` |
|
| Home Axis, Touch Off, Tool Touch Off, Override Limits | `home_joint`, `home_joint_number`, touch-off/override widgets | Home Joint uses explicit numeric selection and native homemod. Touch Off is a coordinate adapter; Tool Touch Off/Override Limits remain blocked. |
|
||||||
| Spindle CCW/Stop/CW, slower/faster, brake | `spindle`, `spindle_decrease`, `spindle_increase`, `spindle_constant`, `brake` | `SOURCE_ADAPTER`; button-state formulas and press/release ordering apply |
|
| Spindle CCW/Stop/CW, slower/faster, brake | `spindle`, `spindle_decrease`, `spindle_increase`, `spindle_constant`, `brake` | `SOURCE_ADAPTER`; button-state formulas and press/release ordering apply |
|
||||||
| Mist and Flood | `mist`, `flood`, `mist_toggle`, `flood_toggle` | `SOURCE_ADAPTER`; independent canonical IO bits |
|
| Mist and Flood | `mist`, `flood`, `mist_toggle`, `flood_toggle` | `SOURCE_ADAPTER`; independent canonical IO bits |
|
||||||
| MDI entry, Go, Enter, history activation | `send_mdi`, `send_mdi_command` | `SOURCE_ADAPTER`; source trim, mode and adjacent-history de-duplication apply |
|
| MDI entry, Go, Enter, history activation | `send_mdi`, `send_mdi_command` | `SOURCE_ADAPTER`; source trim, mode and adjacent-history de-duplication apply |
|
||||||
@@ -184,7 +186,7 @@ command are blocked instead of being silently mapped to another operation.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `F1`, `F2`, lowercase `o`, `r`, `t`, `p`, `s`, `Escape`, `Ctrl-r`, `F7`-`F12` | E-stop, power, open, run, step, pause, resume, stop, reload, coolant and spindle handlers | Worker-backed commands where the corresponding control is supported. Display labels remain `O/R/T/P/S`. |
|
| `F1`, `F2`, lowercase `o`, `r`, `t`, `p`, `s`, `Escape`, `Ctrl-r`, `F7`-`F12` | E-stop, power, open, run, step, pause, resume, stop, reload, coolant and spindle handlers | Worker-backed commands where the corresponding control is supported. Display labels remain `O/R/T/P/S`. |
|
||||||
| uppercase `R`, `F` | `task_reverse`, `task_forward` | Source controls are recorded as blocked because the selected WASM program ABI has no reverse/forward command; they are never mapped to Run. |
|
| uppercase `R`, `F` | `task_reverse`, `task_forward` | Source controls are recorded as blocked because the selected WASM program ABI has no reverse/forward command; they are never mapped to Run. |
|
||||||
| `Home`, `Ctrl-Home`, `Shift-Home`, `End`, `Ctrl-End` | selected joint home, home all, zero selected G54 axis, system touch off, tool touch off | Home uses the fixed XYZAC axis-to-joint adapter (and Home All uses the all-joint transaction); system Touch Off has an adapter; zero-G54 and Tool Touch Off are blocked. |
|
| `Home`, `Ctrl-Home`, `Shift-Home`, `End`, `Ctrl-End` | selected joint home, home all, zero selected G54 axis, system touch off, tool touch off | Home uses numeric joint selection (and Home All uses the native configured joint sequences); system Touch Off has an adapter; zero-G54 and Tool Touch Off are blocked. |
|
||||||
| lowercase `x`, `y`, `z`, `a` | Select the first four source axes | Axis selection adapter. Native AXIS does **not** bind lowercase `c` to C-axis selection; `c` selects continuous jog. C remains selectable only through its radio control. |
|
| lowercase `x`, `y`, `z`, `a` | Select the first four source axes | Axis selection adapter. Native AXIS does **not** bind lowercase `c` to C-axis selection; `c` selects continuous jog. C remains selectable only through its radio control. |
|
||||||
| `-`, `=`, arrows/Page keys/`[`/`]` and their Shift variants | `jog_on_map`/`jog_off_map` or selected-axis `jog_minus`/`jog_plus` | Jog key semantics require keydown start and keyup stop. Pointer controls are supported; unavailable key bindings are not reassigned. |
|
| `-`, `=`, arrows/Page keys/`[`/`]` and their Shift variants | `jog_on_map`/`jog_off_map` or selected-axis `jog_minus`/`jog_plus` | Jog key semantics require keydown start and keyup stop. Pointer controls are supported; unavailable key bindings are not reassigned. |
|
||||||
| lowercase `c`, lowercase `i`, uppercase `I` | Continuous, next incremental and previous incremental jog modes | Represented by the jog increment selector; no second machine-mode state is created. |
|
| lowercase `c`, lowercase `i`, uppercase `I` | Continuous, next incremental and previous incremental jog modes | Represented by the jog increment selector; no second machine-mode state is created. |
|
||||||
@@ -219,15 +221,12 @@ command are blocked instead of being silently mapped to another operation.
|
|||||||
`motion/control.c:2186` 在执行 ID 改变后暂停 TP,不能“解析一行后永久
|
`motion/control.c:2186` 在执行 ID 改变后暂停 TP,不能“解析一行后永久
|
||||||
冻结”,也不能一次 Step 把全部已排队运动执行完。Web 单步的源码行解析
|
冻结”,也不能一次 Step 把全部已排队运动执行完。Web 单步的源码行解析
|
||||||
与原生 NML 指令细分仍是需单独验证的适配边界,不能用行数相同替代运动断言。
|
与原生 NML 指令细分仍是需单独验证的适配边界,不能用行数相同替代运动断言。
|
||||||
5. **回零类型不能混同。** `switchkins.c::kinematicsType()` 始终返回
|
5. **回零采用原生 homemod。** `switchkins.c::kinematicsType()` 始终返回
|
||||||
`KINEMATICS_BOTH`;M429 的 `active_type=0` 不代表 KINEMATICS_IDENTITY。
|
`KINEMATICS_BOTH`,M429 不改变这一类型。世界轴字母不能作为回零目标;
|
||||||
原生 `home_joint` 在这种配置下只接受数字 joint;直接把世界坐标字母传给
|
`manual-home-joint-select` 明确选择数字关节,`manual-home-axis` 保留 ID,
|
||||||
`home_joint` 会被源码拒绝。固定的 XYZAC TRT Web 面板没有原生 joint
|
按钮显示 Home Joint,提交 `HOME {joint:0..4}`。Home All 提交
|
||||||
单选组,因此 `Home Axis` 使用明确记录的 Web 适配:
|
`HOME {all:true}`。WASM 直接编译源 `homing.c` 和 `simple_tp.c`,
|
||||||
`X/Y/Z/A/C -> joint 0/1/2/3/4`,Worker 只把这个映射后的数字 joint
|
不再预览 home-limit 后排一条协调 TP 回零直线。完整语义见下方 Native Homing。
|
||||||
交给 WASM home-limit authority,并在结果中回显 `source_axis`、
|
|
||||||
`joint` 和 `motion_mode`。它不是把 `KINEMATICS_BOTH` 冒充成
|
|
||||||
`KINEMATICS_IDENTITY`,也不是用坐标轴字母绕过 joint 约束。
|
|
||||||
6. **自动恢复标签模式。** `axis.tcl::update_state` 在 ON+IDLE 的状态边沿,
|
6. **自动恢复标签模式。** `axis.tcl::update_state` 在 ON+IDLE 的状态边沿,
|
||||||
当前 task_mode 为 AUTO 时调用 `set_mode_from_tab`;显式 Open/Reload 完成后
|
当前 task_mode 为 AUTO 时调用 `set_mode_from_tab`;显式 Open/Reload 完成后
|
||||||
也调用它。Web 必须通过排队的 `SET_MODE` 恢复所选 Manual/MDI 页,不能只
|
也调用它。Web 必须通过排队的 `SET_MODE` 恢复所选 Manual/MDI 页,不能只
|
||||||
@@ -254,7 +253,7 @@ command are blocked instead of being silently mapped to another operation.
|
|||||||
| 功能 | 原生行为 / 当前边界 | 禁止的错误结论 |
|
| 功能 | 原生行为 / 当前边界 | 禁止的错误结论 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Run 警告 | `run_warn` 对 XYZ 预览边界给出确认;当前 WASM 没有完整含刀偏 extents 警告事务 | 不得声称已复制 run_warn 的确认/取消交互 |
|
| Run 警告 | `run_warn` 对 XYZ 预览边界给出确认;当前 WASM 没有完整含刀偏 extents 警告事务 | 不得声称已复制 run_warn 的确认/取消交互 |
|
||||||
| Joint/World、Home Axis | KINEMATICS_BOTH 需要 joint 数字选择与 FREE/TELEOP 状态;固定 XYZAC Web 面板以 X/Y/Z/A/C 的明确一对一映射提供 Home Axis 适配,执行时仍切入 FREE/joint 并由 WASM 返回数字 joint 结果 | 不得用 switchkins 编号判断 kinematics 类型,不得把世界轴字母直接交给 `home_joint`,不得把本地坐标归零冒充 joint home |
|
| Joint/World、Home Axis | 回零执行使用原生 FREE/homemod;数字关节选择用于 Home Joint,轴选择仍用于世界坐标 Jog/Touch Off | 不得用 X/Y/Z/A/C 偷换 joint,也不得把 M429 当作 KINEMATICS_IDENTITY。完整 Joint/World 点动切换尚未提供。 |
|
||||||
| 速度滑块 | 原生线性/角速度独立,显示物理单位;当前 Jog/Max 的百分比与 6% 下限属于已公开的 Web 适配 | 不得把单一比例视为独立 angular jog 控制 |
|
| 速度滑块 | 原生线性/角速度独立,显示物理单位;当前 Jog/Max 的百分比与 6% 下限属于已公开的 Web 适配 | 不得把单一比例视为独立 angular jog 控制 |
|
||||||
| 预览图层 | limits 现从 WASM XYZ 软限位 getter 读取;offsets 使用已实现的 G5x 平移;extents 使用 canonical 采样包围框 | G92/旋转/刀具偏置范围与原生文字标注仍未全量覆盖,不能称完整原生图层复刻 |
|
| 预览图层 | limits 现从 WASM XYZ 软限位 getter 读取;offsets 使用已实现的 G5x 平移;extents 使用 canonical 采样包围框 | G92/旋转/刀具偏置范围与原生文字标注仍未全量覆盖,不能称完整原生图层复刻 |
|
||||||
| commanded/actual | 仿真当前两者相同,需要分别存在的 canonical 字段才可声明跟随误差显示 | 不得从 React 标签推导真实反馈差异 |
|
| commanded/actual | 仿真当前两者相同,需要分别存在的 canonical 字段才可声明跟随误差显示 | 不得从 React 标签推导真实反馈差异 |
|
||||||
@@ -354,10 +353,10 @@ safety adaptation, not attributed to AXIS.
|
|||||||
| DRO coordinate/display/units | local projection | Maps AXIS `coord_type`, `display_type` and `metric` variables; values remain Worker snapshot projections. |
|
| DRO coordinate/display/units | local projection | Maps AXIS `coord_type`, `display_type` and `metric` variables; values remain Worker snapshot projections. |
|
||||||
| Preview layers | local projection | `SOURCE_ADAPTER` only where the source variable exists: path=`show_program`, live=`show_live_plot`, limits=`show_machine_limits`, velocity=`show_machine_speed`, offsets=`show_offsets`, extents=`show_extents`. AXIS also has rapids, alpha, tool and DTG controls that the Web does not expose. Toggling visibility never deletes layer data. |
|
| Preview layers | local projection | `SOURCE_ADAPTER` only where the source variable exists: path=`show_program`, live=`show_live_plot`, limits=`show_machine_limits`, velocity=`show_machine_speed`, offsets=`show_offsets`, extents=`show_extents`. AXIS also has rapids, alpha, tool and DTG controls that the Web does not expose. Toggling visibility never deletes layer data. |
|
||||||
| Manual / MDI tabs | `SET_MODE` | `SOURCE_ADAPTER`: native tab callbacks are `ensure_manual` and `ensure_mdi`, including `set_motion_teleop` transitions. `axis.tcl::update_state` calls `set_mode_from_tab` only when returning to ON+IDLE from a non-idle/task-state transition while task mode is AUTO. No unrelated React effect may force a mode. |
|
| Manual / MDI tabs | `SET_MODE` | `SOURCE_ADAPTER`: native tab callbacks are `ensure_manual` and `ensure_mdi`, including `set_motion_teleop` transitions. `axis.tcl::update_state` calls `set_mode_from_tab` only when returning to ON+IDLE from a non-idle/task-state transition while task mode is AUTO. No unrelated React effect may force a mode. |
|
||||||
| Joint/World (teleop) mode | not exposed | AXIS View menu calls `set_teleop_mode`/`c.teleop_enable`; the selected WASM motion ABI has no canonical teleop setter, so Web does not pretend its coordinate mode is a joint/world toggle. |
|
| Joint/World (teleop) mode | not exposed | AXIS View menu calls `set_teleop_mode`/`c.teleop_enable`; WASM now exposes motion mode for homing and task transitions; the full joint/world jog toggle is still not rendered. |
|
||||||
| Jog `-` / `+` | `JOG_START`, `SIM_STEP`, `JOG_STOP` | `SOURCE_ADAPTER`: source requires `manual_ok` and the visible Manual tab. Incremental sends one `JOG_INCREMENT`; Continuous sends one `JOG_CONTINUOUS` and motion continues until the deferred release issues `JOG_STOP`. Web servo sampling is transport/runtime work, not repeated user jog commands. |
|
| Jog `-` / `+` | `JOG_START`, `SIM_STEP`, `JOG_STOP` | `SOURCE_ADAPTER`: source requires `manual_ok` and the visible Manual tab. Incremental sends one `JOG_INCREMENT`; Continuous sends one `JOG_CONTINUOUS` and motion continues until the deferred release issues `JOG_STOP`. Web servo sampling is transport/runtime work, not repeated user jog commands. |
|
||||||
| Home Axis | `HOME {axis,joint}` | `SOURCE_ADAPTER`: UI uses the fixed map `x/y/z/a/c -> 0/1/2/3/4`; Worker validates the pair, applies the source `manual_ok` gate, enters MANUAL, switches to FREE/joint for the `c.home(joint)` equivalent, waits for the WASM home-limit result, then publishes the canonical joint mask. An unmapped axis, mismatched joint or invalid numeric joint is rejected before any mode/home mutation. |
|
| Home Axis / Home Joint | `HOME {joint}` | `SOURCE_ADAPTER`: select numeric joint, confirm re-home if applicable, ensure MANUAL, native FREE + `do_home_joint(j)`, then servo-driven `do_homing`/`simple_tp_update`. Letter-axis requests reject for BOTH. |
|
||||||
| Home All / Re-home All | `HOME {all:true}` | `SOURCE_ADAPTER`: `manual_ok`, ensure MANUAL, if `all_homed()` ask the native re-home confirmation, then switch to FREE/joint, call the WASM equivalent of `c.home(-1)`, wait, and publish all five homed bits. The fixed Web axis adapter restores its prior world-axis presentation mode after the completed transaction; the source mode used for homing is still recorded. Native text remains Home All; the Web `RE-HOME ALL` label is presentation-only and cannot trigger a hidden home during load. |
|
| Home All / Re-home All | `HOME {all:true}` | `SOURCE_ADAPTER`: manual_ok, ensure MANUAL, re-home confirmation, native FREE + `do_home_joint(-1)`. ACK means accepted; only native homing/homed/home_state establish completion. HOME_SEQUENCE comes from INI. |
|
||||||
| Touch Off | `TOUCH_OFF {axis,value,system,units}` | `SOURCE_ADAPTER`: source UI condition is the Touch Off formula above; task-layer MDI additionally enforces homing unless `NO_FORCE_HOMING`. Prompt for `P0 Current` through `P9 G59.3`, save mode, ensure MDI, scale linear units, issue `G10 L20 <P-token> <axis>[value*scale]`, wait, redraw/reload, restore saved mode and TELEOP. |
|
| Touch Off | `TOUCH_OFF {axis,value,system,units}` | `SOURCE_ADAPTER`: source UI condition is the Touch Off formula above; task-layer MDI additionally enforces homing unless `NO_FORCE_HOMING`. Prompt for `P0 Current` through `P9 G59.3`, save mode, ensure MDI, scale linear units, issue `G10 L20 <P-token> <axis>[value*scale]`, wait, redraw/reload, restore saved mode and TELEOP. |
|
||||||
| Tool Touch Off | `TOOL_TOUCH_OFF {axis,value,system,units}` | LinuxCNC source requires `G10 L10/L11` plus `G43`, but the selected WASM interpreter does not expose canonical tool-offset storage. The control is therefore disabled with `blocked_by_abi_tool_offsets`; it never sends a fake fixed `G43`. |
|
| Tool Touch Off | `TOOL_TOUCH_OFF {axis,value,system,units}` | LinuxCNC source requires `G10 L10/L11` plus `G43`, but the selected WASM interpreter does not expose canonical tool-offset storage. The control is therefore disabled with `blocked_by_abi_tool_offsets`; it never sends a fake fixed `G43`. |
|
||||||
| Override limits | disabled | `SOURCE_BLOCKED`: widget enable requires `on_any_limit`; handler separately requires interpreter IDLE. If already overridden it ensures AUTO; otherwise it ensures MANUAL and calls `c.override_limits()`. The selected WASM ABI cannot represent this canonical path. |
|
| Override limits | disabled | `SOURCE_BLOCKED`: widget enable requires `on_any_limit`; handler separately requires interpreter IDLE. If already overridden it ensures AUTO; otherwise it ensures MANUAL and calls `c.override_limits()`. The selected WASM ABI cannot represent this canonical path. |
|
||||||
@@ -404,15 +403,16 @@ history rows intentionally share the family ID `mdi-history-select`.
|
|||||||
| `toolbar-clear-live-plot` | `clear_live_plot` -> `positionlogger.clear()` | Worker `VISUAL_CLEAR`; always visual-only | Advances `live_path_epoch`, clears Live Plot only and retains static preview |
|
| `toolbar-clear-live-plot` | `clear_live_plot` -> `positionlogger.clear()` | Worker `VISUAL_CLEAR`; always visual-only | Advances `live_path_epoch`, clears Live Plot only and retains static preview |
|
||||||
| `workspace-case-select` | Web selection of the INI/configuration input | Local selection; selected catalog is restricted to supported XYZAC case | Does not load or change machine state until `toolbar-load-case` |
|
| `workspace-case-select` | Web selection of the INI/configuration input | Local selection; selected catalog is restricted to supported XYZAC case | Does not load or change machine state until `toolbar-load-case` |
|
||||||
| `workspace-load-3d-chips` | Web packaged-file equivalent of `open_file_guts` | Fetch published `3D_Chips.ngc`, then `PROGRAM_LOAD`; IDLE only | Same replacement path as Open; retains Live Plot, no automatic Run/Home |
|
| `workspace-load-3d-chips` | Web packaged-file equivalent of `open_file_guts` | Fetch published `3D_Chips.ngc`, then `PROGRAM_LOAD`; IDLE only | Same replacement path as Open; retains Live Plot, no automatic Run/Home |
|
||||||
| `manual-home-all` | Machine Homing menu -> `home_all_joints` | `HOME {all:true}`; `manual_ok`, MANUAL, FREE/joint home, wait, canonical snapshot | Repeat asks source confirmation; Web Re-home label is non-authoritative presentation only; response records the mode phase and five-joint homed mask |
|
| `manual-home-all` | `home_all_joints` | `HOME {all:true}`; ON+IDLE, no concurrent homing/jog | Native homemod selects the configured sequences. No artificial coordinated return-to-zero motion. |
|
||||||
| `switchkins-0`, `switchkins-1`, `switchkins-2` | `M429`, `M428`, `M430` remaps and `motion.switchkins-type` | `KINEMATICS_SELECT`; ON, homed, IDLE, empty TP | Identity/XYZAC/User K canonical type, or stable gate reason |
|
| `switchkins-0`, `switchkins-1`, `switchkins-2` | `M429`, `M428`, `M430` remaps and `motion.switchkins-type` | `KINEMATICS_SELECT`; ON, homed, IDLE, empty TP | Identity/XYZAC/User K canonical type, or stable gate reason |
|
||||||
| `tab-manual`, `tab-mdi` | AXIS notebook F3/F5 `ensure_manual`/`ensure_mdi` | `SET_MODE` adapter under `manual_ok` | Acknowledged MANUAL/MDI task mode. The Web does not expose a separate Joint/World toggle; Home transactions use an internal FREE/joint phase and restore the selected fixed XYZAC world-axis presentation mode after completion. |
|
| `tab-manual`, `tab-mdi` | `ensure_manual` / `ensure_mdi` | `SET_MODE`; manual_ok and motion exclusion | Task mode acknowledgement and WASM motion mode are monitored separately; no local tab is mode authority. |
|
||||||
| `tab-diagnostics` | Web diagnostics boundary | Local tab only | Reads Worker state/gates; sends no command |
|
| `tab-diagnostics` | Web diagnostics boundary | Local tab only | Reads Worker state/gates; sends no command |
|
||||||
| `tab-preview`, `tab-dro` | AXIS preview/DRO presentation | Local tab only | Changes visible projection; machine state unchanged |
|
| `tab-preview`, `tab-dro` | AXIS preview/DRO presentation | Local tab only | Changes visible projection; machine state unchanged |
|
||||||
| `manual-axis-x`, `manual-axis-y`, `manual-axis-z`, `manual-axis-a`, `manual-axis-c` | AXIS axis radio group / `ja_rbutton` | Local axis selection; source manualgroup ON+IDLE | Supplies the selected axis to Jog/Home/Touch Off; sends no motion by itself |
|
| `manual-axis-x`, `manual-axis-y`, `manual-axis-z`, `manual-axis-a`, `manual-axis-c` | AXIS axis radio group / `ja_rbutton` | Local axis selection; source manualgroup ON+IDLE | Supplies the selected world axis to Jog/Touch Off; sends no motion by itself |
|
||||||
| `manual-jog-increment` | `jogincrement` combobox | Local choice: Continuous or configured increment | Determines `JOG_CONTINUOUS` versus `JOG_INCREMENT` payload only |
|
| `manual-jog-increment` | `jogincrement` combobox | Local choice: Continuous or configured increment | Determines `JOG_CONTINUOUS` versus `JOG_INCREMENT` payload only |
|
||||||
| `manual-jog-negative`, `manual-jog-positive` | `jogminus`, `jogplus` -> `jog_on` | `JOG_START`; visible Manual tab and `manual_ok`; source chooses joint/teleop mode from kinematics+homing | Incremental submits once; Continuous remains active until one release/cancel `JOG_STOP` |
|
| `manual-jog-negative`, `manual-jog-positive` | `jogminus`, `jogplus` -> `jog_on` | `JOG_START`; visible Manual tab and `manual_ok`; source chooses joint/teleop mode from kinematics+homing | Incremental submits once; Continuous remains active until one release/cancel `JOG_STOP` |
|
||||||
| `manual-home-axis` | `home_joint` | UI ON+IDLE; `HOME {axis,joint}` validates the fixed XYZAC mapping, then follows the source MANUAL -> FREE/joint -> home transaction | Selected axis is homed through its mapped numeric joint; already-homed joint asks for explicit re-home confirmation; invalid mapping is rejected without mode/home mutation |
|
| `manual-home-joint-select` | Native numeric joint Homing selection | Local integer 0..4; same homing gate | Selects only the target joint; no motion command. |
|
||||||
|
| `manual-home-axis` | `home_joint` / numeric Homing menu | `HOME {joint}`; ON+IDLE and no concurrent motion | Label Home Joint; numeric target only. Native state and motor offset determine the result. |
|
||||||
| `manual-touch-off` | `touch_off_system` | `TOUCH_OFF`; exact Touch Off UI formula, `manual_ok`, valid axis/value/system, plus task-layer homing policy | MDI `G10 L20`, wait, reload and restore saved task/motion mode |
|
| `manual-touch-off` | `touch_off_system` | `TOUCH_OFF`; exact Touch Off UI formula, `manual_ok`, valid axis/value/system, plus task-layer homing policy | MDI `G10 L20`, wait, reload and restore saved task/motion mode |
|
||||||
| `manual-tool-touch-off` | `touch_off_tool` | Permanently disabled for selected ABI | `blocked_by_abi_tool_offsets`; never approximated with `G92`/fixed `G43` |
|
| `manual-tool-touch-off` | `touch_off_tool` | Permanently disabled for selected ABI | `blocked_by_abi_tool_offsets`; never approximated with `G92`/fixed `G43` |
|
||||||
| `manual-override-limits` | `toggle_override_limits` | Permanently disabled for selected ABI | No state mutation; explanatory ABI title |
|
| `manual-override-limits` | `toggle_override_limits` | Permanently disabled for selected ABI | No state mutation; explanatory ABI title |
|
||||||
@@ -449,10 +449,10 @@ React must derive its button gates from that state on the next snapshot.
|
|||||||
| `ESTOP(active=false)` | Source toggle requires current status ESTOP | IO E-stop clears; observed source task state becomes `ESTOP_RESET`; Web may display `STOPPED` only as an adapter label | Power is enabled; motion/manual/MDI remain disabled until ON |
|
| `ESTOP(active=false)` | Source toggle requires current status ESTOP | IO E-stop clears; observed source task state becomes `ESTOP_RESET`; Web may display `STOPPED` only as an adapter label | Power is enabled; motion/manual/MDI remain disabled until ON |
|
||||||
| `POWER(active=true)` | Source ON edge is valid only from `ESTOP_RESET`; it does not require interpreter IDLE | Trajectory enables and observed task state becomes ON | Manual group requires ON+IDLE; program Run still meets UI/taskfile adapter and task-layer homing rules |
|
| `POWER(active=true)` | Source ON edge is valid only from `ESTOP_RESET`; it does not require interpreter IDLE | Trajectory enables and observed task state becomes ON | Manual group requires ON+IDLE; program Run still meets UI/taskfile adapter and task-layer homing rules |
|
||||||
| `POWER(active=false)` | Source toolbar allows the toggle whenever status is not ESTOP, including active execution | Motion/task/IO abort, trajectory disables, volatile-home joints unhome, task plan synchronizes; taskfile retained | Observed state returns to ESTOP_RESET; nonvolatile homing is not falsely cleared by the UI |
|
| `POWER(active=false)` | Source toolbar allows the toggle whenever status is not ESTOP, including active execution | Motion/task/IO abort, trajectory disables, volatile-home joints unhome, task plan synchronizes; taskfile retained | Observed state returns to ESTOP_RESET; nonvolatile homing is not falsely cleared by the UI |
|
||||||
| `HOME(all/axis)` | AXIS homing UI: ON+IDLE; handler `manual_ok`, then MANUAL; `go_home()` selects FREE/joint before `c.home()` | Home authority updates the canonical five-joint `homed[]`; Web axis selection is validated and mapped to one numeric joint for this fixed one-to-one XYZAC case | AUTO's task layer may run only when all required joints are homed unless `NO_FORCE_HOMING=1`; an invalid axis/joint pair never changes mode or home state |
|
| `HOME(all/joint)` | ON+IDLE, no concurrent homing or jog; ensure MANUAL | FREE mode, original homing sequence and joint state machines; `homing`, `homed`, `home_state`, motor offset and position from WASM | Home is not AUTO execution. INTERP_IDLE does not mean homing is inactive. |
|
||||||
| `PROGRAM_LOAD` | Native Open/Reload requires interpreter IDLE; Web additionally excludes active TP/MDI queue and concurrent parse | Save mode, ensure MANUAL, restore mode, set line 0, synchronize, open/replace taskfile and canon, parse status-derived preview, then apply tab mode | New taskfile/preview replace the old selection. Static preview replacement is independent of Live Plot data. |
|
| `PROGRAM_LOAD` | Native Open/Reload requires interpreter IDLE; Web additionally excludes active TP/MDI queue and concurrent parse | Save mode, ensure MANUAL, restore mode, set line 0, synchronize, open/replace taskfile and canon, parse status-derived preview, then apply tab mode | New taskfile/preview replace the old selection. Static preview replacement is independent of Live Plot data. |
|
||||||
| `KINEMATICS_SELECT` | Machine on, homed, idle, TP queue empty, valid type 0/1/2 | M429/M428/M430 selection updates canonical kinematics and the HAL switchkins type | KINEMATICS_BOTH remains unchanged; a non-empty queue blocks selection |
|
| `KINEMATICS_SELECT` | Machine on, homed, idle, TP queue empty, valid type 0/1/2 | M429/M428/M430 selection updates canonical kinematics and the HAL switchkins type | KINEMATICS_BOTH remains unchanged; a non-empty queue blocks selection |
|
||||||
| `SET_MODE(MANUAL)` | Source task rejects leaving active AUTO; tab handler also requires `manual_ok` | Task mode MANUAL acknowledged; explicit joint/teleop switching remains the stated boundary | Manual widgets are still controlled by ON+IDLE, not mode alone |
|
| `SET_MODE(MANUAL)` | Source task rejects leaving active AUTO; tab handler also requires `manual_ok` | Task mode MANUAL acknowledged; motion mode selects TELEOP when homed, FREE otherwise | Manual widgets are still controlled by ON+IDLE, not mode alone |
|
||||||
| `SET_MODE(MDI)` | Tab handler requires `manual_ok`; task mode transition follows source motion-mode calls | Task mode MDI | MDI widgets use ON+queue-capacity; submission still calls `manual_ok` |
|
| `SET_MODE(MDI)` | Tab handler requires `manual_ok`; task mode transition follows source motion-mode calls | Task mode MDI | MDI widgets use ON+queue-capacity; submission still calls `manual_ok` |
|
||||||
| `PROGRAM_RUN` | Native toolbar formula is ON+IDLE and does not test taskfile. Web adds current-taskfile validation; task acceptance adds all-homed unless `NO_FORCE_HOMING=1`. Completed/aborted are valid idle starts. | `task_run`: warning, ensure AUTO, `AUTO_RUN(start_line)`, reset next start line to 0. Task layer reopens the retained file when plan is closed. | During non-IDLE, native Run relief is pressed and Open/Reload/Run/manual controls disable; on IDLE, direct Run is available again. |
|
| `PROGRAM_RUN` | Native toolbar formula is ON+IDLE and does not test taskfile. Web adds current-taskfile validation; task acceptance adds all-homed unless `NO_FORCE_HOMING=1`. Completed/aborted are valid idle starts. | `task_run`: warning, ensure AUTO, `AUTO_RUN(start_line)`, reset next start line to 0. Task layer reopens the retained file when plan is closed. | During non-IDLE, native Run relief is pressed and Open/Reload/Run/manual controls disable; on IDLE, direct Run is available again. |
|
||||||
| `PROGRAM_PAUSE` | Source handler: AUTO plus READING/WAITING; combined toolbar: AUTO/MDI plus non-IDLE and not currently paused | Trajectory pauses, previous interpreter state is saved, interpreter becomes PAUSED | Combined toolbar remains enabled and becomes Resume; Stop, E-stop and Power remain enabled by their own formulas |
|
| `PROGRAM_PAUSE` | Source handler: AUTO plus READING/WAITING; combined toolbar: AUTO/MDI plus non-IDLE and not currently paused | Trajectory pauses, previous interpreter state is saved, interpreter becomes PAUSED | Combined toolbar remains enabled and becomes Resume; Stop, E-stop and Power remain enabled by their own formulas |
|
||||||
@@ -628,7 +628,7 @@ Run 仍需用户单独操作,不能因加载文件而自动上电、回零或
|
|||||||
| Open、Reload、案例和打包程序 | 四条入口共用 `gates.load`,包含 PAUSED 与活动 TP 队列;Reload 还要求已有 taskfile | 文件读取完成后再次检查门控,再提交一次 `PROGRAM_LOAD` | 用户取消文件选择不发命令;门控已变化则不替换编辑器文本、路径或预览;解析失败属于已选新文件的源码错误 |
|
| Open、Reload、案例和打包程序 | 四条入口共用 `gates.load`,包含 PAUSED 与活动 TP 队列;Reload 还要求已有 taskfile | 文件读取完成后再次检查门控,再提交一次 `PROGRAM_LOAD` | 用户取消文件选择不发命令;门控已变化则不替换编辑器文本、路径或预览;解析失败属于已选新文件的源码错误 |
|
||||||
| 点动和主轴调速按住按钮 | 主指针按下或聚焦按钮的 Space/Enter 按下;重复 keydown/第二指针不重复提交 | 记录本次手势的 axis/direction/increment 与 release 操作;按下和释放有固定命令顺序 | pointerup/cancel/leave/lost capture、keyup、窗口失焦、页面隐藏、离开 Manual 页只结束同一手势一次;release 早于 ACK 时仍按 start→stop 顺序处理 |
|
| 点动和主轴调速按住按钮 | 主指针按下或聚焦按钮的 Space/Enter 按下;重复 keydown/第二指针不重复提交 | 记录本次手势的 axis/direction/increment 与 release 操作;按下和释放有固定命令顺序 | pointerup/cancel/leave/lost capture、keyup、窗口失焦、页面隐藏、离开 Manual 页只结束同一手势一次;release 早于 ACK 时仍按 start→stop 顺序处理 |
|
||||||
| 坐标轴、步长、DRO、视图、图层、选项卡 | 使用表内本地显示状态;切换 Manual/MDI 另走 `SET_MODE` | 更新选择或投影,不能变更 canonical machine state | 每个选项均验证显示变化;隐藏 Live Plot 不得删除轨迹;旋转、平移和缩放须验证实际画布变化 |
|
| 坐标轴、步长、DRO、视图、图层、选项卡 | 使用表内本地显示状态;切换 Manual/MDI 另走 `SET_MODE` | 更新选择或投影,不能变更 canonical machine state | 每个选项均验证显示变化;隐藏 Live Plot 不得删除轨迹;旋转、平移和缩放须验证实际画布变化 |
|
||||||
| 系统对刀、重复回零 | 源码门控通过后显示参数/确认对话框 | 确认后只发送一次 `TOUCH_OFF` 或 `HOME` | 取消不发运动/对刀命令;Home All 的原生 ensure MANUAL 在确认之前,Web 确认前模式处理属于适配差异;空白/非有限数字/非法 P 或 G 系统标识不提交 |
|
| 系统对刀、重复回零 | 源码门控通过后显示参数/确认对话框 | 确认后只发送一次 `TOUCH_OFF` 或 `HOME` | 取消不发运动/对刀命令;Home All 在确认前 ensure MANUAL,单关节重复回零在确认后 ensure MANUAL;空白/非有限数字/非法 P 或 G 系统标识不提交 |
|
||||||
| About 和菜单 | 本地打开、关闭、Escape;About 打开时焦点进入,关闭后返回入口 | 显示/隐藏相应内容 | 弹窗打开期间不触发普通机器快捷键;F1 急停仍可用;关闭动作不触发 Stop |
|
| About 和菜单 | 本地打开、关闭、Escape;About 打开时焦点进入,关闭后返回入口 | 显示/隐藏相应内容 | 弹窗打开期间不触发普通机器快捷键;F1 急停仍可用;关闭动作不触发 Stop |
|
||||||
| ABI/桌面边界控件 | 原生 disabled 属性与明确原因 | 无成功路径 | `SOURCE_BLOCKED` 不得出现机器命令或本地假开关;全部在浏览器清单中检查 |
|
| ABI/桌面边界控件 | 原生 disabled 属性与明确原因 | 无成功路径 | `SOURCE_BLOCKED` 不得出现机器命令或本地假开关;全部在浏览器清单中检查 |
|
||||||
|
|
||||||
@@ -792,38 +792,83 @@ block whose target already equals the current canonical pose is a zero-motion
|
|||||||
block: it advances the interpreter line but must not enqueue an invalid
|
block: it advances the interpreter line but must not enqueue an invalid
|
||||||
zero-length TP segment.
|
zero-length TP segment.
|
||||||
|
|
||||||
### Homing must execute through the WASM trajectory
|
### Native Homing and State Monitoring (2026-09-09)
|
||||||
|
|
||||||
`sim_kinematics_home_limit_home()` and
|
This section supersedes the former axis-to-joint alias and coordinated-TP
|
||||||
`sim_kinematics_home_limit_home_joint()` are home-state authorities. They
|
return-to-zero implementation. The reference is the pinned native source,
|
||||||
calculate and commit the canonical homed mask, joint values and forward pose;
|
including the selected INI, not the previous browser tests.
|
||||||
they are not a substitute for the LinuxCNC `c.home()` motion command. The Web
|
|
||||||
must therefore use a two-phase transaction whenever the current pose differs
|
| Requested status | Correct native field / source | Web snapshot |
|
||||||
from the home target:
|
| --- | --- | --- |
|
||||||
|
| Task mode | `emcStatus->task.mode` | `source_status.task.mode`: MANUAL/MDI/AUTO |
|
||||||
|
| Machine state | `emcStatus->task.state` | `source_status.task.state`: STATE_ESTOP/STATE_ESTOP_RESET/STATE_ON |
|
||||||
|
| Interpreter state | `emcStatus->task.interpState` | `source_status.task.interpState`: INTERP_IDLE/READING/WAITING/PAUSED |
|
||||||
|
| Motion mode | `emcStatus->motion.traj.mode` | `source_status.traj.mode`: FREE/COORD/TELEOP |
|
||||||
|
| Motion type | `emcStatus->motion.traj.motion_type` | `source_status.traj.motion_type`: motion classification, not mode |
|
||||||
|
| Joint homing/homed | `get_homing(j)` / `get_homed(j)`, reflected in joint status | `source_status.joint[j].homing/homed` |
|
||||||
|
| Joint home state | `homing.c::H[j].home_state`, HAL `joint.N.home-state` | `source_status.joint[j].home_state/home_state_name` |
|
||||||
|
|
||||||
|
The native NML joint status does not directly expose a `home_state` member.
|
||||||
|
WASM reads the original homemod state that drives the HAL pin. Task status
|
||||||
|
remains the existing WASM task/interpreter adapter; this is not a claim that
|
||||||
|
the entire NML task process has been ported. Motion mode is stored in WASM.
|
||||||
|
|
||||||
|
The visible monitor retains only Task mode, Task state, Interpreter, Motion
|
||||||
|
mode, Motion type and Homing. Per-joint status, positions, motor offsets and
|
||||||
|
bounded servo transition history remain in the Worker snapshot for logic and
|
||||||
|
diagnostics, but their table and history list are not rendered on the panel.
|
||||||
|
The detailed status strip is the single visible machine-state summary. The
|
||||||
|
duplicate footer and header operation badge are removed. Connection status
|
||||||
|
remains separately labelled. The case strip shows the current program basename
|
||||||
|
with its full path in a tooltip, and renders the command result once. Existing
|
||||||
|
machine/operation values remain on the main element as data attributes for
|
||||||
|
browser diagnostics; no control command or gate changes with this layout.
|
||||||
|
Home Joint, the numeric joint selector and Home All are rendered in the Manual
|
||||||
|
tab's action area; the standalone Machine sequence heading is not rendered.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
HOME click
|
HOME(all/joint)
|
||||||
-> validate ON+IDLE/manual_ok and axis-to-joint mapping
|
-> ON+IDLE and no active motion; ensure MANUAL
|
||||||
-> ensure MANUAL -> select FREE/joint homing phase
|
-> native FREE, do_home_joint(-1 or numeric joint)
|
||||||
-> WASM kinematics computes target pose without committing home state
|
-> ACK: accepted; does not mean homed
|
||||||
-> enqueue one LinuxCNC TP homing move to that target
|
-> servo cycle: original do_homing() then simple_tp_update()
|
||||||
-> return HOME {homing.phase:"moving"}; keep current pose and homed state
|
-> forward kinematics of resulting joint coordinates
|
||||||
-> browser scheduler sends SIM_STEP task/servo ticks
|
-> synchronize coordinated planner coordinate origin to native result
|
||||||
-> TP reaches target and queue becomes empty
|
-> publish homing/homed/home_state and motor position
|
||||||
-> commit sim_kinematics_home(_joint)
|
-> native all-homed edge: control.c switches to TELEOP; partial home remains FREE
|
||||||
-> synchronize interpreter/TP canonical position
|
-> native homing inactive: synchronize interpreter
|
||||||
-> restore the prior Web presentation mode
|
|
||||||
-> publish HOME {homing.phase:"complete", wasm_home_limit, pose}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When the current pose already equals the target, the transaction may commit in
|
`HOME_SEARCH_VEL=0` and default `HOME_LATCH_VEL=0` select home-at-current-
|
||||||
the same command because there is no motion segment to execute. A Home request
|
position (`homing.c:805`). `HOME_SET_SWITCH_POSITION` adjusts joint coordinates
|
||||||
must never call `setPosition()` to jump the TP to zero, must never change the
|
and motor offset without moving the motor. The final move targets INI HOME
|
||||||
React pose locally, and must never advance `home_sequence` before the queued
|
through the joint `free_tp` (`homing.c:1248`). In the selected XYZAC INI,
|
||||||
motion has completed. `HOME ALL` targets all five configured joint home values;
|
HOME and HOME_OFFSET are zero; re-home after Jog can therefore reset displayed
|
||||||
`Home Axis` targets only the mapped joint and lets the selected XYZAC
|
joint coordinates with unchanged physical motor position. This is native
|
||||||
kinematics calculate the resulting world pose. A pending home blocks another
|
behavior. It must not be replaced by G0, G53 or a coordinated TP line to zero.
|
||||||
Home until its queue has completed or an ESTOP/Power Off aborts it.
|
|
||||||
|
A nonzero HOME relative to HOME_OFFSET executes a real native free_tp final
|
||||||
|
move. Search/index inputs use homemod inputs; the selected browser fixture
|
||||||
|
has no physical switches. Unlock hardware is unsupported and configurations
|
||||||
|
requiring it are rejected, rather than faking an unlocked device.
|
||||||
|
|
||||||
|
Home completion never follows coordinated queue depth. Abort, ESTOP and power
|
||||||
|
off cancel homemod; aborted joints do not become homed. Power off additionally
|
||||||
|
applies VOLATILE_HOME. During homing, conflicting Run/MDI/Jog/Load/mode/recovery
|
||||||
|
requests reject, while status sampling and abort remain available. These extra
|
||||||
|
Web UI exclusions are explicit adapters around the native button formulas.
|
||||||
|
|
||||||
|
`visual.pose_epoch` follows native home command sequence. It invalidates stale
|
||||||
|
display playback without clearing Live Plot. Neither this sequence nor the
|
||||||
|
displayed zero value is evidence of homing completion.
|
||||||
|
|
||||||
|
Validation boundary: the native homing state round-trip preserves joint/motor
|
||||||
|
positions and offsets. The existing general recovery audit still reports
|
||||||
|
`endpoint_at_checkpoint_equal=false` for planner/HAL fields; a roughly 7.7 MB
|
||||||
|
full-history RECOVER envelope also exceeds the existing ccall-array stack
|
||||||
|
boundary. These are not closed by this homing change and must not be included
|
||||||
|
in claims of full recovery parity. Native homing checkpoint bytes are stored
|
||||||
|
sparsely to avoid copying unused joint compensation tables into JSON.
|
||||||
|
|
||||||
### Touch-off command encoding
|
### Touch-off command encoding
|
||||||
|
|
||||||
@@ -902,7 +947,7 @@ caller with its obsolete flag. Clear Live Plot is an explicit user operation.
|
|||||||
|
|
||||||
1. `web/worker/sim-session.mjs` is the command gate and transaction boundary.
|
1. `web/worker/sim-session.mjs` is the command gate and transaction boundary.
|
||||||
2. `web/worker/wasm-linuxcnc-tp.mjs`, `wasm-program-runtime.mjs`,
|
2. `web/worker/wasm-linuxcnc-tp.mjs`, `wasm-program-runtime.mjs`,
|
||||||
`wasm-xyzac-io.mjs` and `wasm-xyzac-device-io.mjs` are the only authorities
|
`wasm-native-homing.mjs`, `wasm-xyzac-io.mjs` and `wasm-xyzac-device-io.mjs` are the only authorities
|
||||||
for selected-case motion, interpreter and device state.
|
for selected-case motion, interpreter and device state.
|
||||||
3. `web/react/src/App.tsx` may render a button disabled from a gate snapshot,
|
3. `web/react/src/App.tsx` may render a button disabled from a gate snapshot,
|
||||||
but a Worker gate is still mandatory because UI state can be stale.
|
but a Worker gate is still mandatory because UI state can be stale.
|
||||||
@@ -930,14 +975,14 @@ caller with its obsolete flag. Clear Live Plot is an explicit user operation.
|
|||||||
|
|
||||||
Focused evidence for this contract is kept in
|
Focused evidence for this contract is kept in
|
||||||
`web/worker/test_xyzac_touch_off_source_flow.mjs`,
|
`web/worker/test_xyzac_touch_off_source_flow.mjs`,
|
||||||
`web/worker/test_xyzac_home_source_flow.mjs`,
|
`web/worker/test_native_homing.mjs`,
|
||||||
`web/worker/test_xyzac_panel_buttons.mjs` and
|
`web/worker/test_xyzac_panel_buttons.mjs` and
|
||||||
`scripts/test_xyzac_panel_buttons_browser.mjs`. The completed/aborted direct
|
`scripts/test_xyzac_panel_buttons_browser.mjs`. The completed/aborted direct
|
||||||
rerun contract is covered by `web/worker/test_xyzac_program_rerun.mjs` and the
|
rerun contract is covered by `web/worker/test_xyzac_program_rerun.mjs`. The
|
||||||
direct visible Jog -> Home Axis/Home All sequence at
|
direct visible Jog -> Home Joint/Home All sequence at
|
||||||
`scripts/test_xyzac_home_reset_browser.mjs` records the in-flight TP pose,
|
`scripts/test_xyzac_home_reset_browser.mjs` records native homing transitions,
|
||||||
queue depth and final zero pose on the local `:5175` panel.
|
joint coordinates and unchanged motor positions for no-search homing on the local `:5175` panel.
|
||||||
published-site execution sequence is covered by
|
The published-site execution sequence is covered by
|
||||||
`scripts/test_xyzac_program_execution_browser_strict.mjs`.
|
`scripts/test_xyzac_program_execution_browser_strict.mjs`.
|
||||||
The source-surface audit is enforced by `scripts/test_axis_control_contract.mjs`.
|
The source-surface audit is enforced by `scripts/test_axis_control_contract.mjs`.
|
||||||
The press/release, focus-loss, MDI history, modal keyboard and desktop/mobile
|
The press/release, focus-loss, MDI history, modal keyboard and desktop/mobile
|
||||||
@@ -948,8 +993,8 @@ OFF/ESTOP overrides and the source mode transitions against real WASM.
|
|||||||
`update_state` procedure across 576 combinations and compares 13 widget
|
`update_state` procedure across 576 combinations and compares 13 widget
|
||||||
predicates (7488 assertions), rather than comparing two rewritten formulas.
|
predicates (7488 assertions), rather than comparing two rewritten formulas.
|
||||||
`web/worker/test_axis_source_execution.mjs` verifies repeated Step, queued
|
`web/worker/test_axis_source_execution.mjs` verifies repeated Step, queued
|
||||||
motion Step, WAITING/MDI Pause-Resume, source OFF state, fixed XYZAC
|
motion Step, WAITING/MDI Pause-Resume, source OFF state, KINEMATICS_BOTH
|
||||||
axis-to-joint Home Axis mapping and spindle override 0-to-100 restoration using
|
world-axis rejection and spindle override 0-to-100 restoration using
|
||||||
the actual WASM runtime.
|
the actual WASM runtime.
|
||||||
The exhaustive control-ID audit is enforced by
|
The exhaustive control-ID audit is enforced by
|
||||||
`scripts/test_axis_control_inventory.mjs` and
|
`scripts/test_axis_control_inventory.mjs` and
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.6 MiB |
@@ -13,7 +13,7 @@ const [contract, app, panel, preview] = await Promise.all([
|
|||||||
const controlIds = [
|
const controlIds = [
|
||||||
"menu-file", "menu-file-edit-tool-data", "menu-file-quit", "menu-help", "menu-help-about", "about-close",
|
"menu-file", "menu-file-edit-tool-data", "menu-file-quit", "menu-help", "menu-help-about", "about-close",
|
||||||
"toolbar-estop", "toolbar-power", "toolbar-open", "toolbar-load-case", "toolbar-reload", "toolbar-run", "toolbar-step", "toolbar-pause-resume", "toolbar-stop", "toolbar-block-delete", "toolbar-optional-pause", "toolbar-zoom-in", "toolbar-zoom-out", "toolbar-view-z", "toolbar-view-z2", "toolbar-view-x", "toolbar-view-y", "toolbar-view-y2", "toolbar-view-p", "toolbar-rotate", "toolbar-clear-live-plot",
|
"toolbar-estop", "toolbar-power", "toolbar-open", "toolbar-load-case", "toolbar-reload", "toolbar-run", "toolbar-step", "toolbar-pause-resume", "toolbar-stop", "toolbar-block-delete", "toolbar-optional-pause", "toolbar-zoom-in", "toolbar-zoom-out", "toolbar-view-z", "toolbar-view-z2", "toolbar-view-x", "toolbar-view-y", "toolbar-view-y2", "toolbar-view-p", "toolbar-rotate", "toolbar-clear-live-plot",
|
||||||
"workspace-case-select", "workspace-load-3d-chips", "manual-home-all",
|
"workspace-case-select", "workspace-load-3d-chips", "manual-home-all", "manual-home-joint-select",
|
||||||
"switchkins-0", "switchkins-1", "switchkins-2",
|
"switchkins-0", "switchkins-1", "switchkins-2",
|
||||||
"tab-manual", "tab-mdi", "tab-diagnostics", "tab-preview", "tab-dro",
|
"tab-manual", "tab-mdi", "tab-diagnostics", "tab-preview", "tab-dro",
|
||||||
"manual-axis-x", "manual-axis-y", "manual-axis-z", "manual-axis-a", "manual-axis-c",
|
"manual-axis-x", "manual-axis-y", "manual-axis-z", "manual-axis-a", "manual-axis-c",
|
||||||
|
|||||||
@@ -1,79 +1,107 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
const modulePath = process.env.PLAYWRIGHT_MODULE;
|
const imported = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href);
|
||||||
assert.ok(modulePath, "PLAYWRIGHT_MODULE is required");
|
const { chromium } = imported.default ?? imported;
|
||||||
const imported = await import(pathToFileURL(modulePath).href);
|
const url = process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5175/";
|
||||||
const playwright = imported.default ?? imported;
|
const browser = await chromium.launch({ headless: false, slowMo: 100, executablePath: "/usr/bin/google-chrome", args: ["--disable-gpu"] });
|
||||||
const browser = await playwright.chromium.launch({
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } });
|
||||||
headless: false,
|
const errors = [];
|
||||||
executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome",
|
let confirmHome = true;
|
||||||
args: ["--disable-gpu", "--ignore-certificate-errors", "--window-size=1440,1000"],
|
page.on("pageerror", (e) => errors.push(e.message));
|
||||||
});
|
page.on("dialog", async (d) => { await page.waitForTimeout(600); if (d.type() === "confirm" && confirmHome) await d.accept(); else await d.dismiss(); });
|
||||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
await page.addInitScript(() => {
|
||||||
const main = page.locator("main.axis-app");
|
window.homeResults = []; window.homeRequests = [];
|
||||||
page.on("dialog", async (dialog) => { if (dialog.type() === "confirm") await dialog.accept(); else await dialog.dismiss(); });
|
const Original = window.Worker;
|
||||||
const state = () => main.evaluate((element) => {
|
window.Worker = class extends Original {
|
||||||
const preview = element.querySelector("[data-xyzac-preview]");
|
constructor(...args) {
|
||||||
return {
|
super(...args);
|
||||||
loading: element.dataset.axisProgramLoading,
|
this.addEventListener("message", ({ data }) => {
|
||||||
homed: element.dataset.axisHomed,
|
const p = data.payload;
|
||||||
pose: preview?.dataset.xyzacLivePose,
|
window.homeResults.push({ seq: data.seq, kind: data.kind, command: p?.command, home: p?.homing,
|
||||||
hud: element.querySelector(".axis-preview-hud")?.textContent ?? "",
|
status: p?.source_status ?? p?.panel_snapshot?.source_status,
|
||||||
queue: element.dataset.axisTpQueueDepth,
|
pose: p?.panel_snapshot?.visual?.pose, epoch: p?.panel_snapshot?.visual?.live_path_epoch });
|
||||||
command: element.querySelector(".command-status")?.textContent ?? "",
|
if (window.homeResults.length > 500) window.homeResults.shift();
|
||||||
error: element.querySelector(".diagnostic-error")?.textContent ?? null,
|
});
|
||||||
|
}
|
||||||
|
postMessage(data, ...args) { window.homeRequests.push({ seq: data.seq, kind: data.kind, payload: data.payload }); super.postMessage(data, ...args); }
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const wait = async (predicate, label) => {
|
const control = (id) => page.locator(`[data-axis-control="${id}"]`);
|
||||||
const started = Date.now(); let latest;
|
const status = () => page.evaluate(() => window.homeResults.filter((r) => r.status).at(-1).status);
|
||||||
while (Date.now() - started < 45000) { latest = await state(); if (predicate(latest)) return latest; await page.waitForTimeout(100); }
|
const home = async (id) => {
|
||||||
throw new Error(`${label}:${JSON.stringify(latest)}`);
|
const seq = await page.evaluate(() => window.homeRequests.at(-1)?.seq ?? 0);
|
||||||
|
await control(id).click();
|
||||||
|
await page.waitForFunction((seq) => window.homeResults.some((r) => r.seq > seq && r.home?.phase === "accepted"), seq);
|
||||||
|
await page.waitForFunction((seq) => window.homeResults.some((r) => r.seq > seq && r.status?.homing_history?.length > 1 && !r.status.homing_active), seq);
|
||||||
|
const result = await status();
|
||||||
|
assert.equal(result.task.mode, "MANUAL"); assert.equal(result.task.interpState, "INTERP_IDLE"); assert.equal(result.traj.mode, result.joint.every((j) => j.homed) ? "TELEOP" : "FREE");
|
||||||
|
assert.ok(result.homing_history.some((event) => event.joint.some((j) => j.home_state === 21 && j.homing)));
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const jog = async (axis) => {
|
||||||
|
await control(`manual-axis-${axis}`).check();
|
||||||
|
const button = control("manual-jog-positive");
|
||||||
|
await button.scrollIntoViewIfNeeded();
|
||||||
|
const box = await button.boundingBox();
|
||||||
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
await page.mouse.down(); await page.waitForTimeout(900); await page.mouse.up();
|
||||||
|
await page.waitForFunction(() => document.querySelector("main").dataset.axisTpQueueDepth === "0");
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
return status();
|
||||||
};
|
};
|
||||||
const control = (id) => main.locator(`[data-axis-control="${id}"]`);
|
|
||||||
const click = async (id) => { const button = control(id); await wait(async () => await button.count() > 0 && !await button.isDisabled(), `${id}_enabled`); await button.click(); };
|
|
||||||
const pose = async () => JSON.parse((await state()).pose ?? "{}");
|
|
||||||
const screenshot = async (name) => { await page.screenshot({ path: `/tmp/axis-home-reset-${name}.png` }); console.log(name, JSON.stringify(await state())); };
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await page.goto(process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5175/", { waitUntil: "domcontentloaded" });
|
await page.goto(url);
|
||||||
await wait((value) => value.loading === "false", "startup");
|
await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramLoading === "false");
|
||||||
await click("toolbar-power"); await wait((value) => value.command.includes("POWER acknowledged"), "power");
|
await control("toolbar-power").click();
|
||||||
await click("manual-home-all"); await wait((value) => value.command.includes("HOME acknowledged") && value.homed === "[true,true,true,true,true]", "home_all");
|
await home("manual-home-all");
|
||||||
await screenshot("01-home-all-zero");
|
for (const [joint, axis] of ["x", "y", "z", "a", "c"].entries()) {
|
||||||
await control("manual-axis-x").check();
|
process.stderr.write(`Visible UI: Jog ${axis.toUpperCase()}, then Home Joint ${joint}\n`);
|
||||||
const jog = control("manual-jog-positive");
|
const before = await jog(axis);
|
||||||
await jog.dispatchEvent("pointerdown", { pointerId: 41, button: 0 });
|
assert.ok(before.joint[joint].position > 0);
|
||||||
await wait((value) => value.command.includes("JOG_START acknowledged"), "jog_start");
|
await control("manual-home-joint-select").selectOption(String(joint));
|
||||||
await page.waitForTimeout(450);
|
const after = await home("manual-home-axis");
|
||||||
await jog.dispatchEvent("pointerup", { pointerId: 41, button: 0 });
|
for (let j = 0; j < 5; j++) {
|
||||||
await wait((value) => value.command.includes("JOG_STOP acknowledged") && value.queue === "0", "jog_stop");
|
assert.ok(Math.abs(after.joint[j].motor_position - before.joint[j].motor_position) < 1e-8);
|
||||||
const jogged = await pose(); assert.ok(Math.abs(Number(jogged.x)) > 1e-6, JSON.stringify(jogged));
|
assert.ok(Math.abs(after.joint[j].position - (j === joint ? 0 : before.joint[j].position)) < 1e-8);
|
||||||
await screenshot("02-jogged-x");
|
}
|
||||||
await click("manual-home-axis");
|
|
||||||
const movingAxis = await wait((value) => value.command.includes("HOME acknowledged") && value.queue !== "0", "home_axis_motion");
|
|
||||||
const movingAxisPose = JSON.parse(movingAxis.pose); assert.ok(Math.abs(Number(movingAxisPose.x)) > 1e-6, JSON.stringify(movingAxisPose));
|
|
||||||
await screenshot("03-home-axis-moving");
|
|
||||||
const homedAxis = await wait((value) => value.command.includes("HOME acknowledged") && value.queue === "0", "home_axis");
|
|
||||||
const homeAxisPose = JSON.parse(homedAxis.pose); assert.ok(Math.abs(Number(homeAxisPose.x)) < 1e-8, JSON.stringify(homeAxisPose));
|
|
||||||
await screenshot("04-home-axis-zero");
|
|
||||||
for (const [axis, pointerId] of [["y", 42], ["z", 43], ["a", 44], ["c", 45]]) {
|
|
||||||
await control(`manual-axis-${axis}`).check();
|
|
||||||
const button = control("manual-jog-negative");
|
|
||||||
await button.dispatchEvent("pointerdown", { pointerId, button: 0 });
|
|
||||||
await wait((value) => value.command.includes("JOG_START acknowledged"), `${axis}_jog_start`);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await button.dispatchEvent("pointerup", { pointerId, button: 0 });
|
|
||||||
await wait((value) => value.command.includes("JOG_STOP acknowledged") && value.queue === "0", `${axis}_jog_stop`);
|
|
||||||
}
|
}
|
||||||
await screenshot("05-multi-axis-jogged");
|
const before = await jog("x");
|
||||||
await click("manual-home-all");
|
confirmHome = false;
|
||||||
const movingAll = await wait((value) => value.command.includes("HOME acknowledged") && value.queue !== "0", "home_all_motion");
|
const count = await page.evaluate(() => window.homeRequests.filter((r) => r.kind === "HOME").length);
|
||||||
const movingAllPose = JSON.parse(movingAll.pose); assert.ok(Object.entries(movingAllPose).some(([axis, value]) => ["x", "y", "z", "a", "c"].includes(axis) && Math.abs(Number(value)) > 1e-6), JSON.stringify(movingAllPose));
|
await control("manual-home-all").click(); await page.waitForTimeout(800);
|
||||||
await screenshot("06-home-all-moving");
|
assert.equal(await page.evaluate(() => window.homeRequests.filter((r) => r.kind === "HOME").length), count);
|
||||||
const final = await wait((value) => value.command.includes("HOME acknowledged") && value.queue === "0", "home_all_again");
|
confirmHome = true;
|
||||||
const finalPose = JSON.parse(final.pose);
|
const after = await home("manual-home-all");
|
||||||
for (const axis of ["x", "y", "z", "a", "c"]) assert.ok(Math.abs(Number(finalPose[axis])) < 1e-8, `${axis}:${JSON.stringify(finalPose)}`);
|
assert.deepEqual(after.joint.map((j) => j.motor_position), before.joint.map((j) => j.motor_position));
|
||||||
await screenshot("07-home-all-zero");
|
assert.ok(after.joint.every((j) => j.homed && Math.abs(j.position) < 1e-8));
|
||||||
console.log(JSON.stringify({ schema: "xyzac-home-reset-browser.v1", status: "verified", url: process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5175/", jogged, home_axis: homeAxisPose, home_all: finalPose, screenshots: ["/tmp/axis-home-reset-01-home-all-zero.png", "/tmp/axis-home-reset-02-jogged-x.png", "/tmp/axis-home-reset-03-home-axis-moving.png", "/tmp/axis-home-reset-04-home-axis-zero.png", "/tmp/axis-home-reset-05-multi-axis-jogged.png", "/tmp/axis-home-reset-06-home-all-moving.png", "/tmp/axis-home-reset-07-home-all-zero.png"] }));
|
await control("tab-manual").click();
|
||||||
|
await page.waitForFunction(() => window.homeResults.filter((r) => r.status).at(-1)?.status.traj.mode === "TELEOP");
|
||||||
|
await page.locator('[aria-label="LinuxCNC status"]').scrollIntoViewIfNeeded();
|
||||||
|
await page.screenshot({ path: "/tmp/axis-native-home-desktop.png", fullPage: true });
|
||||||
|
const pixelCheck = async () => page.locator("canvas[data-engine]").evaluate((canvas) => {
|
||||||
|
const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
|
||||||
|
const data = new Uint8Array(canvas.width * canvas.height * 4);
|
||||||
|
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||||
|
const colors = new Set(); for (let i=0;i<data.length;i+=16) colors.add(`${data[i]},${data[i+1]},${data[i+2]}`);
|
||||||
|
return colors.size;
|
||||||
|
});
|
||||||
|
assert.ok(await pixelCheck() > 5);
|
||||||
|
await control("toolbar-run").click();
|
||||||
|
await page.waitForFunction(() => window.homeResults.filter((r) => r.status).at(-1)?.status.task.mode === "AUTO");
|
||||||
|
assert.equal((await status()).traj.mode, "COORD");
|
||||||
|
await control("toolbar-pause-resume").click();
|
||||||
|
await page.waitForFunction(() => window.homeResults.filter((r) => r.status).at(-1)?.status.task.interpState === "INTERP_PAUSED");
|
||||||
|
await control("tab-manual").click(); assert.equal((await status()).task.mode, "AUTO");
|
||||||
|
assert.equal(await control("manual-home-all").isDisabled(), true);
|
||||||
|
await control("toolbar-stop").click();
|
||||||
|
await page.waitForFunction(() => document.querySelector("main").dataset.axisTpQueueDepth === "0");
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await home("manual-home-all");
|
||||||
|
await page.locator('[aria-label="LinuxCNC status"]').scrollIntoViewIfNeeded();
|
||||||
|
await page.screenshot({ path: "/tmp/axis-native-home-mobile.png", fullPage: true });
|
||||||
|
assert.ok(await pixelCheck() > 5);
|
||||||
|
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(JSON.stringify({ status: "verified", url, checks: ["visible-five-joint-home", "motor-position-preserved", "native-state-history", "cancel", "auto-paused-mode", "desktop-mobile-canvas"] }));
|
||||||
} finally { await browser.close(); }
|
} finally { await browser.close(); }
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ const state = () => main.evaluate((element) => {
|
|||||||
loading: element.dataset.axisProgramLoading,
|
loading: element.dataset.axisProgramLoading,
|
||||||
path: element.dataset.axisProgramPath,
|
path: element.dataset.axisProgramPath,
|
||||||
program: element.dataset.axisProgramStatus,
|
program: element.dataset.axisProgramStatus,
|
||||||
operation: element.querySelector(".operation-state")?.textContent?.trim(),
|
operation: `Operation ${element.dataset.axisStateOperation}`,
|
||||||
machine: element.querySelector(".machine-state")?.textContent?.trim(),
|
machine: `Machine ${element.dataset.axisStateMachine}`,
|
||||||
homed: element.dataset.axisHomed,
|
homed: element.dataset.axisHomed,
|
||||||
line: element.dataset.axisCurrentLine,
|
line: element.dataset.axisCurrentLine,
|
||||||
queue: element.dataset.axisTpQueueDepth,
|
queue: element.dataset.axisTpQueueDepth,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const errors = [];
|
|||||||
page.on("pageerror", (error) => errors.push(`pageerror:${error.message}`));
|
page.on("pageerror", (error) => errors.push(`pageerror:${error.message}`));
|
||||||
page.on("console", (message) => { if (message.type() === "error") errors.push(`console:${message.text()}`); });
|
page.on("console", (message) => { if (message.type() === "error") errors.push(`console:${message.text()}`); });
|
||||||
const main = page.locator("main.axis-app");
|
const main = page.locator("main.axis-app");
|
||||||
const state = () => main.evaluate((element) => ({ source: element.dataset.axisStateSource, machine: element.querySelector(".machine-state")?.textContent?.trim(), program: element.dataset.axisProgramStatus, line: element.dataset.axisCurrentLine, path: element.querySelector(".xyzac-tool-preview")?.dataset.xyzacToolpathCount, live: element.querySelector(".xyzac-tool-preview")?.dataset.xyzacLiveTraceCount, loading: element.querySelector(".case-status")?.getAttribute("aria-busy") === "true", command: element.querySelector(".command-status")?.textContent, error: element.querySelector(".diagnostic-error")?.textContent ?? null }));
|
const state = () => main.evaluate((element) => ({ source: element.dataset.axisStateSource, machine: `Machine ${element.dataset.axisStateMachine}`, program: element.dataset.axisProgramStatus, line: element.dataset.axisCurrentLine, path: element.querySelector(".xyzac-tool-preview")?.dataset.xyzacToolpathCount, live: element.querySelector(".xyzac-tool-preview")?.dataset.xyzacLiveTraceCount, loading: element.querySelector(".case-status")?.getAttribute("aria-busy") === "true", command: element.querySelector(".command-status")?.textContent, error: element.querySelector(".diagnostic-error")?.textContent ?? null }));
|
||||||
const wait = async (predicate, label, timeout = 45000) => { const started = Date.now(); let last; while (Date.now() - started < timeout) { last = await state(); if (predicate(last)) return last; await page.waitForTimeout(100); } throw new Error(`${label}:${JSON.stringify(last)}`); };
|
const wait = async (predicate, label, timeout = 45000) => { const started = Date.now(); let last; while (Date.now() - started < timeout) { last = await state(); if (predicate(last)) return last; await page.waitForTimeout(100); } throw new Error(`${label}:${JSON.stringify(last)}`); };
|
||||||
const click = async (locator, label) => { for (let index = 0; index < 300; index += 1) { if (await locator.count() && !await locator.first().isDisabled()) { await locator.first().click(); return; } await page.waitForTimeout(100); } throw new Error(`${label}:disabled_or_missing`); };
|
const click = async (locator, label) => { for (let index = 0; index < 300; index += 1) { if (await locator.count() && !await locator.first().isDisabled()) { await locator.first().click(); return; } await page.waitForTimeout(100); } throw new Error(`${label}:disabled_or_missing`); };
|
||||||
const loadFile = async (name, source) => { const input = main.locator('input[type="file"][aria-label="Open G-code file"]'); await input.setInputFiles({ name, mimeType: "text/plain", buffer: Buffer.from(source) }); return wait((value) => value.program === "loaded" && !value.loading && value.error === null, `${name}_loaded`); };
|
const loadFile = async (name, source) => { const input = main.locator('input[type="file"][aria-label="Open G-code file"]'); await input.setInputFiles({ name, mimeType: "text/plain", buffer: Buffer.from(source) }); return wait((value) => value.program === "loaded" && !value.loading && value.error === null, `${name}_loaded`); };
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ page.on("dialog", async (dialog) => { dialogs.push({ type: dialog.type(), messag
|
|||||||
const main = page.locator("main.axis-app");
|
const main = page.locator("main.axis-app");
|
||||||
const state = () => main.evaluate((element) => ({
|
const state = () => main.evaluate((element) => ({
|
||||||
source: element.dataset.axisStateSource,
|
source: element.dataset.axisStateSource,
|
||||||
machine: element.querySelector(".machine-state")?.textContent?.trim(),
|
machine: `Machine ${element.dataset.axisStateMachine}`,
|
||||||
operation: element.querySelector(".operation-state")?.textContent?.trim(),
|
operation: `Operation ${element.dataset.axisStateOperation}`,
|
||||||
program: element.dataset.axisProgramStatus,
|
program: element.dataset.axisProgramStatus,
|
||||||
line: element.dataset.axisCurrentLine,
|
line: element.dataset.axisCurrentLine,
|
||||||
queue: element.dataset.axisTpQueueDepth,
|
queue: element.dataset.axisTpQueueDepth,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ if (!modulePath) {
|
|||||||
|
|
||||||
const imported = await import(pathToFileURL(modulePath).href);
|
const imported = await import(pathToFileURL(modulePath).href);
|
||||||
const playwright = imported.default ?? imported;
|
const playwright = imported.default ?? imported;
|
||||||
const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--ignore-certificate-errors", "--disable-gpu"] });
|
const browser = await playwright.chromium.launch({ headless: process.env.HEADED !== "1", executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--ignore-certificate-errors", "--disable-gpu"] });
|
||||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, ignoreHTTPSErrors: true });
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, ignoreHTTPSErrors: true });
|
||||||
const failures = [];
|
const failures = [];
|
||||||
const browserErrors = [];
|
const browserErrors = [];
|
||||||
@@ -29,8 +29,8 @@ const state = () => main.evaluate((element) => ({
|
|||||||
source: element.dataset.axisStateSource,
|
source: element.dataset.axisStateSource,
|
||||||
motion: element.dataset.axisMotionAuthority,
|
motion: element.dataset.axisMotionAuthority,
|
||||||
task: element.dataset.axisTaskAuthority,
|
task: element.dataset.axisTaskAuthority,
|
||||||
machine: element.querySelector(".machine-state")?.textContent?.trim(),
|
machine: `Machine ${element.dataset.axisStateMachine}`,
|
||||||
operation: element.querySelector(".operation-state")?.textContent?.trim(),
|
operation: `Operation ${element.dataset.axisStateOperation}`,
|
||||||
case: element.querySelector(".case-status")?.textContent?.trim(),
|
case: element.querySelector(".case-status")?.textContent?.trim(),
|
||||||
mode: element.dataset.axisTaskMode,
|
mode: element.dataset.axisTaskMode,
|
||||||
program: element.dataset.axisProgramStatus,
|
program: element.dataset.axisProgramStatus,
|
||||||
@@ -115,7 +115,7 @@ try {
|
|||||||
results.push(await record("MANUAL MODE", async () => { await main.getByRole("tab", { name: "Manual Control [F3]" }).click(); await wait((s) => s.mode === "MANUAL" || s.command?.includes("SET_MODE acknowledged"), "manual_mode_ack"); }));
|
results.push(await record("MANUAL MODE", async () => { await main.getByRole("tab", { name: "Manual Control [F3]" }).click(); await wait((s) => s.mode === "MANUAL" || s.command?.includes("SET_MODE acknowledged"), "manual_mode_ack"); }));
|
||||||
results.push(await record("HOME/TOUCH OFF boundaries", async () => {
|
results.push(await record("HOME/TOUCH OFF boundaries", async () => {
|
||||||
await click(main.locator('button[data-axis-control="manual-home-axis"]'), "home_axis_repeat");
|
await click(main.locator('button[data-axis-control="manual-home-axis"]'), "home_axis_repeat");
|
||||||
await wait((s) => s.error === null && s.command?.includes("HOME acknowledged"), "home_axis_ack");
|
await wait((s) => s.error === null && s.command?.includes("HOME complete"), "home_joint_complete");
|
||||||
await click(main.locator('button[data-axis-control="manual-touch-off"]'), "touch_off");
|
await click(main.locator('button[data-axis-control="manual-touch-off"]'), "touch_off");
|
||||||
await wait((s) => s.error === null && s.command?.includes("TOUCH_OFF acknowledged"), "touch_off_ack");
|
await wait((s) => s.error === null && s.command?.includes("TOUCH_OFF acknowledged"), "touch_off_ack");
|
||||||
if (!await main.locator('[data-axis-control="manual-tool-touch-off"]').isDisabled()) throw new Error("tool_touch_off_must_be_disabled");
|
if (!await main.locator('[data-axis-control="manual-tool-touch-off"]').isDisabled()) throw new Error("tool_touch_off_must_be_disabled");
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const control = (id) => page.locator(`[data-axis-control="${id}"]`);
|
|||||||
const state = () => page.locator("main.axis-app").evaluate((main) => {
|
const state = () => page.locator("main.axis-app").evaluate((main) => {
|
||||||
const viewer = main.querySelector("[data-xyzac-preview]");
|
const viewer = main.querySelector("[data-xyzac-preview]");
|
||||||
return { loading: main.dataset.axisProgramLoading, program: main.dataset.axisProgramStatus, path: main.dataset.axisProgramPath,
|
return { loading: main.dataset.axisProgramLoading, program: main.dataset.axisProgramStatus, path: main.dataset.axisProgramPath,
|
||||||
mode: main.dataset.axisTaskMode, operation: main.querySelector("[data-axis-state-operation]")?.dataset.axisStateOperation,
|
mode: main.dataset.axisTaskMode, operation: main.dataset.axisStateOperation,
|
||||||
pose: viewer?.dataset.xyzacLivePose, tick: Number(viewer?.dataset.xyzacTick), epoch: viewer?.dataset.xyzacLivePathEpoch,
|
pose: viewer?.dataset.xyzacLivePose, tick: Number(viewer?.dataset.xyzacTick), epoch: viewer?.dataset.xyzacLivePathEpoch,
|
||||||
queue: Number(main.dataset.axisTpQueueDepth), command: main.querySelector(".command-status")?.textContent,
|
queue: Number(main.dataset.axisTpQueueDepth), command: main.querySelector(".command-status")?.textContent,
|
||||||
error: main.querySelector(".diagnostic-error")?.textContent };
|
error: main.querySelector(".diagnostic-error")?.textContent };
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ const state = () => main.evaluate((element) => ({
|
|||||||
source: element.dataset.axisStateSource,
|
source: element.dataset.axisStateSource,
|
||||||
authority: element.dataset.axisAuthority,
|
authority: element.dataset.axisAuthority,
|
||||||
motion: element.dataset.axisMotionAuthority,
|
motion: element.dataset.axisMotionAuthority,
|
||||||
machine: element.querySelector(".machine-state")?.textContent?.trim(),
|
machine: `Machine ${element.dataset.axisStateMachine}`,
|
||||||
operation: element.querySelector(".operation-state")?.textContent?.trim(),
|
operation: `Operation ${element.dataset.axisStateOperation}`,
|
||||||
mode: element.dataset.axisTaskMode,
|
mode: element.dataset.axisTaskMode,
|
||||||
program: element.dataset.axisProgramStatus,
|
program: element.dataset.axisProgramStatus,
|
||||||
runEnabled: element.dataset.axisRunEnabled,
|
runEnabled: element.dataset.axisRunEnabled,
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ const state = () => main.evaluate((element) => ({
|
|||||||
sourceHash: element.dataset.axisProgramSourceHash,
|
sourceHash: element.dataset.axisProgramSourceHash,
|
||||||
program: element.dataset.axisProgramStatus,
|
program: element.dataset.axisProgramStatus,
|
||||||
runEnabled: element.dataset.axisRunEnabled,
|
runEnabled: element.dataset.axisRunEnabled,
|
||||||
machine: element.querySelector(".machine-state")?.textContent?.trim(),
|
machine: `Machine ${element.dataset.axisStateMachine}`,
|
||||||
operation: element.querySelector(".operation-state")?.textContent?.trim(),
|
operation: `Operation ${element.dataset.axisStateOperation}`,
|
||||||
homed: element.dataset.axisHomed,
|
homed: element.dataset.axisHomed,
|
||||||
line: element.dataset.axisCurrentLine,
|
line: element.dataset.axisCurrentLine,
|
||||||
queue: element.dataset.axisTpQueueDepth,
|
queue: element.dataset.axisTpQueueDepth,
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ add_library(linuxcnc_tp_source STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp/spherical_arc.c
|
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp/spherical_arc.c
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/nml_intf/emcpose.c
|
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/nml_intf/emcpose.c
|
||||||
src/linuxcnc_tp_compat.c
|
src/linuxcnc_tp_compat.c
|
||||||
|
src/sim_native_homing.c
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/motion/simple_tp.c
|
||||||
)
|
)
|
||||||
target_include_directories(linuxcnc_tp_source PRIVATE
|
target_include_directories(linuxcnc_tp_source PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src
|
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src
|
||||||
@@ -64,11 +66,16 @@ target_include_directories(sim_core PRIVATE
|
|||||||
target_link_libraries(sim_core PUBLIC linuxcnc_tp_source)
|
target_link_libraries(sim_core PUBLIC linuxcnc_tp_source)
|
||||||
|
|
||||||
if(NOT EMSCRIPTEN)
|
if(NOT EMSCRIPTEN)
|
||||||
|
add_executable(sim_native_homing_test tests/sim_native_homing_test.c)
|
||||||
|
target_include_directories(sim_native_homing_test PRIVATE include)
|
||||||
|
target_compile_options(sim_native_homing_test PRIVATE -UNDEBUG)
|
||||||
|
target_link_libraries(sim_native_homing_test PRIVATE sim_core)
|
||||||
add_executable(sim_api_test tests/sim_api_test.c)
|
add_executable(sim_api_test tests/sim_api_test.c)
|
||||||
target_link_libraries(sim_api_test PRIVATE sim_core)
|
target_link_libraries(sim_api_test PRIVATE sim_core)
|
||||||
target_include_directories(sim_api_test PRIVATE include)
|
target_include_directories(sim_api_test PRIVATE include)
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
add_test(NAME sim_native_homing_test COMMAND sim_native_homing_test)
|
||||||
add_test(NAME sim_api_test COMMAND sim_api_test)
|
add_test(NAME sim_api_test COMMAND sim_api_test)
|
||||||
|
|
||||||
add_executable(sim_protocol_test tests/sim_protocol_test.c)
|
add_executable(sim_protocol_test tests/sim_protocol_test.c)
|
||||||
@@ -289,6 +296,7 @@ if(EMSCRIPTEN)
|
|||||||
)
|
)
|
||||||
target_compile_options(sim_core_module PRIVATE -ffp-contract=off -fno-fast-math)
|
target_compile_options(sim_core_module PRIVATE -ffp-contract=off -fno-fast-math)
|
||||||
target_link_libraries(sim_core_module PRIVATE sim_posemath linuxcnc_tp_source)
|
target_link_libraries(sim_core_module PRIVATE sim_posemath linuxcnc_tp_source)
|
||||||
|
target_link_options(sim_core_module PRIVATE "-Wl,-u,sim_native_home_init")
|
||||||
set_target_properties(sim_core_module PROPERTIES
|
set_target_properties(sim_core_module PROPERTIES
|
||||||
OUTPUT_NAME "linuxcnc-sim-core"
|
OUTPUT_NAME "linuxcnc-sim-core"
|
||||||
SUFFIX ".js"
|
SUFFIX ".js"
|
||||||
|
|||||||
15
wasm-core/include/sim_native_homing.h
Normal file
15
wasm-core/include/sim_native_homing.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#ifndef SIM_NATIVE_HOMING_H
|
||||||
|
#define SIM_NATIVE_HOMING_H
|
||||||
|
#include <stdint.h>
|
||||||
|
typedef struct native_home native_home;
|
||||||
|
uint32_t sim_native_home_size(void);
|
||||||
|
int sim_native_home_init(native_home *, int count, double period);
|
||||||
|
int sim_native_home_config(native_home *, int joint, const double *parameters);
|
||||||
|
int sim_native_home_sync(native_home *, const double *joints);
|
||||||
|
int sim_native_home_mode(native_home *, int mode);
|
||||||
|
int sim_native_home_start(native_home *, int joint, uint32_t seq);
|
||||||
|
int sim_native_home_step(native_home *);
|
||||||
|
int sim_native_home_cancel(native_home *, int volatile_only);
|
||||||
|
int sim_native_home_input(native_home *, int joint, int home_switch, int index_enable, int negative_limit, int positive_limit);
|
||||||
|
double sim_native_home_get(native_home *, int joint, int field);
|
||||||
|
#endif
|
||||||
168
wasm-core/src/sim_native_homing.c
Normal file
168
wasm-core/src/sim_native_homing.c
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "../include/sim_native_homing.h"
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <emscripten/emscripten.h>
|
||||||
|
#define API EMSCRIPTEN_KEEPALIVE
|
||||||
|
#else
|
||||||
|
#define API
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Only HAL allocation is adapted. The state machine below is the original
|
||||||
|
* homemod source, including sequence, switch, index and final-move rules. */
|
||||||
|
#define hal_malloc native_home_hal_malloc
|
||||||
|
#define hal_pin_bit_newf native_home_hal_bit
|
||||||
|
#define hal_pin_s32_newf native_home_hal_s32
|
||||||
|
#include "../../linuxcnc/src/emc/motion/homing.c"
|
||||||
|
#include "simple_tp.h"
|
||||||
|
|
||||||
|
static union { double align; unsigned char bytes[8192]; } pin_arena;
|
||||||
|
static size_t pin_used;
|
||||||
|
void *native_home_hal_malloc(long size) {
|
||||||
|
size_t start = (pin_used + 7) & ~(size_t)7;
|
||||||
|
if (size < 0 || start + (size_t)size > sizeof(pin_arena)) return NULL;
|
||||||
|
pin_used = start + size;
|
||||||
|
return pin_arena.bytes + start;
|
||||||
|
}
|
||||||
|
int native_home_hal_bit(hal_pin_dir_t dir, hal_bit_t **pin, int id, const char *fmt, ...) {
|
||||||
|
(void)dir; (void)id; (void)fmt;
|
||||||
|
*pin = native_home_hal_malloc(sizeof(**pin)); return *pin ? 0 : -1;
|
||||||
|
}
|
||||||
|
int native_home_hal_s32(hal_pin_dir_t dir, hal_s32_t **pin, int id, const char *fmt, ...) {
|
||||||
|
(void)dir; (void)id; (void)fmt;
|
||||||
|
*pin = native_home_hal_malloc(sizeof(**pin)); return *pin ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct native_home {
|
||||||
|
emcmot_joint_t joint[EMCMOT_MAX_JOINTS];
|
||||||
|
home_local_data local[EMCMOT_MAX_JOINTS];
|
||||||
|
double period;
|
||||||
|
int count, sequence, current, active, sync, mode;
|
||||||
|
uint32_t tick, command_seq;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* homemod is a singleton in LinuxCNC. Swap its private data at the ABI
|
||||||
|
* boundary to isolate multiple simulation sessions in one WASM module. */
|
||||||
|
static void load(native_home *h) {
|
||||||
|
joints = h->joint; all_joints = h->count; extra_joints = 0;
|
||||||
|
servo_freq = 1 / h->period;
|
||||||
|
memcpy(H, h->local, sizeof(H));
|
||||||
|
sequence_state = h->sequence; current_sequence = h->current;
|
||||||
|
homing_active = h->active; sync_now = h->sync;
|
||||||
|
}
|
||||||
|
static void save(native_home *h) {
|
||||||
|
memcpy(h->local, H, sizeof(H));
|
||||||
|
h->sequence = sequence_state; h->current = current_sequence;
|
||||||
|
h->active = homing_active; h->sync = sync_now;
|
||||||
|
}
|
||||||
|
API uint32_t sim_native_home_size(void) { return sizeof(native_home); }
|
||||||
|
API int sim_native_home_init(native_home *h, int count, double period) {
|
||||||
|
if (!h || count < 1 || count > EMCMOT_MAX_JOINTS || !isfinite(period) || period <= 0) return -1;
|
||||||
|
memset(h, 0, sizeof(*h)); memset(H, 0, sizeof(H));
|
||||||
|
pin_used = 0; memset(&pin_arena, 0, sizeof(pin_arena));
|
||||||
|
h->count = count; h->period = period; h->mode = 1; /* FREE */
|
||||||
|
sequence_state = HOME_SEQUENCE_IDLE; current_sequence = 0; sync_now = 0;
|
||||||
|
if (homing_init(0, period, count, 0, h->joint)) return -1;
|
||||||
|
for (int j = 0; j < count; j++) {
|
||||||
|
h->joint[j].flag |= EMCMOT_JOINT_ACTIVE_BIT | EMCMOT_JOINT_INPOS_BIT;
|
||||||
|
}
|
||||||
|
save(h); return 0;
|
||||||
|
}
|
||||||
|
/* p: offset, home, final velocity, search velocity, latch velocity, flags,
|
||||||
|
* sequence, volatile, min limit, max limit, max velocity, max acceleration. */
|
||||||
|
API int sim_native_home_config(native_home *h, int j, const double *p) {
|
||||||
|
if (!h || !p || j < 0 || j >= h->count) return -1;
|
||||||
|
for (int i = 0; i < 12; i++) if (!isfinite(p[i])) return -1;
|
||||||
|
if (p[8] > p[9] || p[10] <= 0 || p[11] <= 0 || ((int)p[5] & HOME_UNLOCK_FIRST)) return -1;
|
||||||
|
load(h);
|
||||||
|
set_joint_homing_params(j, p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7] != 0);
|
||||||
|
joints[j].min_pos_limit = p[8]; joints[j].max_pos_limit = p[9];
|
||||||
|
joints[j].vel_limit = p[10]; joints[j].acc_limit = p[11];
|
||||||
|
joints[j].free_tp.max_acc = p[11]; save(h); return 0;
|
||||||
|
}
|
||||||
|
API int sim_native_home_sync(native_home *h, const double *position) {
|
||||||
|
if (!h || !position || h->active || h->sequence != HOME_SEQUENCE_IDLE) return -1;
|
||||||
|
for (int j = 0; j < h->count; j++) if (!isfinite(position[j])) return -1;
|
||||||
|
for (int j = 0; j < h->count; j++) {
|
||||||
|
emcmot_joint_t *p = &h->joint[j];
|
||||||
|
p->pos_cmd = p->pos_fb = p->free_tp.curr_pos = position[j];
|
||||||
|
p->motor_pos_fb = position[j] + p->motor_offset;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
API int sim_native_home_mode(native_home *h, int mode) {
|
||||||
|
if (!h || mode < 1 || mode > 3 || h->active || h->sequence != HOME_SEQUENCE_IDLE) return -1;
|
||||||
|
load(h);
|
||||||
|
if (mode != 1 && !get_allhomed()) return -2;
|
||||||
|
h->mode = mode; return 0;
|
||||||
|
}
|
||||||
|
API int sim_native_home_start(native_home *h, int j, uint32_t seq) {
|
||||||
|
if (!h || j < -1 || j >= h->count || h->active || h->sequence != HOME_SEQUENCE_IDLE) return -1;
|
||||||
|
load(h); h->mode = 1; h->command_seq = seq;
|
||||||
|
do_home_joint(j); save(h); return 0;
|
||||||
|
}
|
||||||
|
API int sim_native_home_step(native_home *h) {
|
||||||
|
if (!h) return -1;
|
||||||
|
load(h);
|
||||||
|
/* control.c switches to TELEOP on the native all-homed edge. */
|
||||||
|
if (do_homing()) h->mode = 3;
|
||||||
|
/* control.c: homing runs before free-mode position generation. Ideal
|
||||||
|
* feedback follows the commanded motor position in this simulation. */
|
||||||
|
for (int j = 0; j < h->count; j++) {
|
||||||
|
simple_tp_update(&joints[j].free_tp, h->period);
|
||||||
|
joints[j].pos_cmd = joints[j].free_tp.curr_pos;
|
||||||
|
joints[j].pos_fb = joints[j].pos_cmd;
|
||||||
|
joints[j].motor_pos_fb = joints[j].pos_cmd + joints[j].motor_offset;
|
||||||
|
if (joints[j].free_tp.active) joints[j].flag &= ~EMCMOT_JOINT_INPOS_BIT;
|
||||||
|
else joints[j].flag |= EMCMOT_JOINT_INPOS_BIT;
|
||||||
|
}
|
||||||
|
h->tick++; save(h); return 0;
|
||||||
|
}
|
||||||
|
API int sim_native_home_cancel(native_home *h, int volatile_only) {
|
||||||
|
if (!h) return -1;
|
||||||
|
load(h);
|
||||||
|
for (int j = 0; j < h->count; j++) {
|
||||||
|
if (H[j].homing || H[j].home_state != HOME_IDLE || (homing_active && H[j].joint_in_sequence)) {
|
||||||
|
H[j].home_state = HOME_ABORT;
|
||||||
|
base_1joint_state_machine(j);
|
||||||
|
}
|
||||||
|
joints[j].free_tp.enable = 0;
|
||||||
|
if (volatile_only) {
|
||||||
|
joints[j].free_tp.curr_vel = 0;
|
||||||
|
joints[j].free_tp.active = 0;
|
||||||
|
joints[j].flag |= EMCMOT_JOINT_INPOS_BIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sequence_state = HOME_SEQUENCE_IDLE; homing_active = 0;
|
||||||
|
if (volatile_only) set_unhomed(-2, EMCMOT_MOTION_DISABLED);
|
||||||
|
h->mode = 1; save(h); return 0;
|
||||||
|
}
|
||||||
|
API int sim_native_home_input(native_home *h, int j, int sw, int index, int neg, int pos) {
|
||||||
|
if (!h || j < 0 || j >= h->count) return -1;
|
||||||
|
h->local[j].home_sw = sw != 0; h->local[j].index_enable = index != 0;
|
||||||
|
h->joint[j].on_neg_limit = neg != 0; h->joint[j].on_pos_limit = pos != 0; return 0;
|
||||||
|
}
|
||||||
|
API double sim_native_home_get(native_home *h, int j, int field) {
|
||||||
|
if (!h) return -1;
|
||||||
|
if (field == 0) {
|
||||||
|
for (int i = 0; i < h->count; i++) if (h->joint[i].free_tp.active) return 1;
|
||||||
|
return h->active || h->sequence != HOME_SEQUENCE_IDLE;
|
||||||
|
}
|
||||||
|
if (field == 1) return h->mode;
|
||||||
|
if (field == 2) return h->tick;
|
||||||
|
if (field == 3) return h->command_seq;
|
||||||
|
if (j < 0 || j >= h->count) return -1;
|
||||||
|
switch (field) {
|
||||||
|
case 4: return h->local[j].homing;
|
||||||
|
case 5: return h->local[j].homed;
|
||||||
|
case 6: return h->local[j].home_state;
|
||||||
|
case 7: return h->joint[j].pos_fb;
|
||||||
|
case 8: return h->joint[j].motor_pos_fb;
|
||||||
|
case 9: return h->joint[j].motor_offset;
|
||||||
|
case 10: return h->local[j].home_sequence;
|
||||||
|
case 11: return h->joint[j].free_tp.curr_vel;
|
||||||
|
case 12: return h->local[j].index_enable;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
43
wasm-core/tests/sim_native_homing_test.c
Normal file
43
wasm-core/tests/sim_native_homing_test.c
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#include "sim_native_homing.h"
|
||||||
|
#include <assert.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
native_home *h = malloc(sim_native_home_size());
|
||||||
|
native_home *other = malloc(sim_native_home_size());
|
||||||
|
double p[12] = {0,0,-1,0,0,0,0,0,-200,200,20,300};
|
||||||
|
double position[5] = {10,20,30,15,45};
|
||||||
|
assert(sim_native_home_init(h,5,.001) == 0);
|
||||||
|
assert(sim_native_home_init(other,5,.001) == 0);
|
||||||
|
for (int j=0;j<5;j++) assert(sim_native_home_config(h,j,p) == 0);
|
||||||
|
assert(sim_native_home_sync(h,position) == 0);
|
||||||
|
assert(sim_native_home_start(h,0,70000) == 0);
|
||||||
|
assert(sim_native_home_get(h,0,7) == 10);
|
||||||
|
assert(sim_native_home_step(h) == 0);
|
||||||
|
assert(sim_native_home_get(h,0,6) == 21);
|
||||||
|
assert(sim_native_home_get(h,0,4) == 1);
|
||||||
|
assert(sim_native_home_get(h,0,5) == 0);
|
||||||
|
assert(sim_native_home_get(h,0,7) == 0);
|
||||||
|
assert(sim_native_home_get(h,0,8) == 10);
|
||||||
|
assert(sim_native_home_get(h,1,7) == 20);
|
||||||
|
for(int i=0;i<10;i++) assert(sim_native_home_step(h) == 0);
|
||||||
|
assert(sim_native_home_get(h,0,5) == 1);
|
||||||
|
assert(sim_native_home_get(other,0,5) == 0);
|
||||||
|
p[1]=5;
|
||||||
|
assert(sim_native_home_config(h,1,p) == 0);
|
||||||
|
assert(sim_native_home_start(h,1,70001) == 0);
|
||||||
|
assert(sim_native_home_step(h) == 0);
|
||||||
|
assert(sim_native_home_get(h,1,7)>0 && sim_native_home_get(h,1,7)<5);
|
||||||
|
for(int i=0;i<2000 && sim_native_home_get(h,0,0);i++) sim_native_home_step(h);
|
||||||
|
assert(fabs(sim_native_home_get(h,1,7)-5)<1e-8);
|
||||||
|
assert(fabs(sim_native_home_get(h,1,8)-25)<1e-8);
|
||||||
|
assert(sim_native_home_start(h,1,70002) == 0);
|
||||||
|
sim_native_home_step(h);
|
||||||
|
assert(sim_native_home_cancel(h,0) == 0);
|
||||||
|
assert(sim_native_home_get(h,1,5) == 0);
|
||||||
|
for (int i=0;i<1000 && sim_native_home_get(h,0,0);i++) sim_native_home_step(h);
|
||||||
|
assert(sim_native_home_get(h,0,0) == 0);
|
||||||
|
free(h); free(other);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
16
web/react/src/MachineStatus.tsx
Normal file
16
web/react/src/MachineStatus.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { SourceStatus } from "./protocol";
|
||||||
|
|
||||||
|
export function MachineStatus({ status, commandStatus }: { status?: SourceStatus; commandStatus: string }) {
|
||||||
|
return <section className="machine-status" aria-label="LinuxCNC status">
|
||||||
|
<div className="machine-status-dimensions">
|
||||||
|
<span>Task mode <output>{status?.task.mode ?? "PENDING"}</output></span>
|
||||||
|
<span>Task state <output>{status?.task.state ?? "PENDING"}</output></span>
|
||||||
|
<span>Interpreter <output>{status?.task.interpState ?? "PENDING"}</output></span>
|
||||||
|
<span>Motion mode <output>{status?.traj.mode ?? "PENDING"}</output></span>
|
||||||
|
<span>Motion type <output>{status?.traj.motion_type ?? "-"}</output></span>
|
||||||
|
<span>Homing <output>{status ? status.homing_active ? "ACTIVE" : "IDLE" : "PENDING"}</output></span>
|
||||||
|
<span>Machine sequence <output>{status ? status.joint.length > 0 && status.joint.every((joint) => joint.homed) ? "HOMED" : "NOT HOMED" : "WASM PENDING"}</output></span>
|
||||||
|
</div>
|
||||||
|
<span className="command-status" role="status" aria-live="polite" title={commandStatus}>{commandStatus}</span>
|
||||||
|
</section>;
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export type MessageEnvelope<TPayload = unknown> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type MotionSnapshot = {
|
export type MotionSnapshot = {
|
||||||
|
homing?: boolean;
|
||||||
stepping?: boolean;
|
stepping?: boolean;
|
||||||
axes?: number[];
|
axes?: number[];
|
||||||
joints?: number[];
|
joints?: number[];
|
||||||
@@ -42,6 +43,18 @@ export type MotionSnapshot = {
|
|||||||
max_velocity?: number;
|
max_velocity?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HomeJointStatus = {
|
||||||
|
joint: number; homing: boolean; homed: boolean; home_state: number; home_state_name: string;
|
||||||
|
position: number; motor_position: number; motor_offset: number; velocity: number; home_sequence: number;
|
||||||
|
};
|
||||||
|
export type SourceStatus = {
|
||||||
|
task: { mode: string; state: string; interpState: string };
|
||||||
|
traj: { mode: string; motion_type: number };
|
||||||
|
homing_active: boolean;
|
||||||
|
joint: HomeJointStatus[];
|
||||||
|
homing_history: Array<{ tick: number; joint: HomeJointStatus[] }>;
|
||||||
|
};
|
||||||
|
|
||||||
export type TrajectorySnapshot = {
|
export type TrajectorySnapshot = {
|
||||||
queue?: Array<{ id?: number; source_line?: number | null; start?: Record<string, number>; end?: Record<string, number> }>;
|
queue?: Array<{ id?: number; source_line?: number | null; start?: Record<string, number>; end?: Record<string, number> }>;
|
||||||
active?: { id?: number; source_line?: number | null; start?: Record<string, number>; end?: Record<string, number> } | null;
|
active?: { id?: number; source_line?: number | null; start?: Record<string, number>; end?: Record<string, number> } | null;
|
||||||
@@ -49,6 +62,7 @@ export type TrajectorySnapshot = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SimSnapshotV1 = {
|
export type SimSnapshotV1 = {
|
||||||
|
source_status?: SourceStatus;
|
||||||
schema?: string;
|
schema?: string;
|
||||||
snapshot_id?: string;
|
snapshot_id?: string;
|
||||||
base_snapshot_id?: string;
|
base_snapshot_id?: string;
|
||||||
@@ -114,6 +128,7 @@ export type XyzacCapabilitySnapshot = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PanelSnapshotV1 = {
|
export type PanelSnapshotV1 = {
|
||||||
|
source_status?: SourceStatus;
|
||||||
schema: "axis-panel-snapshot.v1";
|
schema: "axis-panel-snapshot.v1";
|
||||||
context_id: string;
|
context_id: string;
|
||||||
session_id: string;
|
session_id: string;
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
|||||||
} : previous.snapshot.visual;
|
} : previous.snapshot.visual;
|
||||||
const nextSnapshot = {
|
const nextSnapshot = {
|
||||||
...previous.snapshot,
|
...previous.snapshot,
|
||||||
|
source_status: (result.source_status as SimSnapshotV1["source_status"]) ?? (previous.snapshot.source_status ? {
|
||||||
|
...previous.snapshot.source_status,
|
||||||
|
task: { ...previous.snapshot.source_status.task,
|
||||||
|
mode: String((result.task as Record<string, unknown>)?.mode ?? previous.snapshot.source_status.task.mode),
|
||||||
|
interpState: (result.axis_state as AxisStateView)?.source_interp_state ?? previous.snapshot.source_status.task.interpState },
|
||||||
|
} : undefined),
|
||||||
simulation_tick: message.simulation_tick ?? delta.tick ?? previous.snapshot.simulation_tick,
|
simulation_tick: message.simulation_tick ?? delta.tick ?? previous.snapshot.simulation_tick,
|
||||||
state_hash: String(result.state_hash ?? previous.snapshot.state_hash),
|
state_hash: String(result.state_hash ?? previous.snapshot.state_hash),
|
||||||
task: (result.task as SimSnapshotV1["task"]) ?? previous.snapshot.task,
|
task: (result.task as SimSnapshotV1["task"]) ?? previous.snapshot.task,
|
||||||
@@ -147,6 +153,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
|||||||
const axes = [pose.x, pose.y, pose.z, pose.a, pose.c].map((value) => Number.isFinite(value) ? value : 0);
|
const axes = [pose.x, pose.y, pose.z, pose.a, pose.c].map((value) => Number.isFinite(value) ? value : 0);
|
||||||
const snapshot = {
|
const snapshot = {
|
||||||
schema: panel.schema,
|
schema: panel.schema,
|
||||||
|
source_status: panel.source_status,
|
||||||
simulation_tick: panel.simulation_tick,
|
simulation_tick: panel.simulation_tick,
|
||||||
state_hash: panel.state_hash,
|
state_hash: panel.state_hash,
|
||||||
task: panel.task as SimSnapshotV1["task"],
|
task: panel.task as SimSnapshotV1["task"],
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ button { color: inherit; }
|
|||||||
.machine-sequence-heading { display: grid; gap: 2px; min-width: 148px; }
|
.machine-sequence-heading { display: grid; gap: 2px; min-width: 148px; }
|
||||||
.machine-sequence-heading strong { font-size: 12px; }
|
.machine-sequence-heading strong { font-size: 12px; }
|
||||||
.machine-sequence-heading span { color: #555; font: 10px ui-monospace, monospace; }
|
.machine-sequence-heading span { color: #555; font: 10px ui-monospace, monospace; }
|
||||||
.machine-sequence-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-width: 0; }
|
.machine-sequence-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-width: 0; padding: 8px; border-bottom: 1px solid #aaa; }
|
||||||
.machine-sequence-actions .axis-text-button { min-height: 28px; }
|
.machine-sequence-actions .axis-text-button { min-height: 28px; }
|
||||||
.machine-sequence-actions .axis-text-button:first-child { border-color: #9b4a4a; }
|
.machine-sequence-actions .axis-text-button:first-child { border-color: #9b4a4a; }
|
||||||
|
|
||||||
@@ -245,14 +245,6 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
|||||||
.visually-hidden-file-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
.visually-hidden-file-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
||||||
.axis-layer-controls { display: flex; flex-wrap: wrap; gap: 8px 12px; margin-top: 4px; color: #4d4d4d; font-size: 11px; }
|
.axis-layer-controls { display: flex; flex-wrap: wrap; gap: 8px 12px; margin-top: 4px; color: #4d4d4d; font-size: 11px; }
|
||||||
.axis-layer-controls input { margin: 0; }
|
.axis-layer-controls input { margin: 0; }
|
||||||
.axis-info-bar { display: flex; align-items: stretch; width: 100%; min-width: 0; min-height: 27px; margin-top: auto; border-top: 1px solid #777; background: #c4c4c4; }
|
|
||||||
.axis-info-bar > span { display: flex; align-items: center; min-width: 0; min-height: 25px; padding: 2px 9px; overflow: hidden; border-right: 1px solid #999; font: 12px ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
.task-state { min-width: 92px; background: #ececec; font-weight: 700 !important; }
|
|
||||||
.task-state.running { color: #145d1c; }
|
|
||||||
.task-state.blocked, .task-state.error { color: #9b111e; background: #f2d6d6; }
|
|
||||||
.tool-state { min-width: 0; flex: 1; }
|
|
||||||
.position-state { min-width: 230px; }
|
|
||||||
.footer-status { min-width: 180px; color: #505050; }
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.xyzac-control-band { grid-template-columns: 1fr; }
|
.xyzac-control-band { grid-template-columns: 1fr; }
|
||||||
@@ -287,6 +279,7 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
|||||||
.dro-mode-row { font-size: 9px; }
|
.dro-mode-row { font-size: 9px; }
|
||||||
.dro-row output { font-size: 13px; }
|
.dro-row output { font-size: 13px; }
|
||||||
.dro-footer { flex-direction: column; gap: 3px; }
|
.dro-footer { flex-direction: column; gap: 3px; }
|
||||||
.axis-info-bar { flex-wrap: wrap; }
|
|
||||||
.position-state, .footer-status { min-width: 0; flex: 1; }
|
|
||||||
}
|
}
|
||||||
|
.machine-status { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px; padding: 6px 8px; border-bottom: 1px solid #999; min-width: 0; }
|
||||||
|
.machine-status-dimensions { display: flex; flex: 1 1 auto; flex-wrap: wrap; gap: 6px 18px; }
|
||||||
|
.machine-status-dimensions output { font-weight: bold; margin-left: 5px; }
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export function axisState({ task = {}, program = {}, motion = {}, devices = {},
|
|||||||
const machineState = hasIoState ? ioEstop ? "ESTOP" : ioReady ? "ON" : "OFF"
|
const machineState = hasIoState ? ioEstop ? "ESTOP" : ioReady ? "ON" : "OFF"
|
||||||
: taskState === "ESTOP" ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(taskState) ? "ON" : "OFF";
|
: taskState === "ESTOP" ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(taskState) ? "ON" : "OFF";
|
||||||
let operationState = programActivity(task, program);
|
let operationState = programActivity(task, program);
|
||||||
|
if (motion.homing) operationState = "HOME";
|
||||||
const state = machineState === "ESTOP" || machineState === "ESTOP_RESET" ? machineState : operationState === "HOME" ? "HOME" : operationState === "RUNNING" ? "RUNNING" : operationState === "PAUSED" ? "PAUSED" : machineState === "OFF" ? "OFF" : "IDLE";
|
const state = machineState === "ESTOP" || machineState === "ESTOP_RESET" ? machineState : operationState === "HOME" ? "HOME" : operationState === "RUNNING" ? "RUNNING" : operationState === "PAUSED" ? "PAUSED" : machineState === "OFF" ? "OFF" : "IDLE";
|
||||||
const sourceTaskState = machineState === "ESTOP" ? "STATE_ESTOP" : machineState === "OFF" ? "STATE_ESTOP_RESET" : "STATE_ON";
|
const sourceTaskState = machineState === "ESTOP" ? "STATE_ESTOP" : machineState === "OFF" ? "STATE_ESTOP_RESET" : "STATE_ON";
|
||||||
const sourceInterpState = operationState === "PAUSED" ? "INTERP_PAUSED" : operationState === "RUNNING" ? (program?.program_status === "completed" || String(task.mode).toUpperCase() === "MDI" ? "INTERP_WAITING" : "INTERP_READING") : "INTERP_IDLE";
|
const sourceInterpState = operationState === "PAUSED" ? "INTERP_PAUSED" : operationState === "RUNNING" ? (program?.program_status === "completed" || String(task.mode).toUpperCase() === "MDI" ? "INTERP_WAITING" : "INTERP_READING") : "INTERP_IDLE";
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { axisSourceUi } from "./axis-source-ui.mjs";
|
|||||||
import { WasmMotionQueue } from "./wasm-motion-queue.mjs";
|
import { WasmMotionQueue } from "./wasm-motion-queue.mjs";
|
||||||
import { WasmLinuxcncTp } from "./wasm-linuxcnc-tp.mjs";
|
import { WasmLinuxcncTp } from "./wasm-linuxcnc-tp.mjs";
|
||||||
import { WasmHomeLimit } from "./wasm-home-limit.mjs";
|
import { WasmHomeLimit } from "./wasm-home-limit.mjs";
|
||||||
|
import { WasmNativeHoming } from "./wasm-native-homing.mjs";
|
||||||
import { WasmXyzacKinematics, XYZAC_MODES } from "./wasm-xyzac-kinematics.mjs";
|
import { WasmXyzacKinematics, XYZAC_MODES } from "./wasm-xyzac-kinematics.mjs";
|
||||||
import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
|
import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
|
||||||
import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs";
|
import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs";
|
||||||
@@ -33,7 +34,6 @@ import { axisGate, xyzacGate, xyzacGateMatrix } from "../app/axis-contract.mjs";
|
|||||||
|
|
||||||
const PROTOCOL_SCHEMA = 1;
|
const PROTOCOL_SCHEMA = 1;
|
||||||
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
||||||
const XYZAC_AXIS_TO_JOINT = Object.freeze({ x: 0, y: 1, z: 2, a: 3, c: 4 });
|
|
||||||
|
|
||||||
const OPERATION_KINDS = new Set([
|
const OPERATION_KINDS = new Set([
|
||||||
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
|
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
|
||||||
@@ -135,13 +135,8 @@ export class SimSession {
|
|||||||
this.xyzacIo = null;
|
this.xyzacIo = null;
|
||||||
this.xyzacDeviceIo = null;
|
this.xyzacDeviceIo = null;
|
||||||
this.xyzacProgram = null;
|
this.xyzacProgram = null;
|
||||||
|
this.homingHistory = [];
|
||||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||||
// The Web panel presents fixed XYZAC world-axis controls. Homing still
|
|
||||||
// follows AXIS's KINEMATICS_BOTH transaction and uses a numeric joint;
|
|
||||||
// keep the presentation mode separate so the adapter can restore it
|
|
||||||
// after the accepted home transaction.
|
|
||||||
this.xyzacMotionMode = "TELEOP";
|
|
||||||
this.pendingHome = null;
|
|
||||||
this.xyzacRapidVelocity = 35;
|
this.xyzacRapidVelocity = 35;
|
||||||
this.xyzacMotionCycleTime = 0.01;
|
this.xyzacMotionCycleTime = 0.01;
|
||||||
this.xyzacTaskCycleTicks = 1;
|
this.xyzacTaskCycleTicks = 1;
|
||||||
@@ -192,7 +187,7 @@ export class SimSession {
|
|||||||
// Compact payloads are only valid for successful motion samples. Error
|
// Compact payloads are only valid for successful motion samples. Error
|
||||||
// responses must retain their code/reason instead of being rewritten into
|
// responses must retain their code/reason instead of being rewritten into
|
||||||
// a delta with undefined fields.
|
// a delta with undefined fields.
|
||||||
const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion;
|
const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion && !statePayload.homing_sample && !this.wasmHomeLimit.active;
|
||||||
// The frontend retains the canonical preview from PROGRAM_LOAD. Repeating
|
// The frontend retains the canonical preview from PROGRAM_LOAD. Repeating
|
||||||
// thousands of immutable segments on unrelated control responses makes a
|
// thousands of immutable segments on unrelated control responses makes a
|
||||||
// loaded program delay abort/power/home commands behind structured-clone
|
// loaded program delay abort/power/home commands behind structured-clone
|
||||||
@@ -263,6 +258,13 @@ export class SimSession {
|
|||||||
simulation_tick: tick,
|
simulation_tick: tick,
|
||||||
payload: {
|
payload: {
|
||||||
...responsePayload,
|
...responsePayload,
|
||||||
|
...(motionDelta ? { source_status: {
|
||||||
|
task: { mode: task.mode, state: axisState({ task, program, io: this.xyzacIo.export() }).source_task_state,
|
||||||
|
interpState: axisState({ task, program, io: this.xyzacIo.export() }).source_interp_state },
|
||||||
|
traj: { mode: this.wasmHomeLimit.mode, motion_type: currentMotion.state.motion_type },
|
||||||
|
homing_active: this.wasmHomeLimit.active, joint: this.wasmHomeLimit.read().state.joint,
|
||||||
|
homing_history: this.homingHistory,
|
||||||
|
} } : {}),
|
||||||
...(panelSnapshot ? { panel_snapshot: panelSnapshot } : {}),
|
...(panelSnapshot ? { panel_snapshot: panelSnapshot } : {}),
|
||||||
motion_authority: responsePayload.motion_authority ?? this.motionAuthority,
|
motion_authority: responsePayload.motion_authority ?? this.motionAuthority,
|
||||||
task_authority: responsePayload.task_authority ?? this.task.authority(),
|
task_authority: responsePayload.task_authority ?? this.task.authority(),
|
||||||
@@ -275,6 +277,7 @@ export class SimSession {
|
|||||||
wasmMotionProjection(program = null) {
|
wasmMotionProjection(program = null) {
|
||||||
const wasmMotion = this.wasmMotion.read();
|
const wasmMotion = this.wasmMotion.read();
|
||||||
const position = wasmMotion.status === "verified" ? wasmMotion.state.position : { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 };
|
const position = wasmMotion.status === "verified" ? wasmMotion.state.position : { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 };
|
||||||
|
if (this.xyzacProgram && !this.wasmHomeLimit.active) this.wasmHomeLimit.sync(wasmMotion.state.joint_commanded);
|
||||||
const home = this.wasmHomeLimit.read();
|
const home = this.wasmHomeLimit.read();
|
||||||
const io = this.xyzacIo?.canonical?.();
|
const io = this.xyzacIo?.canonical?.();
|
||||||
const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null;
|
const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null;
|
||||||
@@ -308,7 +311,8 @@ export class SimSession {
|
|||||||
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), a: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
|
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), a: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
|
||||||
on_any_limit: Boolean(home.state?.limit_error),
|
on_any_limit: Boolean(home.state?.limit_error),
|
||||||
following_error: 0,
|
following_error: 0,
|
||||||
motion_mode: this.xyzacProgram ? this.xyzacMotionMode : (home.state?.homed ? "TELEOP" : "FREE"),
|
motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (home.state?.homed ? "TELEOP" : "FREE"),
|
||||||
|
homing: Boolean(home.state?.homing),
|
||||||
jog: null,
|
jog: null,
|
||||||
stepping: this.wasmMotion.isStepping?.() ?? false,
|
stepping: this.wasmMotion.isStepping?.() ?? false,
|
||||||
estop: Boolean(ioState?.estop),
|
estop: Boolean(ioState?.estop),
|
||||||
@@ -401,9 +405,12 @@ export class SimSession {
|
|||||||
const gateActions = ["load", "run", "runLine", "step", "pause", "resume", "abort", "restart", "reload", "edit", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "overrideLimits", "reset", "spindle", "coolant", "touchOff", "toolTouchOff"];
|
const gateActions = ["load", "run", "runLine", "step", "pause", "resume", "abort", "restart", "reload", "edit", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "overrideLimits", "reset", "spindle", "coolant", "touchOff", "toolTouchOff"];
|
||||||
const ui = axisSourceUi({ task_state: axis.source_task_state, interp_state: axis.source_interp_state,
|
const ui = axisSourceUi({ task_state: axis.source_task_state, interp_state: axis.source_interp_state,
|
||||||
taskfile: Boolean(this.xyzacProgram?.path), queued_mdi_commands: task.queued_mdi_commands ?? 0,
|
taskfile: Boolean(this.xyzacProgram?.path), queued_mdi_commands: task.queued_mdi_commands ?? 0,
|
||||||
motion_mode: this.xyzacProgram ? this.xyzacMotionMode : (homeLimit.state?.homed ? "TELEOP" : "FREE"), kinematics_type: "BOTH",
|
motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (homeLimit.state?.homed ? "TELEOP" : "FREE"), kinematics_type: "BOTH",
|
||||||
spindle_direction: Number(devices.spindles?.[0]?.direction ?? 0), tool: Number(devices.tool?.current ?? 0),
|
spindle_direction: Number(devices.spindles?.[0]?.direction ?? 0), tool: Number(devices.tool?.current ?? 0),
|
||||||
on_any_limit: motion.on_any_limit, paused: axis.operation_state === "PAUSED" });
|
on_any_limit: motion.on_any_limit, paused: axis.operation_state === "PAUSED" });
|
||||||
|
if (homeLimit.state?.homing) {
|
||||||
|
for (const name of ["run", "step", "load", "reload", "home", "jog", "mdi", "touchOff"]) ui.gates[name] = { action: name, enabled: false, reason: "linuxcnc_homing_active" };
|
||||||
|
}
|
||||||
const gates = this.xyzacProgram
|
const gates = this.xyzacProgram
|
||||||
? {
|
? {
|
||||||
load: this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }),
|
load: this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }),
|
||||||
@@ -483,6 +490,14 @@ export class SimSession {
|
|||||||
task,
|
task,
|
||||||
program,
|
program,
|
||||||
motion,
|
motion,
|
||||||
|
...(this.xyzacProgram ? { source_status: {
|
||||||
|
task: { mode: task.mode, state: axis.source_task_state, interpState: axis.source_interp_state },
|
||||||
|
traj: { mode: this.wasmHomeLimit.mode, motion_type: wasmMotion.state.motion_type },
|
||||||
|
homing_active: Boolean(homeLimit.state.homing),
|
||||||
|
homing_history: this.homingHistory,
|
||||||
|
joint: homeLimit.state.joint,
|
||||||
|
authority: { task: "wasm-task-adapter", homing: "linuxcnc-homing.c", home_state: "joint.N.home-state" },
|
||||||
|
} } : {}),
|
||||||
coordinates: { machine: [...axisCoordinates], relative: axisCoordinates.map((value, index) => value - displayedOffsets[index]), commanded: [...axisCoordinates], offsets: displayedOffsets, system: coordinateContext?.system ?? 1 },
|
coordinates: { machine: [...axisCoordinates], relative: axisCoordinates.map((value, index) => value - displayedOffsets[index]), commanded: [...axisCoordinates], offsets: displayedOffsets, system: coordinateContext?.system ?? 1 },
|
||||||
overrides: { feed: motion.feed_override, spindle: [motion.spindle_override] },
|
overrides: { feed: motion.feed_override, spindle: [motion.spindle_override] },
|
||||||
wasm_motion: wasmMotion,
|
wasm_motion: wasmMotion,
|
||||||
@@ -556,6 +571,8 @@ export class SimSession {
|
|||||||
const program = this.xyzacProgram.snapshot();
|
const program = this.xyzacProgram.snapshot();
|
||||||
const io = this.xyzacIo?.canonical?.() ?? {};
|
const io = this.xyzacIo?.canonical?.() ?? {};
|
||||||
const wasmMotion = this.wasmMotion.read();
|
const wasmMotion = this.wasmMotion.read();
|
||||||
|
if (["HOME", "SET_MODE"].includes(input.kind) && Number(wasmMotion.state?.queue_depth) > 0) return { enabled: false, reason: "xyzac_motion_active" };
|
||||||
|
if (input.kind === "JOG_START" && !this.wasmHomeLimit.read().state.homed) return { enabled: false, reason: "xyzac_not_all_homed" };
|
||||||
if (input.kind === "PROGRAM_LOAD" && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) > 0) {
|
if (input.kind === "PROGRAM_LOAD" && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) > 0) {
|
||||||
return { action: null, enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } };
|
return { action: null, enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } };
|
||||||
}
|
}
|
||||||
@@ -563,7 +580,7 @@ export class SimSession {
|
|||||||
return { action: input.payload?.type === 0 ? "switchkinsIdentity" : input.payload?.type === 1 ? "switchkinsXyzac" : "switchkinsUserk", enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } };
|
return { action: input.payload?.type === 0 ? "switchkinsIdentity" : input.payload?.type === 1 ? "switchkinsXyzac" : "switchkinsUserk", enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } };
|
||||||
}
|
}
|
||||||
const homeLimit = this.wasmHomeLimit.read();
|
const homeLimit = this.wasmHomeLimit.read();
|
||||||
if (this.pendingHome && ["HOME", "JOG_START", "MDI_EXECUTE", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_STEP", "KINEMATICS_SELECT", "SET_MODE", "TOUCH_OFF", "SPINDLE_COMMAND", "SPINDLE_CONSTANT", "SPINDLE_BRAKE", "COOLANT_COMMAND"].includes(input.kind)) {
|
if (this.wasmHomeLimit.active && ["HOME", "JOG_START", "JOG_STOP", "MDI_EXECUTE", "PROGRAM_LOAD", "LOAD_WORKSPACE", "PROGRAM_RUN", "PROGRAM_STEP", "PROGRAM_RESTART", "PROGRAM_SEEK", "KINEMATICS_SELECT", "SET_MODE", "TOUCH_OFF", "RECOVER", "CHECKPOINT"].includes(input.kind)) {
|
||||||
return { action: PANEL_GATE_ACTIONS[input.kind] ?? "home", enabled: false, reason: "xyzac_home_in_progress", taskState: task.state, interpState: "READING" };
|
return { action: PANEL_GATE_ACTIONS[input.kind] ?? "home", enabled: false, reason: "xyzac_home_in_progress", taskState: task.state, interpState: "READING" };
|
||||||
}
|
}
|
||||||
const estop = Boolean(io.estop);
|
const estop = Boolean(io.estop);
|
||||||
@@ -659,8 +676,10 @@ export class SimSession {
|
|||||||
// canonical P-number form (G54=P1, ..., G59.3=P9).
|
// canonical P-number form (G54=P1, ..., G59.3=P9).
|
||||||
const block = `G10 L20 P${systemNumber} ${axis}${formatted}`;
|
const block = `G10 L20 P${systemNumber} ${axis}${formatted}`;
|
||||||
const savedMode = this.task.state.mode;
|
const savedMode = this.task.state.mode;
|
||||||
|
const savedMotionMode = this.wasmHomeLimit.mode;
|
||||||
try {
|
try {
|
||||||
if (savedMode !== "MDI") this.task.setMode("MDI");
|
if (savedMode !== "MDI") this.task.setMode("MDI");
|
||||||
|
this.wasmHomeLimit.setMode("COORD");
|
||||||
this.wasmMotion.setPaused(false);
|
this.wasmMotion.setPaused(false);
|
||||||
const synchronized = this.xyzacProgram.synchronizePosition(this.wasmMotion.read().state.position);
|
const synchronized = this.xyzacProgram.synchronizePosition(this.wasmMotion.read().state.position);
|
||||||
if (synchronized.status !== "verified") return synchronized;
|
if (synchronized.status !== "verified") return synchronized;
|
||||||
@@ -675,6 +694,7 @@ export class SimSession {
|
|||||||
return { status: "error", code: String(error?.message ?? error) };
|
return { status: "error", code: String(error?.message ?? error) };
|
||||||
} finally {
|
} finally {
|
||||||
if (this.task.state.mode !== savedMode) this.task.setMode(savedMode);
|
if (this.task.state.mode !== savedMode) this.task.setMode(savedMode);
|
||||||
|
this.wasmHomeLimit.setMode(savedMotionMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1359,7 +1379,7 @@ export class SimSession {
|
|||||||
|
|
||||||
const deviceResponse = (kind, payload) => {
|
const deviceResponse = (kind, payload) => {
|
||||||
if (this.xyzacProgram && ["SPINDLE_COMMAND", "SPINDLE_BRAKE", "SPINDLE_CONSTANT"].includes(kind) && !payload?.source_keyboard && (kind !== "SPINDLE_COMMAND" || payload?.delta === undefined)) {
|
if (this.xyzacProgram && ["SPINDLE_COMMAND", "SPINDLE_BRAKE", "SPINDLE_CONSTANT"].includes(kind) && !payload?.source_keyboard && (kind !== "SPINDLE_COMMAND" || payload?.delta === undefined)) {
|
||||||
try { if (this.task.state.mode !== "MANUAL") this.task.setMode("MANUAL"); }
|
try { if (this.task.state.mode !== "MANUAL") { this.task.setMode("MANUAL"); this.wasmHomeLimit.setMode(this.wasmHomeLimit.read().state.homed ? "TELEOP" : "FREE"); } }
|
||||||
catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
||||||
}
|
}
|
||||||
const result = this.xyzacIo ? this.xyzacIo.command(kind, payload ?? {}) : this.devices.command(kind, payload ?? {});
|
const result = this.xyzacIo ? this.xyzacIo.command(kind, payload ?? {}) : this.devices.command(kind, payload ?? {});
|
||||||
@@ -1387,8 +1407,7 @@ export class SimSession {
|
|||||||
this.xyzacAppendDeferredOnActive = false;
|
this.xyzacAppendDeferredOnActive = false;
|
||||||
this.wasmMotion.abort();
|
this.wasmMotion.abort();
|
||||||
this.wasmMotion.setPaused(true);
|
this.wasmMotion.setPaused(true);
|
||||||
this.pendingHome = null;
|
this.wasmHomeLimit.cancel(true);
|
||||||
this.xyzacMotionMode = "TELEOP";
|
|
||||||
const resetProgram = this.resetXyzacProgramToLoaded();
|
const resetProgram = this.resetXyzacProgramToLoaded();
|
||||||
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
|
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
|
||||||
this.task.setProgram("loaded", 0);
|
this.task.setProgram("loaded", 0);
|
||||||
@@ -1460,7 +1479,11 @@ export class SimSession {
|
|||||||
}
|
}
|
||||||
const mode = String(input.payload?.mode ?? "").toUpperCase();
|
const mode = String(input.payload?.mode ?? "").toUpperCase();
|
||||||
if (!["MANUAL", "MDI", "AUTO"].includes(mode)) return finish(this.error(input, "task_mode", "task_mode_invalid"), "failed");
|
if (!["MANUAL", "MDI", "AUTO"].includes(mode)) return finish(this.error(input, "task_mode", "task_mode_invalid"), "failed");
|
||||||
try { this.task.setMode(mode); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
try {
|
||||||
|
this.task.setMode(mode);
|
||||||
|
if (this.xyzacProgram && mode === "MANUAL") this.wasmHomeLimit.setMode(this.wasmHomeLimit.read().state.homed ? "TELEOP" : "FREE");
|
||||||
|
if (this.xyzacProgram && mode !== "MANUAL" && this.wasmHomeLimit.read().state.homed) this.wasmHomeLimit.setMode("COORD");
|
||||||
|
} catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
||||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, mode, task: this.task.export(), operation_id: input.operation_id ?? null }));
|
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, mode, task: this.task.export(), operation_id: input.operation_id ?? null }));
|
||||||
}
|
}
|
||||||
case "POWER": {
|
case "POWER": {
|
||||||
@@ -1492,8 +1515,7 @@ export class SimSession {
|
|||||||
this.xyzacAppendDeferredOnActive = false;
|
this.xyzacAppendDeferredOnActive = false;
|
||||||
this.wasmMotion.abort();
|
this.wasmMotion.abort();
|
||||||
this.wasmMotion.setPaused(true);
|
this.wasmMotion.setPaused(true);
|
||||||
this.pendingHome = null;
|
this.wasmHomeLimit.cancel(true);
|
||||||
this.xyzacMotionMode = "TELEOP";
|
|
||||||
const resetProgram = this.resetXyzacProgramToLoaded();
|
const resetProgram = this.resetXyzacProgramToLoaded();
|
||||||
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
|
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
|
||||||
this.task.setProgram("loaded", 0);
|
this.task.setProgram("loaded", 0);
|
||||||
@@ -1612,9 +1634,8 @@ export class SimSession {
|
|||||||
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} })
|
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} })
|
||||||
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
|
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
|
||||||
this.xyzacProgram = new WasmProgramRuntime(this.core);
|
this.xyzacProgram = new WasmProgramRuntime(this.core);
|
||||||
|
this.homingHistory = [];
|
||||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||||
this.xyzacMotionMode = "TELEOP";
|
|
||||||
this.pendingHome = null;
|
|
||||||
if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics);
|
if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics);
|
||||||
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
|
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
|
||||||
const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20);
|
const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20);
|
||||||
@@ -1627,7 +1648,7 @@ export class SimSession {
|
|||||||
}
|
}
|
||||||
if (configuration?.home_limits && this.motionAuthority === "wasm") {
|
if (configuration?.home_limits && this.motionAuthority === "wasm") {
|
||||||
this.wasmHomeLimit.destroy();
|
this.wasmHomeLimit.destroy();
|
||||||
this.wasmHomeLimit = new WasmHomeLimit(this.core, {
|
this.wasmHomeLimit = this.xyzacProgram ? new WasmNativeHoming(this.core, configuration, this.xyzacKinematics) : new WasmHomeLimit(this.core, {
|
||||||
jointCount: configuration.home_limits.joint_count ?? 3,
|
jointCount: configuration.home_limits.joint_count ?? 3,
|
||||||
kinematicsType: configuration.home_limits.kinematics_type ?? 1,
|
kinematicsType: configuration.home_limits.kinematics_type ?? 1,
|
||||||
coordinates: configuration.home_limits.coordinates ?? "XYZ",
|
coordinates: configuration.home_limits.coordinates ?? "XYZ",
|
||||||
@@ -1681,6 +1702,7 @@ export class SimSession {
|
|||||||
})) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
|
})) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
|
||||||
}
|
}
|
||||||
case "SIM_STEP": {
|
case "SIM_STEP": {
|
||||||
|
const homingSample = Boolean(this.xyzacProgram && this.wasmHomeLimit.active);
|
||||||
const cycles = input.payload?.cycles;
|
const cycles = input.payload?.cycles;
|
||||||
const limit = validateLimit("max_tick_step", cycles);
|
const limit = validateLimit("max_tick_step", cycles);
|
||||||
if (limit.status !== "accepted") return finish(this.envelope("ERROR", input, { ...limit, operation_id: input.operation_id ?? null, simulation_tick: this.tick, retryable: false }), "failed");
|
if (limit.status !== "accepted") return finish(this.envelope("ERROR", input, { ...limit, operation_id: input.operation_id ?? null, simulation_tick: this.tick, retryable: false }), "failed");
|
||||||
@@ -1711,6 +1733,7 @@ export class SimSession {
|
|||||||
this.scheduler.step(cycles, {
|
this.scheduler.step(cycles, {
|
||||||
task: (nextTick) => {
|
task: (nextTick) => {
|
||||||
if (this.xyzacProgram) {
|
if (this.xyzacProgram) {
|
||||||
|
if (this.wasmHomeLimit.active) return;
|
||||||
const wasmState = this.xyzacProgram.snapshot();
|
const wasmState = this.xyzacProgram.snapshot();
|
||||||
if (wasmState.status !== 2) return;
|
if (wasmState.status !== 2) return;
|
||||||
if (this.pendingTaskSync !== 0) {
|
if (this.pendingTaskSync !== 0) {
|
||||||
@@ -1907,7 +1930,24 @@ export class SimSession {
|
|||||||
let wasmMotion = this.wasmMotion.read();
|
let wasmMotion = this.wasmMotion.read();
|
||||||
const programPaused = this.xyzacProgram?.snapshot().status === 3;
|
const programPaused = this.xyzacProgram?.snapshot().status === 3;
|
||||||
for (let servo = 0; servo < this.xyzacTaskCycleTicks; servo += 1) {
|
for (let servo = 0; servo < this.xyzacTaskCycleTicks; servo += 1) {
|
||||||
|
if (this.xyzacProgram && this.wasmHomeLimit.active) {
|
||||||
|
const home = this.wasmHomeLimit.step();
|
||||||
|
if (home.status !== "verified") throw new Error(home.code);
|
||||||
|
const transition = home.state.joint.map((j) => `${j.home_state}:${Number(j.homing)}:${Number(j.homed)}`).join(",");
|
||||||
|
if (this.homingHistory.at(-1)?.transition !== transition) this.homingHistory = [...this.homingHistory.slice(-63), { tick: home.state.tick, transition, joint: home.state.joint }];
|
||||||
|
// Coordinate synchronization after native motor-offset /
|
||||||
|
// free_tp updates, not an artificial move to machine zero.
|
||||||
|
this.wasmMotion.setActiveKinematicsTag(this.xyzacKinematics.activeMode);
|
||||||
|
this.wasmMotion.setPosition(home.state.pose);
|
||||||
|
wasmMotion = this.wasmMotion.read();
|
||||||
|
if (!home.state.homing) {
|
||||||
|
const sync = this.xyzacProgram.synchronizePosition(home.state.pose);
|
||||||
|
if (sync.status !== "verified") throw new Error(sync.code);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (programPaused && !this.wasmMotion.isStepping()) break;
|
if (programPaused && !this.wasmMotion.isStepping()) break;
|
||||||
|
if (this.wasmMotion.controls.paused && this.wasmMotion.read().state.queue_depth === 0) continue;
|
||||||
if (this.planner === "linuxcnc-tp" && typeof this.wasmMotion.setExecutionState === "function") {
|
if (this.planner === "linuxcnc-tp" && typeof this.wasmMotion.setExecutionState === "function") {
|
||||||
this.wasmMotion.setExecutionState(this.xyzacExecutionWaitSamples > 0 ? 2 : 3);
|
this.wasmMotion.setExecutionState(this.xyzacExecutionWaitSamples > 0 ? 2 : 3);
|
||||||
}
|
}
|
||||||
@@ -1962,18 +2002,6 @@ export class SimSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.pendingHome && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) === 0) {
|
|
||||||
const pendingHome = this.pendingHome;
|
|
||||||
const home = this.wasmHomeLimit.home(pendingHome.seq, pendingHome.all ? null : pendingHome.joint);
|
|
||||||
if (home.status !== "verified") throw new Error(home.code ?? "wasm_home_commit_failed");
|
|
||||||
if (this.xyzacProgram && typeof this.xyzacProgram.synchronizePosition === "function") {
|
|
||||||
const synchronized = this.xyzacProgram.synchronizePosition(home.state.pose);
|
|
||||||
if (synchronized.status !== "verified") throw new Error(synchronized.code ?? "home_position_sync_failed");
|
|
||||||
}
|
|
||||||
this.xyzacMotionMode = pendingHome.modeBefore;
|
|
||||||
this.task.setMode("MANUAL");
|
|
||||||
this.pendingHome = null;
|
|
||||||
}
|
|
||||||
if (motionState?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
if (motionState?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
||||||
if (this.motionAuthority === "wasm") {
|
if (this.motionAuthority === "wasm") {
|
||||||
if (wasmMotion.status === "verified") {
|
if (wasmMotion.status === "verified") {
|
||||||
@@ -2051,7 +2079,7 @@ export class SimSession {
|
|||||||
? null
|
? null
|
||||||
: this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
|
: this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
|
||||||
return finish(this.envelope("COMMAND_RESULT", input, {
|
return finish(this.envelope("COMMAND_RESULT", input, {
|
||||||
status: "verified", cycles, program_steps, execution_trace,
|
status: "verified", cycles, program_steps, execution_trace, homing_sample: homingSample,
|
||||||
...(lightweightXyzac && cycles <= 32 && motion_trace.length ? { motion_trace } : {}),
|
...(lightweightXyzac && cycles <= 32 && motion_trace.length ? { motion_trace } : {}),
|
||||||
motion_authority: this.motionAuthority,
|
motion_authority: this.motionAuthority,
|
||||||
task: this.task.export(), scheduler: this.scheduler.export(), authoritative: snapshotState.authoritative,
|
task: this.task.export(), scheduler: this.scheduler.export(), authoritative: snapshotState.authoritative,
|
||||||
@@ -2143,7 +2171,7 @@ export class SimSession {
|
|||||||
if (this.xyzacProgram) {
|
if (this.xyzacProgram) {
|
||||||
const previousProgramStatus = this.xyzacProgram.snapshot().program_status;
|
const previousProgramStatus = this.xyzacProgram.snapshot().program_status;
|
||||||
const result = this.xyzacProgram.start();
|
const result = this.xyzacProgram.start();
|
||||||
if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
|
if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); this.wasmHomeLimit.setMode("COORD"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
|
||||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, reopened_taskfile: ["completed", "aborted"].includes(previousProgramStatus), start_line: 0, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, reopened_taskfile: ["completed", "aborted"].includes(previousProgramStatus), start_line: 0, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||||
}
|
}
|
||||||
const result = this.program.start();
|
const result = this.program.start();
|
||||||
@@ -2165,6 +2193,7 @@ export class SimSession {
|
|||||||
}), result.status === "verified" ? "verified" : "failed");
|
}), result.status === "verified" ? "verified" : "failed");
|
||||||
}
|
}
|
||||||
case "PROGRAM_PAUSE": case "PROGRAM_RESUME": case "PROGRAM_ABORT": {
|
case "PROGRAM_PAUSE": case "PROGRAM_RESUME": case "PROGRAM_ABORT": {
|
||||||
|
if (this.xyzacProgram && input.kind === "PROGRAM_ABORT" && this.wasmHomeLimit.active) this.wasmHomeLimit.cancel();
|
||||||
const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort";
|
const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort";
|
||||||
if (this.xyzacProgram) {
|
if (this.xyzacProgram) {
|
||||||
// PAUSE/RESUME are interpreter and TP state transitions. They must
|
// PAUSE/RESUME are interpreter and TP state transitions. They must
|
||||||
@@ -2275,6 +2304,7 @@ export class SimSession {
|
|||||||
const current = this.xyzacProgram.snapshot();
|
const current = this.xyzacProgram.snapshot();
|
||||||
const idle = !["RUNNING", "PAUSED"].includes(this.task.state.state);
|
const idle = !["RUNNING", "PAUSED"].includes(this.task.state.state);
|
||||||
this.task.setMode("AUTO");
|
this.task.setMode("AUTO");
|
||||||
|
this.wasmHomeLimit.setMode("COORD");
|
||||||
if (idle) {
|
if (idle) {
|
||||||
const started = this.xyzacProgram.start();
|
const started = this.xyzacProgram.start();
|
||||||
if (started.status !== "verified") return finish(this.error(input, "program_step", started.code), "failed");
|
if (started.status !== "verified") return finish(this.error(input, "program_step", started.code), "failed");
|
||||||
@@ -2364,6 +2394,7 @@ export class SimSession {
|
|||||||
const block = input.payload?.block ?? input.payload?.command;
|
const block = input.payload?.block ?? input.payload?.command;
|
||||||
if (this.xyzacProgram) {
|
if (this.xyzacProgram) {
|
||||||
this.task.setMode("MDI");
|
this.task.setMode("MDI");
|
||||||
|
this.wasmHomeLimit.setMode("COORD");
|
||||||
// AXIS send_mdi_command() leaves the interpreter/TP runnable after
|
// AXIS send_mdi_command() leaves the interpreter/TP runnable after
|
||||||
// a previous manual jog stop. JOG_STOP intentionally pauses the
|
// a previous manual jog stop. JOG_STOP intentionally pauses the
|
||||||
// WASM TP, so clear that transport pause before queuing MDI motion;
|
// WASM TP, so clear that transport pause before queuing MDI motion;
|
||||||
@@ -2416,6 +2447,7 @@ export class SimSession {
|
|||||||
const mode = Number(input.payload?.type);
|
const mode = Number(input.payload?.type);
|
||||||
if (![XYZAC_MODES.identity, XYZAC_MODES.xyzac, XYZAC_MODES.userk].includes(mode)) return finish(this.error(input, "kinematics", "xyzac_kinematics_type_invalid"), "failed");
|
if (![XYZAC_MODES.identity, XYZAC_MODES.xyzac, XYZAC_MODES.userk].includes(mode)) return finish(this.error(input, "kinematics", "xyzac_kinematics_type_invalid"), "failed");
|
||||||
this.task.setMode("MDI");
|
this.task.setMode("MDI");
|
||||||
|
this.wasmHomeLimit.setMode("COORD");
|
||||||
const result = this.switchXyzacKinematics(mode);
|
const result = this.switchXyzacKinematics(mode);
|
||||||
const code = mode === XYZAC_MODES.xyzac ? "M428" : mode === XYZAC_MODES.identity ? "M429" : "M430";
|
const code = mode === XYZAC_MODES.xyzac ? "M428" : mode === XYZAC_MODES.identity ? "M429" : "M430";
|
||||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, remap: { code, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" }, kinematics: result.kinematics ?? this.xyzacKinematics.snapshot(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, remap: { code, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" }, kinematics: result.kinematics ?? this.xyzacKinematics.snapshot(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||||
@@ -2430,49 +2462,22 @@ export class SimSession {
|
|||||||
// that compatible while making the explicit Web contract
|
// that compatible while making the explicit Web contract
|
||||||
// HOME {all:true}.
|
// HOME {all:true}.
|
||||||
const all = payload.all === true || (payload.all === undefined && payload.axis === undefined && payload.joint === undefined);
|
const all = payload.all === true || (payload.all === undefined && payload.axis === undefined && payload.joint === undefined);
|
||||||
const requestedAxis = all ? null : String(payload.axis ?? "").trim().toLowerCase() || null;
|
if (all && payload.joint !== undefined) return finish(this.error(input, "home_limit", "home_target_ambiguous"), "failed");
|
||||||
let requestedJoint = payload.joint;
|
if (payload.axis !== undefined) return finish(this.error(input, "home_limit", "home_joint_requires_joint_mode"), "failed");
|
||||||
if (requestedAxis !== null) {
|
const requestedJoint = payload.joint;
|
||||||
const mappedJoint = XYZAC_AXIS_TO_JOINT[requestedAxis];
|
|
||||||
if (mappedJoint === undefined) return finish(this.error(input, "home_limit", "home_axis_invalid"), "failed");
|
|
||||||
if (requestedJoint !== undefined && requestedJoint !== mappedJoint) return finish(this.error(input, "home_limit", "home_axis_joint_mismatch"), "failed");
|
|
||||||
requestedJoint = mappedJoint;
|
|
||||||
}
|
|
||||||
if (!all && requestedJoint === undefined) return finish(this.error(input, "home_limit", "home_joint_required"), "failed");
|
if (!all && requestedJoint === undefined) return finish(this.error(input, "home_limit", "home_joint_required"), "failed");
|
||||||
if (!all && (!Number.isInteger(requestedJoint) || requestedJoint < 0 || requestedJoint >= 5)) return finish(this.error(input, "home_limit", "home_joint_invalid"), "failed");
|
if (!all && (!Number.isInteger(requestedJoint) || requestedJoint < 0 || requestedJoint >= 5)) return finish(this.error(input, "home_limit", "home_joint_invalid"), "failed");
|
||||||
// AXIS home_all_joints()/home_joint() calls ensure_mode(MANUAL)
|
// AXIS home_all_joints()/home_joint() calls ensure_mode(MANUAL)
|
||||||
// before go_home() selects FREE/joint and issues c.home().
|
// before go_home() selects FREE/joint and issues c.home().
|
||||||
try { if (this.task.state.mode !== "MANUAL") this.task.setMode("MANUAL"); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
try { if (this.task.state.mode !== "MANUAL") this.task.setMode("MANUAL"); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
||||||
const modeBefore = this.xyzacMotionMode;
|
try {
|
||||||
const currentMotion = this.wasmMotion.read();
|
this.wasmHomeLimit.sync(this.wasmMotion.read().state.joint_commanded);
|
||||||
const currentPose = currentMotion.state?.position;
|
const home = this.wasmHomeLimit.home(input.seq, all ? null : requestedJoint);
|
||||||
const currentJoints = Array.isArray(currentMotion.state?.joint_commanded) && currentMotion.state.joint_commanded.length === 5
|
this.homingHistory = [{ tick: home.state.tick, transition: "command", joint: home.state.joint }];
|
||||||
? [...currentMotion.state.joint_commanded]
|
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind,
|
||||||
: [...(this.xyzacKinematics?.snapshot()?.joints ?? [currentPose.x, currentPose.y, currentPose.z, currentPose.a ?? 0, currentPose.c ?? 0])];
|
homing: { phase: "accepted", joint: all ? -1 : requestedJoint, source: "linuxcnc-homing.c" },
|
||||||
const targetHome = this.wasmHomeLimit.previewHome(all ? null : requestedJoint);
|
wasm_home_limit: home, motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||||
if (targetHome.status !== "verified") return finish(this.error(input, "home_limit", targetHome.code ?? "wasm_home_target_failed"), "failed");
|
} catch (error) { return finish(this.error(input, "home_limit", error.message), "failed"); }
|
||||||
const targetPose = targetHome.state.pose;
|
|
||||||
const needsMove = ["x", "y", "z", "a", "b", "c", "u", "v", "w"].some((axis) => Math.abs(Number(currentPose?.[axis] ?? 0) - Number(targetPose?.[axis] ?? 0)) > 1e-9);
|
|
||||||
this.xyzacMotionMode = "FREE";
|
|
||||||
if (needsMove) {
|
|
||||||
const enqueue = this.wasmMotion.enqueue({
|
|
||||||
start: currentPose,
|
|
||||||
end: targetPose,
|
|
||||||
velocity: Math.max(Number(this.xyzacRapidVelocity ?? 35), Number.EPSILON),
|
|
||||||
acceleration: Math.max(Number(this.xyzacAxisAccelerationLimits?.x ?? 300), Number.EPSILON),
|
|
||||||
motion_type: 1,
|
|
||||||
source_line: 0,
|
|
||||||
kinematics_mode: this.xyzacProgramKinematicsMode,
|
|
||||||
});
|
|
||||||
if (enqueue.status !== "verified") { this.xyzacMotionMode = modeBefore; return finish(this.error(input, "home_limit", enqueue.code ?? "home_motion_enqueue_failed"), "failed"); }
|
|
||||||
this.pendingHome = { seq: input.seq, all, axis: requestedAxis, joint: all ? null : requestedJoint, modeBefore, currentJoints, targetPose };
|
|
||||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, homing: { phase: "moving", target: all ? "all" : "joint", source_axis: requestedAxis, joint: all ? null : requestedJoint, motion_mode_before: modeBefore, motion_mode_during: "FREE", motion_mode_after: "FREE", target_pose: targetPose }, motion: this.wasmMotionProjection(), wasm_home_limit: this.wasmHomeLimit.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
|
||||||
}
|
|
||||||
const home = this.wasmHomeLimit.home(input.seq, all ? null : requestedJoint);
|
|
||||||
if (home.status !== "verified") { this.xyzacMotionMode = modeBefore; return finish(this.error(input, "home_limit", home.code), "failed"); }
|
|
||||||
this.xyzacMotionMode = modeBefore;
|
|
||||||
this.task.setMode("MANUAL");
|
|
||||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, homing: { phase: "complete", target: all ? "all" : "joint", source_axis: requestedAxis, joint: all ? null : requestedJoint, motion_mode_before: modeBefore, motion_mode_during: "FREE", motion_mode_after: this.xyzacMotionMode }, motion: this.wasmMotionProjection(), wasm_home_limit: home, motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
|
||||||
}
|
}
|
||||||
const result = this.motion.home(input.payload?.axis ?? null);
|
const result = this.motion.home(input.payload?.axis ?? null);
|
||||||
if (result.status === "verified" && this.motionAuthority === "wasm") {
|
if (result.status === "verified" && this.motionAuthority === "wasm") {
|
||||||
@@ -2520,9 +2525,7 @@ export class SimSession {
|
|||||||
if (!["x", "y", "z", "a", "c"].includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
|
if (!["x", "y", "z", "a", "c"].includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
|
||||||
const direction = input.payload?.direction === -1 ? -1 : input.payload?.direction === 1 ? 1 : null;
|
const direction = input.payload?.direction === -1 ? -1 : input.payload?.direction === 1 ? 1 : null;
|
||||||
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
|
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
|
||||||
// The Web jog buttons are world-axis controls. Mirror AXIS's
|
if (this.xyzacProgram) this.wasmHomeLimit.setMode("TELEOP");
|
||||||
// teleop selection for this adapter before submitting the TP jog.
|
|
||||||
this.xyzacMotionMode = "TELEOP";
|
|
||||||
this.wasmMotion.setPaused(false);
|
this.wasmMotion.setPaused(false);
|
||||||
const requestedIncrement = input.payload?.increment;
|
const requestedIncrement = input.payload?.increment;
|
||||||
const continuous = requestedIncrement === null || input.payload?.continuous === true;
|
const continuous = requestedIncrement === null || input.payload?.continuous === true;
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ assert.equal(panel().ui_gates.run.enabled, true);
|
|||||||
assert.equal(panel().ui_gates.step.enabled, true);
|
assert.equal(panel().ui_gates.step.enabled, true);
|
||||||
assert.equal(send("PROGRAM_RUN").payload.gate_reason, "xyzac_not_all_homed");
|
assert.equal(send("PROGRAM_RUN").payload.gate_reason, "xyzac_not_all_homed");
|
||||||
ok("HOME", { all: true });
|
ok("HOME", { all: true });
|
||||||
const homeAxis = send("HOME", { axis: "x", joint: 0 });
|
ok("SIM_STEP", { cycles: 1 });
|
||||||
assert.equal(homeAxis.payload.status, "verified");
|
assert.equal(send("HOME", { axis: "x" }).payload.message_key, "home_joint_requires_joint_mode");
|
||||||
assert.deepEqual(homeAxis.payload.homing, { phase: "complete", target: "joint", source_axis: "x", joint: 0, motion_mode_before: "TELEOP", motion_mode_during: "FREE", motion_mode_after: "TELEOP" });
|
|
||||||
|
|
||||||
ok("PROGRAM_STEP");
|
ok("PROGRAM_STEP");
|
||||||
assert.equal(panel().program.current_line, 0, "first native Step starts at line zero and pauses");
|
assert.equal(panel().program.current_line, 0, "first native Step starts at line zero and pauses");
|
||||||
|
|||||||
110
web/worker/test_native_homing.mjs
Normal file
110
web/worker/test_native_homing.mjs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
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 path = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini";
|
||||||
|
const bundle = await loadSimulationCase({ path, fetchImpl: async (url) => {
|
||||||
|
try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url), "utf8"); return { ok: true, text: async () => body }; }
|
||||||
|
catch { return { ok: false, text: async () => "" }; }
|
||||||
|
} });
|
||||||
|
const binary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
|
||||||
|
for (const [file, hash] of [["homing.c", "ee73d38f2feb8d5523b9a733cb17d635d3904667aaa219f8a02d7fbffee1ba39"], ["simple_tp.c", "5a0026f4d5bdd9d1bc55290c6ea9c399abd664633b69413c307f7353953d2603"]]) {
|
||||||
|
assert.equal(createHash("sha256").update(await readFile(new URL(`../../linuxcnc/src/emc/motion/${file}`, import.meta.url))).digest("hex"), hash, `source drift: ${file}`);
|
||||||
|
}
|
||||||
|
async function setup(id, finalHome = 0) {
|
||||||
|
const core = await createModule({ wasmBinary: binary });
|
||||||
|
const session = new XyzacSession(core, { sessionId: id });
|
||||||
|
let seq = 0;
|
||||||
|
const send = (kind, payload = {}) => session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: id, seq: ++seq, operation_id: `${id}-${seq}`, kind, payload });
|
||||||
|
const ok = (kind, payload) => { const r = send(kind, payload); assert.notEqual(r.kind, "ERROR", `${kind}: ${r.payload.message_key}`); return r.payload; };
|
||||||
|
ok("HELLO"); ok("SESSION_ATTACH");
|
||||||
|
const configuration = toWorkerConfiguration(bundle);
|
||||||
|
if (finalHome) configuration.files[path] = configuration.files[path].replace(/HOME =\s+0/g, `HOME = ${finalHome}`);
|
||||||
|
ok("LOAD_WORKSPACE", { root_path: "/workspace", configuration });
|
||||||
|
ok("PROGRAM_LOAD", { source: "G21 G90\nG1 X10 F600\nM2\n", path: "native-home.ngc" });
|
||||||
|
ok("POWER", { active: true });
|
||||||
|
const snapshot = () => ok("SNAPSHOT_REQUEST").panel_snapshot;
|
||||||
|
const drainHome = () => {
|
||||||
|
let ticks = 0;
|
||||||
|
while (session.wasmHomeLimit.active && ticks++ < 1000) ok("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||||
|
assert.equal(session.wasmHomeLimit.active, false);
|
||||||
|
return snapshot();
|
||||||
|
};
|
||||||
|
const jog = (axis, increment) => {
|
||||||
|
ok("JOG_START", { axis, direction: 1, increment });
|
||||||
|
let ticks = 0;
|
||||||
|
while (session.wasmMotion.read().state.queue_depth > 0 && ticks++ < 5000) ok("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||||
|
assert.equal(session.wasmMotion.read().state.queue_depth, 0);
|
||||||
|
};
|
||||||
|
return { session, send, ok, snapshot, drainHome, jog };
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = await setup("native-homing");
|
||||||
|
assert.equal(s.snapshot().source_status.traj.mode, "FREE");
|
||||||
|
assert.equal(s.send("HOME", { axis: "x" }).payload.message_key, "home_joint_requires_joint_mode");
|
||||||
|
for (const joint of [0, 1, 2, 3, 4]) {
|
||||||
|
const ack = s.ok("HOME", { joint });
|
||||||
|
assert.equal(ack.homing.phase, "accepted");
|
||||||
|
assert.equal(ack.panel_snapshot.source_status.homing_active, true);
|
||||||
|
assert.equal(ack.panel_snapshot.source_status.task.interpState, "INTERP_IDLE");
|
||||||
|
assert.equal(ack.panel_snapshot.axis_state.operation_state, "HOME");
|
||||||
|
assert.equal(ack.panel_snapshot.motion.homed_axes[joint], false);
|
||||||
|
const done = s.drainHome();
|
||||||
|
assert.equal(done.motion.homed_axes[joint], true);
|
||||||
|
assert.equal(done.source_status.traj.mode, joint === 4 ? "TELEOP" : "FREE");
|
||||||
|
assert.ok(done.source_status.homing_history.some((e) => e.joint[joint].home_state === 21 && e.joint[joint].homing));
|
||||||
|
}
|
||||||
|
for (const [joint, axis] of ["x", "y", "z", "a", "c"].entries()) {
|
||||||
|
s.jog(axis, 3);
|
||||||
|
const before = s.snapshot();
|
||||||
|
s.ok("HOME", { joint });
|
||||||
|
const after = s.drainHome();
|
||||||
|
after.source_status.joint.forEach((j, index) => {
|
||||||
|
assert.ok(Math.abs(j.motor_position - before.source_status.joint[index].motor_position) < 1e-8, "no-search home does not move a motor");
|
||||||
|
assert.ok(Math.abs(j.position - (index === joint ? 0 : before.source_status.joint[index].position)) < 1e-8);
|
||||||
|
});
|
||||||
|
assert.equal(after.visual.live_path_epoch, before.visual.live_path_epoch);
|
||||||
|
}
|
||||||
|
s.jog("x", 4); s.jog("y", 2);
|
||||||
|
const beforeAll = s.snapshot();
|
||||||
|
s.ok("HOME", { all: true });
|
||||||
|
for (const [kind, payload] of [["PROGRAM_RUN", {}], ["MDI_EXECUTE", { block: "G0 X1" }], ["SET_MODE", { mode: "AUTO" }]]) assert.equal(s.send(kind, payload).kind, "ERROR");
|
||||||
|
const afterAll = s.drainHome();
|
||||||
|
assert.deepEqual(afterAll.source_status.joint.map((j) => j.position), [0, 0, 0, 0, 0]);
|
||||||
|
assert.deepEqual(afterAll.source_status.joint.map((j) => j.motor_position), beforeAll.source_status.joint.map((j) => j.motor_position));
|
||||||
|
const nativeCheckpoint = s.session.wasmHomeLimit.exportState();
|
||||||
|
s.session.wasmHomeLimit.reset();
|
||||||
|
s.session.wasmHomeLimit.restoreState(nativeCheckpoint);
|
||||||
|
assert.deepEqual(s.session.wasmHomeLimit.read().state.joint, afterAll.source_status.joint);
|
||||||
|
s.ok("PROGRAM_RUN");
|
||||||
|
assert.equal(s.snapshot().source_status.task.mode, "AUTO");
|
||||||
|
assert.equal(s.snapshot().source_status.traj.mode, "COORD");
|
||||||
|
s.ok("PROGRAM_PAUSE");
|
||||||
|
assert.equal(s.snapshot().source_status.task.interpState, "INTERP_PAUSED");
|
||||||
|
assert.equal(s.send("SET_MODE", { mode: "MANUAL" }).kind, "ERROR");
|
||||||
|
s.ok("PROGRAM_ABORT"); s.ok("SET_MODE", { mode: "MANUAL" });
|
||||||
|
assert.equal(s.snapshot().source_status.traj.mode, "TELEOP");
|
||||||
|
s.ok("HOME", { joint: 0 }); s.ok("ESTOP", { active: true }); s.ok("SIM_STEP", { cycles: 1 });
|
||||||
|
assert.equal(s.snapshot().source_status.homing_active, false);
|
||||||
|
assert.equal(s.snapshot().source_status.joint[0].homed, false);
|
||||||
|
s.ok("ESTOP", { active: false }); s.ok("POWER", { active: true }); s.ok("HOME", { joint: 0 }); s.drainHome();
|
||||||
|
|
||||||
|
// A configured final move uses the original joint free_tp, not coordinated TP.
|
||||||
|
const moving = await setup("native-final-move", 5);
|
||||||
|
moving.ok("HOME", { joint: 0 });
|
||||||
|
const mid = moving.ok("SIM_STEP", { cycles: 1 }).panel_snapshot;
|
||||||
|
assert.equal(mid.source_status.joint[0].home_state, 21);
|
||||||
|
assert.ok(mid.source_status.joint[0].position > 0 && mid.source_status.joint[0].position < 5);
|
||||||
|
assert.equal(mid.wasm_motion.state.queue_depth, 0);
|
||||||
|
assert.equal(mid.source_status.joint[0].homed, false);
|
||||||
|
const done = moving.drainHome();
|
||||||
|
assert.ok(Math.abs(done.source_status.joint[0].position - 5) < 1e-8);
|
||||||
|
assert.ok(Math.abs(done.source_status.joint[0].motor_position - 5) < 1e-8);
|
||||||
|
moving.ok("HOME", { joint: 1 }); moving.ok("SIM_STEP", { cycles: 1 }); moving.ok("PROGRAM_ABORT");
|
||||||
|
moving.drainHome();
|
||||||
|
assert.equal(moving.snapshot().source_status.joint[1].homed, false);
|
||||||
|
assert.equal(moving.snapshot().source_status.homing_active, false);
|
||||||
|
console.log(JSON.stringify({ status: "verified", source: "homing.c + simple_tp.c", checks: ["five-joint-home", "no-search-coordinate-rebase", "motor-position-preserved", "native-final-move", "mode-interlocks", "abort-and-recovery", "servo-transition-history"] }));
|
||||||
@@ -1,74 +1,3 @@
|
|||||||
import assert from "node:assert/strict";
|
// Preserve the documented test entry point; the assertions now verify the
|
||||||
import { readFile } from "node:fs/promises";
|
// original homemod rather than the retired coordinated return-to-zero adapter.
|
||||||
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
import "./test_native_homing.mjs";
|
||||||
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 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 core = await createModule({ wasmBinary });
|
|
||||||
|
|
||||||
function open(sessionId) {
|
|
||||||
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 });
|
|
||||||
assert.equal(send("HELLO").kind, "CAPABILITIES");
|
|
||||||
assert.equal(send("SESSION_ATTACH").kind, "SESSION_READY");
|
|
||||||
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified");
|
|
||||||
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
|
||||||
return { session, send };
|
|
||||||
}
|
|
||||||
|
|
||||||
const partial = open("xyzac-home-axis-source");
|
|
||||||
for (const [axis, joint] of [["x", 0], ["y", 1], ["z", 2], ["a", 3], ["c", 4]]) {
|
|
||||||
const response = partial.send("HOME", { axis, joint });
|
|
||||||
assert.equal(response.kind, "COMMAND_RESULT", `${axis}: ${JSON.stringify(response.payload)}`);
|
|
||||||
assert.deepEqual(response.payload.homing, {
|
|
||||||
phase: "complete", target: "joint", source_axis: axis, joint,
|
|
||||||
motion_mode_before: "TELEOP", motion_mode_during: "FREE", motion_mode_after: "TELEOP",
|
|
||||||
});
|
|
||||||
assert.equal(response.payload.wasm_home_limit.state.homed_mask, (1 << (joint + 1)) - 1);
|
|
||||||
}
|
|
||||||
assert.equal(partial.send("HOME", { axis: "x", joint: 1 }).payload.message_key, "home_axis_joint_mismatch");
|
|
||||||
assert.equal(partial.session.wasmHomeLimit.read().state.homed_mask, 31);
|
|
||||||
|
|
||||||
const all = open("xyzac-home-all-source");
|
|
||||||
assert.equal(all.send("HOME", { all: true }).payload.wasm_home_limit.state.homed, true);
|
|
||||||
const move = all.send("MDI_EXECUTE", { block: "G21 G90 G0 X10 Y5 Z20 A30 C45" });
|
|
||||||
assert.equal(move.kind, "COMMAND_RESULT");
|
|
||||||
for (let tick = 0; tick < 20000 && all.session.wasmMotion.read().state.queue_depth > 0; tick += 1) all.send("SIM_STEP", { cycles: 1 });
|
|
||||||
assert.ok(Math.abs(all.session.wasmMotion.read().state.position.x - 10) < 1e-8);
|
|
||||||
const rehome = all.send("HOME", { all: true });
|
|
||||||
assert.equal(rehome.kind, "COMMAND_RESULT");
|
|
||||||
assert.equal(rehome.payload.homing.phase, "moving");
|
|
||||||
assert.notDeepEqual(rehome.payload.panel_snapshot.visual.pose, { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
|
|
||||||
for (let tick = 0; tick < 2000 && all.session.wasmMotion.read().state.queue_depth > 0; tick += 1) all.send("SIM_STEP", { cycles: 1 });
|
|
||||||
const completed = all.send("SNAPSHOT_REQUEST");
|
|
||||||
assert.deepEqual(completed.payload.panel_snapshot.visual.pose, { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
|
|
||||||
assert.deepEqual(completed.payload.wasm_home_limit.state.joints, [0, 0, 0, 0, 0]);
|
|
||||||
assert.ok(completed.payload.wasm_home_limit.state.home_sequence > rehome.payload.wasm_home_limit.state.home_sequence);
|
|
||||||
|
|
||||||
const running = open("xyzac-home-gate-source");
|
|
||||||
assert.equal(running.send("PROGRAM_LOAD", { source: "G21 G90\nG1 X10 F60\nM2\n", path: "home-gate.ngc" }).kind, "COMMAND_RESULT");
|
|
||||||
assert.equal(running.send("HOME", { all: true }).kind, "COMMAND_RESULT");
|
|
||||||
assert.equal(running.send("PROGRAM_RUN").kind, "COMMAND_RESULT");
|
|
||||||
assert.equal(running.send("HOME", { axis: "x", joint: 0 }).payload.gate_reason, "xyzac_interp_running");
|
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
|
||||||
schema: "xyzac-home-source-flow.v1",
|
|
||||||
status: "verified",
|
|
||||||
source: ["axis.py:home_all_joints", "axis.py:home_joint", "axis.py:go_home", "axis.tcl:update_state"],
|
|
||||||
checks: ["axis_to_joint_map", "free_joint_transaction", "pose_sync", "invalid_pair_rejection", "running_gate"],
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ assert.equal(loadEpoch, 0);
|
|||||||
assert.ok(previewCount > 0);
|
assert.ok(previewCount > 0);
|
||||||
send("POWER", { active: true });
|
send("POWER", { active: true });
|
||||||
send("HOME", { all: true });
|
send("HOME", { all: true });
|
||||||
|
send("SIM_STEP", { cycles: 1 });
|
||||||
|
|
||||||
const run = send("PROGRAM_RUN");
|
const run = send("PROGRAM_RUN");
|
||||||
const runEpoch = epoch(run);
|
const runEpoch = epoch(run);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const snapshot = () => session.xyzacProgram.snapshot();
|
|||||||
const sample = (cycles = 5) => send("SIM_STEP", { cycles, response: "motion_delta" });
|
const sample = (cycles = 5) => send("SIM_STEP", { cycles, response: "motion_delta" });
|
||||||
send("HELLO"); send("SESSION_ATTACH"); send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
send("HELLO"); send("SESSION_ATTACH"); send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||||
send("PROGRAM_LOAD", { path: "pause-reading.ngc", source: "G21 G90\n" + Array.from({ length: 80 }, (_, i) => `G1 X${i % 2 ? 0 : 2} F600`).join("\n") + "\nM2\n" });
|
send("PROGRAM_LOAD", { path: "pause-reading.ngc", source: "G21 G90\n" + Array.from({ length: 80 }, (_, i) => `G1 X${i % 2 ? 0 : 2} F600`).join("\n") + "\nM2\n" });
|
||||||
send("POWER", { active: true }); send("HOME", { all: true }); send("PROGRAM_RUN"); sample();
|
send("POWER", { active: true }); send("HOME", { all: true }); sample(1); send("PROGRAM_RUN"); sample();
|
||||||
for (let round = 0; round < 3; round++) {
|
for (let round = 0; round < 3; round++) {
|
||||||
send("PROGRAM_PAUSE");
|
send("PROGRAM_PAUSE");
|
||||||
const pose = motion().position, line = snapshot().current_line, queue = motion().queue_depth;
|
const pose = motion().position, line = snapshot().current_line, queue = motion().queue_depth;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const source = "G21\nG90\nG0 X0 Y0 Z10 A0 C0\nG1 X10 Y5 Z5 A15 C20 F600\nG1 X0 Y
|
|||||||
assert.equal(send("PROGRAM_LOAD", { source, path: "rerun.ngc" }).payload.status, "verified");
|
assert.equal(send("PROGRAM_LOAD", { source, path: "rerun.ngc" }).payload.status, "verified");
|
||||||
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
||||||
assert.equal(send("HOME", { all: true }).payload.status, "verified");
|
assert.equal(send("HOME", { all: true }).payload.status, "verified");
|
||||||
|
assert.equal(send("SIM_STEP", { cycles: 1 }).payload.status, "verified");
|
||||||
|
|
||||||
assert.equal(send("PROGRAM_RUN").payload.task.state, "RUNNING");
|
assert.equal(send("PROGRAM_RUN").payload.task.state, "RUNNING");
|
||||||
let completed;
|
let completed;
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ async function create(id) {
|
|||||||
const loaded = send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
const loaded = send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||||
assert.equal(loaded.payload.status, "verified");
|
assert.equal(loaded.payload.status, "verified");
|
||||||
assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified");
|
assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified");
|
||||||
assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true);
|
assert.equal(send("HOME", "home-all", { all: true }).payload.homing.phase, "accepted");
|
||||||
|
assert.equal(send("SIM_STEP", "home-complete", { cycles: 1 }).payload.panel_snapshot.motion.homed, true);
|
||||||
return { core, session, send };
|
return { core, session, send };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ for (const estop of [false, true]) {
|
|||||||
assert.equal(snapshot().ui_gates.abort.enabled, false, "Stop widget must be disabled at IDLE");
|
assert.equal(snapshot().ui_gates.abort.enabled, false, "Stop widget must be disabled at IDLE");
|
||||||
}
|
}
|
||||||
send("ESTOP", { active: false }); send("POWER", { active: true }); send("HOME", { all: true });
|
send("ESTOP", { active: false }); send("POWER", { active: true }); send("HOME", { all: true });
|
||||||
|
send("SIM_STEP", { cycles: 1 });
|
||||||
send("SET_MODE", { mode: "MDI" });
|
send("SET_MODE", { mode: "MDI" });
|
||||||
assert.equal(snapshot().gates.spindle.enabled, true);
|
assert.equal(snapshot().gates.spindle.enabled, true);
|
||||||
assert.equal(snapshot().gates.touchOff.enabled, true);
|
assert.equal(snapshot().gates.touchOff.enabled, true);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: to
|
|||||||
assert.equal(send("PROGRAM_LOAD", { source: "G21 G90\nG0 X10 Y5 Z200 A30 C45\nG1 X20 Y10 Z180 A35 C50 F100\nM2\n", path: "touch.ngc" }).payload.status, "verified");
|
assert.equal(send("PROGRAM_LOAD", { source: "G21 G90\nG0 X10 Y5 Z200 A30 C45\nG1 X20 Y10 Z180 A35 C50 F100\nM2\n", path: "touch.ngc" }).payload.status, "verified");
|
||||||
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
||||||
assert.equal(send("HOME", { all: true }).payload.status, "verified");
|
assert.equal(send("HOME", { all: true }).payload.status, "verified");
|
||||||
|
assert.equal(send("SIM_STEP", { cycles: 1 }).payload.status, "verified");
|
||||||
|
|
||||||
const jogStart = send("JOG_START", { axis: "X", direction: 1, increment: null, continuous: true });
|
const jogStart = send("JOG_START", { axis: "X", direction: 1, increment: null, continuous: true });
|
||||||
assert.equal(jogStart.payload.status, "verified");
|
assert.equal(jogStart.payload.status, "verified");
|
||||||
|
|||||||
@@ -108,17 +108,6 @@ export class WasmHomeLimit {
|
|||||||
return this.read();
|
return this.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the canonical target of a home command without committing the
|
|
||||||
// homed mask or pose. The actual commit is performed only after the LinuxCNC
|
|
||||||
// TP has executed the queued move to this target.
|
|
||||||
previewHome(axis = null) {
|
|
||||||
const saved = this.exportState();
|
|
||||||
const target = this.home(0, axis);
|
|
||||||
try { this.restoreState(saved); } catch (error) { return { status: "error", code: String(error?.message ?? error) }; }
|
|
||||||
if (target.status !== "verified") return target;
|
|
||||||
return { status: "verified", state: target.state };
|
|
||||||
}
|
|
||||||
|
|
||||||
check(joints) {
|
check(joints) {
|
||||||
if (!this.available) return this.read();
|
if (!this.available) return this.read();
|
||||||
if (!Array.isArray(joints) || joints.length < this.jointCount || joints.some((value) => typeof value !== "number" || !Number.isFinite(value))) return { status: "error", code: "wasm_home_joints_invalid" };
|
if (!Array.isArray(joints) || joints.length < this.jointCount || joints.some((value) => typeof value !== "number" || !Number.isFinite(value))) return { status: "error", code: "wasm_home_joints_invalid" };
|
||||||
|
|||||||
88
web/worker/wasm-native-homing.mjs
Normal file
88
web/worker/wasm-native-homing.mjs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { fnv1a64 } from "./replay.mjs";
|
||||||
|
import { parseIni } from "../workspace/ini-parser.mjs";
|
||||||
|
|
||||||
|
const STATES = ["IDLE", "START", "UNLOCK", "UNLOCK_WAIT", "INITIAL_BACKOFF_START", "INITIAL_BACKOFF_WAIT", "INITIAL_SEARCH_START", "INITIAL_SEARCH_WAIT", "SET_COARSE_POSITION", "FINAL_BACKOFF_START", "FINAL_BACKOFF_WAIT", "RISE_SEARCH_START", "RISE_SEARCH_WAIT", "FALL_SEARCH_START", "FALL_SEARCH_WAIT", "SET_SWITCH_POSITION", "INDEX_ONLY_START", "INDEX_SEARCH_START", "INDEX_SEARCH_WAIT", "SET_INDEX_POSITION", "FINAL_MOVE_START", "FINAL_MOVE_WAIT", "LOCK", "LOCK_WAIT", "FINISHED", "ABORT"];
|
||||||
|
export class WasmNativeHoming {
|
||||||
|
constructor(core, configuration, kinematics) {
|
||||||
|
this.core = core;
|
||||||
|
this.kinematics = kinematics;
|
||||||
|
this.jointCount = 5;
|
||||||
|
this.size = core._sim_native_home_size();
|
||||||
|
this.pointer = core._malloc(this.size);
|
||||||
|
this.buffer = core._malloc(12 * 8);
|
||||||
|
if (!this.pointer || !this.buffer) throw new Error("native_home_allocation_failed");
|
||||||
|
this.available = true;
|
||||||
|
const parsed = parseIni({ entrypoint: configuration.entrypoint, workspace: { read: (path) => {
|
||||||
|
const value = configuration.files[path.replace(/^\/workspace\//, "")];
|
||||||
|
if (typeof value !== "string") throw new Error(`home_ini_missing:${path}`);
|
||||||
|
return value;
|
||||||
|
} } });
|
||||||
|
this.parameters = Array.from({ length: 5 }, (_, j) => {
|
||||||
|
const section = parsed.sections[`JOINT_${j}`] ?? {};
|
||||||
|
const n = (key, fallback) => section[key] === undefined ? fallback : Number(section[key]);
|
||||||
|
const flags = [["HOME_IGNORE_LIMITS", 1], ["HOME_USE_INDEX", 2], ["HOME_IS_SHARED", 4], ["LOCKING_INDEXER", 8], ["HOME_INDEX_NO_ENCODER_RESET", 128]].reduce((mask, [key, bit]) => mask | (n(key, 0) ? bit : 0), 0);
|
||||||
|
const encoder = n("HOME_ABSOLUTE_ENCODER", 0);
|
||||||
|
if (![0, 1, 2].includes(encoder)) throw new Error("native_home_absolute_encoder_invalid");
|
||||||
|
return [n("HOME_OFFSET", 0), n("HOME", 0), n("HOME_FINAL_VEL", -1), n("HOME_SEARCH_VEL", 0), n("HOME_LATCH_VEL", 0), flags | (encoder ? 16 | 32 | (encoder === 2 ? 64 : 0) : 0), n("HOME_SEQUENCE", 999), n("VOLATILE_HOME", 0), n("MIN_LIMIT", -1e9), n("MAX_LIMIT", 1e9), n("MAX_VELOCITY", 1), n("MAX_ACCELERATION", 1)];
|
||||||
|
});
|
||||||
|
this.period = Number(parsed.sections.EMCMOT?.SERVO_PERIOD ?? 1000000) / 1e9;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
invoke(name, ...args) {
|
||||||
|
const rc = this.core[`_sim_native_home_${name}`](this.pointer, ...args);
|
||||||
|
if (rc !== 0) throw new Error(`native_home_${name}_${rc}`);
|
||||||
|
}
|
||||||
|
get(j, field) { return this.core._sim_native_home_get(this.pointer, j, field); }
|
||||||
|
get active() { return Boolean(this.get(0, 0)); }
|
||||||
|
get mode() { return [null, "FREE", "COORD", "TELEOP"][this.get(0, 1)]; }
|
||||||
|
setMode(mode) { this.invoke("mode", { FREE: 1, COORD: 2, TELEOP: 3 }[mode]); }
|
||||||
|
sync(joints) { this.core.HEAPF64.set(joints, this.buffer / 8); this.invoke("sync", this.buffer); }
|
||||||
|
home(seq, joint = null) { this.invoke("start", joint ?? -1, seq); return this.read(); }
|
||||||
|
step() { this.invoke("step"); return this.read(); }
|
||||||
|
cancel(volatile = false) { this.invoke("cancel", Number(volatile)); return this.read(); }
|
||||||
|
reset() {
|
||||||
|
this.invoke("init", 5, this.period);
|
||||||
|
this.parameters.forEach((p, j) => { this.core.HEAPF64.set(p, this.buffer / 8); this.invoke("config", j, this.buffer); });
|
||||||
|
return this.read();
|
||||||
|
}
|
||||||
|
check(joints) { if (!this.active) this.sync(joints); return this.read(); }
|
||||||
|
read() {
|
||||||
|
const joint = Array.from({ length: 5 }, (_, j) => ({
|
||||||
|
joint: j, homing: Boolean(this.get(j, 4)), homed: Boolean(this.get(j, 5)),
|
||||||
|
home_state: this.get(j, 6), home_state_name: `HOME_${STATES[this.get(j, 6)]}`,
|
||||||
|
position: this.get(j, 7), motor_position: this.get(j, 8), motor_offset: this.get(j, 9),
|
||||||
|
home_sequence: this.get(j, 10), velocity: this.get(j, 11), index_enable: Boolean(this.get(j, 12)),
|
||||||
|
}));
|
||||||
|
const joints = joint.map((j) => j.position);
|
||||||
|
const result = this.kinematics.forward(joints);
|
||||||
|
return { status: result.status, ...(result.code ? { code: result.code } : {}), state: {
|
||||||
|
schema: "sim_home_limit_state.v1", source: "linuxcnc-homing.c", joint_count: 5,
|
||||||
|
homed: joint.every((j) => j.homed), homed_mask: joint.reduce((mask, j) => mask | (j.homed ? 1 << j.joint : 0), 0),
|
||||||
|
homing: this.active, motion_mode: this.mode, joint, joints, pose: result.pose ?? null,
|
||||||
|
home_sequence: this.get(0, 3), tick: this.get(0, 2), last_error: result.result ?? 0, limit_error: result.result === -702 ? -702 : 0, kinematics_error: result.result ?? 0,
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
exportState() {
|
||||||
|
// emcmot_joint_t includes large unused compensation tables. Preserve the
|
||||||
|
// complete native state while avoiding repeated zero bytes in checkpoints.
|
||||||
|
const bytes = this.core.HEAPU8.subarray(this.pointer, this.pointer + this.size);
|
||||||
|
const nonzero = [];
|
||||||
|
for (let i = 0; i < bytes.length; i++) if (bytes[i] !== 0) nonzero.push([i, bytes[i]]);
|
||||||
|
const value = { schema: "native-homing.v1", parameters: this.parameters, period: this.period,
|
||||||
|
byte_length: this.size, nonzero_bytes: nonzero };
|
||||||
|
return { ...value, state_hash: fnv1a64(value) };
|
||||||
|
}
|
||||||
|
restoreState(value) {
|
||||||
|
const { state_hash, ...unsigned } = value;
|
||||||
|
if (value.schema !== "native-homing.v1" || state_hash !== fnv1a64(unsigned) || value.byte_length !== this.size || value.period !== this.period || fnv1a64(value.parameters) !== fnv1a64(this.parameters) || !Array.isArray(value.nonzero_bytes)) throw new Error("native_home_restore_invalid");
|
||||||
|
let previous = -1;
|
||||||
|
const bytes = new Uint8Array(this.size);
|
||||||
|
for (const [offset, byte] of value.nonzero_bytes) {
|
||||||
|
if (!Number.isInteger(offset) || offset <= previous || offset >= this.size || !Number.isInteger(byte) || byte < 1 || byte > 255) throw new Error("native_home_restore_bytes_invalid");
|
||||||
|
bytes[offset] = byte; previous = offset;
|
||||||
|
}
|
||||||
|
this.core.HEAPU8.set(bytes, this.pointer);
|
||||||
|
return this.read();
|
||||||
|
}
|
||||||
|
destroy() { this.core._free(this.pointer); this.core._free(this.buffer); }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user