From 78d884bc67a116d787982790cbfd66ca8e2b6467 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Fri, 28 Aug 2026 13:04:47 +0800 Subject: [PATCH] Enforce LinuxCNC WASM XYZAC command authority --- coverage/xyzac-trt-gate-matrix.report.json | 63 ++++++++- coverage/xyzac-trt-hal-tool-alarm.report.json | 6 +- coverage/xyzac-trt-manual-control.report.json | 6 +- .../xyzac-trt-program-preview.report.json | 6 +- coverage/xyzac-trt-protocol-react.report.json | 16 ++- coverage/xyzac-trt-recovery.report.json | 12 +- ...zac-trt-source-slice-inventory.report.json | 34 ++++- coverage/xyzac-trt-tool-preview.report.json | 21 ++- coverage/xyzac-trt-trace-parity.report.json | 4 +- ...xyzac-trt-wasm-authority-audit.report.json | 88 ++++++------ ...zac-trt-wasm-only-release-gate.report.json | 8 +- .../xyzac-trt-web-wasm-parity.report.json | 30 ++-- coverage/xyzac-trt-workflow.report.json | 6 +- .../generate_xyzac_trt_gate_matrix_report.py | 14 +- scripts/generate_xyzac_trt_source_slices.py | 6 +- wasm-core/CMakeLists.txt | 1 + wasm-core/include/sim_io.h | 1 + wasm-core/src/sim_io.c | 27 ++++ wasm-core/tests/sim_io_test.c | 3 + web/app/axis-contract.mjs | 18 ++- web/app/axis-operation-controller.mjs | 23 +-- web/app/test_axis_operation_controller.mjs | 9 +- web/react/src/App.tsx | 31 +++-- web/react/src/legacy.d.ts | 2 - web/worker/axis-state.mjs | 16 ++- web/worker/capabilities.mjs | 2 +- web/worker/sim-session.mjs | 131 +++++++++++++++++- web/worker/test_xyzac_button_constraints.mjs | 62 +++++++++ web/worker/test_xyzac_gate_matrix.mjs | 20 ++- web/worker/test_xyzac_hal_tool_alarm.mjs | 1 + web/worker/test_xyzac_machine_sequence.mjs | 12 +- web/worker/test_xyzac_manual_control.mjs | 4 +- web/worker/test_xyzac_protocol_react.mjs | 13 +- web/worker/test_xyzac_recovery.mjs | 1 + web/worker/test_xyzac_rtcp.mjs | 1 + web/worker/test_xyzac_trace_parity.mjs | 6 +- web/worker/test_xyzac_trt_case.mjs | 1 + web/worker/test_xyzac_trt_io_authority.mjs | 4 +- .../test_xyzac_trt_wasm_program_authority.mjs | 1 + web/worker/test_xyzac_workflow.mjs | 1 + web/worker/wasm-xyzac-io.mjs | 2 + web/workspace/xyzac-trt-case-contract.mjs | 6 +- web/workspace/xyzac-trt-source-slices.mjs | 34 ++++- 43 files changed, 567 insertions(+), 186 deletions(-) create mode 100644 web/worker/test_xyzac_button_constraints.mjs diff --git a/coverage/xyzac-trt-gate-matrix.report.json b/coverage/xyzac-trt-gate-matrix.report.json index 453b7e5..00bcd01 100644 --- a/coverage/xyzac-trt-gate-matrix.report.json +++ b/coverage/xyzac-trt-gate-matrix.report.json @@ -1,5 +1,30 @@ { "authority": "linuxcnc-wasm-worker", + "button_constraints": { + "authority": "linuxcnc-wasm-worker", + "case": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", + "gates": { + "off_blocks_motion": true, + "pause_resume_exclusive": true, + "running_blocks_manual": true, + "running_blocks_power_off": true + }, + "schema": "xyzac-button-constraints.v1", + "sequence": [ + "ESTOP", + "ESTOP_RESET", + "POWER_ON", + "HOME", + "IDLE", + "RUNNING", + "PAUSED", + "RESUME", + "ABORT", + "ESTOP" + ], + "state_hash": "fnv1a64:7c18597c9e7091d7", + "status": "verified" + }, "candidate_eligible": true, "case": { "path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", @@ -10,16 +35,37 @@ "SESSION_ATTACH", "LOAD_WORKSPACE", "PROGRAM_LOAD", + "POWER_ON", + "HOME_ALL", "PROGRAM_RUN", "PROGRAM_PAUSE", + "PROGRAM_RESUME", "PROGRAM_ABORT", "ESTOP", - "IO_RECOVER", + "ESTOP_RESET", + "POWER_ON", "SNAPSHOT_REQUEST" ], "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", "failure_samples": [], "gate_scenarios": { + "button_sequence": { + "off_blocks_motion": true, + "running_blocks_manual": true, + "running_blocks_power_off": true, + "sequence": [ + "ESTOP", + "ESTOP_RESET", + "POWER_ON", + "HOME", + "IDLE", + "RUNNING", + "PAUSED", + "RESUME", + "ABORT" + ], + "status": "verified" + }, "highlight_missing": { "reason": "axis_highlight_missing", "status": "verified" @@ -51,10 +97,10 @@ } }, "release_status": "STOP_RELEASE", - "report_sha256": "ae3ebd07146e75e1fa50c90d37e56ca195e2c6b82069718aedf9b63338ad2219", + "report_sha256": "fd24448f6f46d484a2e27f711383db6d0695feafb73c18365082975ed33c8f97", "schema": "xyzac-gate-matrix-rtcp-03.v1", "scope": "xyzac_only", - "state_hash": "fnv1a64:9abaa158693c6300", + "state_hash": "fnv1a64:7b5f8d0ed1c175dd", "status": "verified", "task_id": "RTCP-03", "tests": [ @@ -63,7 +109,16 @@ "node", "web/worker/test_xyzac_gate_matrix.mjs" ], - "output_sha256": "a8e61897c9e8f666970804f4ba8d0a959557960df40bf26261a7b4d73517de9a", + "output_sha256": "c0b5ad1449a1bc463f2c282a1b783092ffcadef9bf20b93a56eff285033d971c", + "returncode": 0, + "status": "verified" + }, + { + "command": [ + "node", + "web/worker/test_xyzac_button_constraints.mjs" + ], + "output_sha256": "2e105b80c6548ef53a2ebc656d6da39fe3bd7a5905f906188a3655e57e345b8e", "returncode": 0, "status": "verified" } diff --git a/coverage/xyzac-trt-hal-tool-alarm.report.json b/coverage/xyzac-trt-hal-tool-alarm.report.json index 144cd11..2d0b3c8 100644 --- a/coverage/xyzac-trt-hal-tool-alarm.report.json +++ b/coverage/xyzac-trt-hal-tool-alarm.report.json @@ -54,11 +54,11 @@ "vismach-clear" ], "release_status": "STOP_RELEASE", - "report_sha256": "75c368880a881da1230d3e66ec5265ad693053dc9eecce462d92c84f5a025872", + "report_sha256": "37026591b5875615e8a68374672fe7a7b433ef53504a3d52bc922a735f6fd140", "schema": "xyzac-hal-tool-alarm-rtcp-07.v1", "scope": "xyzac_only", "spindle_rpm": 1200, - "state_hash": "fnv1a64:184c713588cb828d", + "state_hash": "fnv1a64:198fc8da045d3e0f", "status": "verified", "task_id": "RTCP-07", "tests": [ @@ -67,7 +67,7 @@ "node", "web/worker/test_xyzac_hal_tool_alarm.mjs" ], - "output_sha256": "02c7e7eace9b8be9d3a3144f259fdfbf870569df26ba01c22b4146e1bc3bc9e2", + "output_sha256": "831953b1e6712c0187d8029facbc07cacf0a1290e17165019801a2000b8b4e40", "returncode": 0, "status": "verified" } diff --git a/coverage/xyzac-trt-manual-control.report.json b/coverage/xyzac-trt-manual-control.report.json index 487e6a0..52641c8 100644 --- a/coverage/xyzac-trt-manual-control.report.json +++ b/coverage/xyzac-trt-manual-control.report.json @@ -61,10 +61,10 @@ }, "mdi": "verified", "release_status": "STOP_RELEASE", - "report_sha256": "0660bca7d4ae6d68e584346606911c98aaacb14922f1b5fc9a7a44d782936be6", + "report_sha256": "a4ca2d5637e68c387e2ca5be460a2c9340d6b7198715cb1aa95d90055c623058", "schema": "xyzac-manual-control-rtcp-06.v1", "scope": "xyzac_only", - "state_hash": "fnv1a64:fac98a76f1a41fbc", + "state_hash": "fnv1a64:8e9acc7cf9c8cec1", "status": "verified", "task_id": "RTCP-06", "tests": [ @@ -73,7 +73,7 @@ "node", "web/worker/test_xyzac_manual_control.mjs" ], - "output_sha256": "930fe1e249389de9b0c0460f59c36e46820e3e006dc4675ffdf2528891731066", + "output_sha256": "c484daae09f135544f0a55376c79377e251e416651ab7d338e8d4d20f0d20768", "returncode": 0, "status": "verified" } diff --git a/coverage/xyzac-trt-program-preview.report.json b/coverage/xyzac-trt-program-preview.report.json index b3612e6..ef90933 100644 --- a/coverage/xyzac-trt-program-preview.report.json +++ b/coverage/xyzac-trt-program-preview.report.json @@ -37,12 +37,12 @@ "replay_same_operation": "verified" }, "release_status": "STOP_RELEASE", - "report_sha256": "bde1a054c9a633a62595ec19e5097d73499ca8fbcf07c817c01265dbf643f04a", + "report_sha256": "f44c782f09851b87a90e90583657a95c7ea30bffc5fd254fa8e3e16f87ed985e", "schema": "xyzac-program-preview-rtcp-04.v1", "scope": "xyzac_only", "segment_count": 2, "source_hash": "fnv1a64:b7f4678312207b31", - "state_hash": "fnv1a64:fda625a2e7b00ac7", + "state_hash": "fnv1a64:dadc751fbbb70835", "status": "verified", "task_id": "RTCP-04", "tests": [ @@ -51,7 +51,7 @@ "node", "web/worker/test_xyzac_program_preview.mjs" ], - "output_sha256": "79dd24d44e07958fd86484197c4502d3bdadd6d6971b7e74727b594d1d6b630f", + "output_sha256": "96e5c2c6203d26162a49206d5e9416b532bf880065a4a3418c863ea0c32bdb28", "returncode": 0, "status": "verified" } diff --git a/coverage/xyzac-trt-protocol-react.report.json b/coverage/xyzac-trt-protocol-react.report.json index f4f4be8..d8bd5d6 100644 --- a/coverage/xyzac-trt-protocol-react.report.json +++ b/coverage/xyzac-trt-protocol-react.report.json @@ -56,16 +56,18 @@ "failure_samples": [], "gates": { "controller": [ - "blocked", + "pending", + "result", "pending", "result" ], "invalid": "xyzac_kinematics_type_invalid", - "running": "xyzac_interp_not_idle" + "running": "xyzac_interp_running" }, "known_blockers": [], "operation_phases": [ - "blocked", + "pending", + "result", "pending", "result" ], @@ -80,13 +82,13 @@ }, { "path": "web/worker/test_xyzac_protocol_react.mjs", - "sha256": "69d942dfdcb7bc0d7fb26d6708220a47af13aa84acdcf490780980f6de0cc03d" + "sha256": "daa74b5091de4579b7ce5eb283b802e226e275bf51209af339e4cf6334fd6b5b" } ], "release_status": "STOP_RELEASE", - "report_sha256": "a8ee87b6cb3ccbd683e9888c85a063dabbe17fafdd2708636bd48be592b7b520", + "report_sha256": "f25c81c8aa94762e563bdfacceef0bf8b211ca632b2eed5556c88fd408b1c5b5", "schema": "xyzac-trt-protocol-react-report.v1", - "state_hash": "fnv1a64:2de42fa3ac15497a", + "state_hash": "fnv1a64:4cb77e59ba75a666", "status": "verified", "switchkins": [ { @@ -117,7 +119,7 @@ "node", "web/worker/test_xyzac_protocol_react.mjs" ], - "output_sha256": "cf72a973132e3e79c155fcb0feb58f07259c082970287127d786e94a92cc4327", + "output_sha256": "862f3c71bccb137c1551a5bfb4ed85014a7cbb2a2506f265193bbe29944c36a1", "returncode": 0 }, { diff --git a/coverage/xyzac-trt-recovery.report.json b/coverage/xyzac-trt-recovery.report.json index dd61c42..838c6fd 100644 --- a/coverage/xyzac-trt-recovery.report.json +++ b/coverage/xyzac-trt-recovery.report.json @@ -24,8 +24,8 @@ "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", "endpoint": { "at_checkpoint_equal": true, - "continuous_hash": "fnv1a64:a4dee4490d19b4ec", - "restored_hash": "fnv1a64:ca9e3481aaa28f6d" + "continuous_hash": "fnv1a64:b105b6fc1bcc8a70", + "restored_hash": "fnv1a64:857caa278d03c259" }, "exclusions": [ "xyzbc", @@ -61,11 +61,11 @@ "outputs": [ { "path": "web/worker/test_xyzac_recovery.mjs", - "sha256": "d8f02b0ab5b7c9fc5ef30a3a67f7896071ecf9bbaa7d2f3614c031d118e655e7" + "sha256": "d25dc8f5de748b37ca231ee7e5a0df049878511f4c0fec98061208730f57b145" }, { "path": "web/worker/sim-session.mjs", - "sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3" + "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1" }, { "path": "web/worker/wasm-program-runtime.mjs", @@ -73,7 +73,7 @@ } ], "release_status": "STOP_RELEASE", - "report_sha256": "6c38c4e541868934f7a8231d36314b7e736daeb6d685f1dad48809fa49ef65ae", + "report_sha256": "be2d59e86990e26858d11e3220cfa63d4b8f5d84f8b81f1067d89b645f78a9ae", "required_checkpoint_fields": [ "context_id", "simulation_tick", @@ -95,7 +95,7 @@ "node", "web/worker/test_xyzac_recovery.mjs" ], - "output_sha256": "d13944363157b9b97a440adb6135ec8977332f31778fb7ae40383e52ba2f1f35", + "output_sha256": "3649f6971bb8ed8857dd5cab886834485a3d82c8dec71365853481c0f12a46c9", "returncode": 0 } ] diff --git a/coverage/xyzac-trt-source-slice-inventory.report.json b/coverage/xyzac-trt-source-slice-inventory.report.json index b8b7e08..cac1176 100644 --- a/coverage/xyzac-trt-source-slice-inventory.report.json +++ b/coverage/xyzac-trt-source-slice-inventory.report.json @@ -1160,6 +1160,10 @@ "command": "PROGRAM_ABORT", "slice_id": "AXIS-XYZAC-PROGRAM-MDI" }, + { + "command": "PROGRAM_RESTART", + "slice_id": "AXIS-XYZAC-PROGRAM-MDI" + }, { "command": "MDI_EXECUTE", "slice_id": "AXIS-XYZAC-PROGRAM-MDI" @@ -1176,6 +1180,18 @@ "command": "HOME", "slice_id": "AXIS-XYZAC-JOG-HOME" }, + { + "command": "POWER", + "slice_id": "AXIS-XYZAC-SHELL-MENU" + }, + { + "command": "ESTOP", + "slice_id": "AXIS-XYZAC-SHELL-MENU" + }, + { + "command": "RESET", + "slice_id": "AXIS-XYZAC-SHELL-MENU" + }, { "command": "M428", "slice_id": "AXIS-XYZAC-M428-M429-M430" @@ -1772,7 +1788,7 @@ } ], "release_status": "STOP_RELEASE", - "report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e", + "report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f", "runtime_evidence": { "native": { "command": [ @@ -2622,6 +2638,22 @@ "slice_id": "AXIS-XYZAC-PROGRAM-MDI", "state": "program.current_line" }, + { + "slice_id": "AXIS-XYZAC-SHELL-MENU", + "state": "axis_state.machine_state" + }, + { + "slice_id": "AXIS-XYZAC-SHELL-MENU", + "state": "axis_state.operation_state" + }, + { + "slice_id": "AXIS-XYZAC-SHELL-MENU", + "state": "device_io.state.estop" + }, + { + "slice_id": "AXIS-XYZAC-SHELL-MENU", + "state": "device_io.state.ready" + }, { "slice_id": "AXIS-XYZAC-KINEMATICS", "state": "motion.joints" diff --git a/coverage/xyzac-trt-tool-preview.report.json b/coverage/xyzac-trt-tool-preview.report.json index bfc9fe2..8a08a4a 100644 --- a/coverage/xyzac-trt-tool-preview.report.json +++ b/coverage/xyzac-trt-tool-preview.report.json @@ -74,7 +74,7 @@ }, { "path": "web/worker/sim-session.mjs", - "sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3" + "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1" }, { "path": "web/worker/test_xyzac_exact_preview.mjs", @@ -90,7 +90,7 @@ }, { "path": "web/worker/wasm-xyzac-io.mjs", - "sha256": "fdb92e8819e38010aa7a1b8f5f58473a930644fbfeba3d7d5a5c23e075ee92ca" + "sha256": "692dfb4c869130fd4a0ee78a500a54817e326991f2a5ac23a91059136d41eabd" }, { "path": "web/worker/wasm-xyzac-device-io.mjs", @@ -98,11 +98,11 @@ }, { "path": "web/worker/test_xyzac_machine_sequence.mjs", - "sha256": "9dcd89d54596fe4b339d5aca4e028a0c44e173a8e646f1254defc706bd3fcc96" + "sha256": "386fa1e72f194ba0c93ad0083474da48c8f526b627bd22ef398e3bf5b1ad1552" }, { "path": "web/react/src/App.tsx", - "sha256": "8cbec29f00c44e18a01ac92734e4f8b64764cc04db4cec48ce9c28693bd2bbef" + "sha256": "31692dd2df5d9a1a26503e78627c5d10524426c975d8ffb0dfbd3f7cf5c0f8af" }, { "path": "web/react/src/styles.css", @@ -289,7 +289,7 @@ "schema": "xyzac-exact-tool-preview.v1", "segment_count": 29, "source_hash": "fnv1a64:522e219683194ad2", - "state_hash": "fnv1a64:c7e53444cce4a142", + "state_hash": "fnv1a64:1b46405fa36a5abe", "status": "verified", "visual_segments": 29 }, @@ -344,14 +344,13 @@ "schema": "xyzac-machine-sequence.v1", "sequence": [ "ESTOP", - "ESTOP_RELEASE", - "IO_RECOVER", + "ESTOP_RESET", "POWER", "HOME_ALL", "PROGRAM_RUN", "SIM_STEP" ], - "state_hash": "fnv1a64:c8f6326c97e651f9", + "state_hash": "fnv1a64:3fe83d3f4768868c", "status": "verified" }, "static": { @@ -384,7 +383,7 @@ } }, "release_status": "STOP_RELEASE", - "report_sha256": "94b18227ec72689934629a5b428cf6a9f16500d30b200d4b36a10d03c84c56b9", + "report_sha256": "0127822355f55c04898dcb0f9333d291d5fa8fbb116d7199cbdf02694b97fe87", "schema": "xyzac-trt-tool-preview-report.v1", "status": "verified", "task_id": "RTCP-10-TOOL-PREVIEW", @@ -403,7 +402,7 @@ "node", "web/worker/test_xyzac_exact_preview.mjs" ], - "output_sha256": "60cec025efd2f29de45fb6139f0929f329a505bd331212e6b01ab5fbb5f8fca8", + "output_sha256": "625aaa1f95578666d70390a782745ff1e74862f6ef11bbf2911b1870a2b864dd", "returncode": 0 }, { @@ -411,7 +410,7 @@ "node", "web/worker/test_xyzac_machine_sequence.mjs" ], - "output_sha256": "2a72e723603080bb3eff8169b8be5352fa46d6b0c8ae04dbeeb2e5e1371e1c47", + "output_sha256": "a361d9e4f695d048093e7ff4b7b5e6688534071c5b3811a40bbe69e00d114133", "returncode": 0 } ], diff --git a/coverage/xyzac-trt-trace-parity.report.json b/coverage/xyzac-trt-trace-parity.report.json index 8d55065..47d18b9 100644 --- a/coverage/xyzac-trt-trace-parity.report.json +++ b/coverage/xyzac-trt-trace-parity.report.json @@ -46,7 +46,7 @@ }, { "path": "web/worker/test_xyzac_trace_parity.mjs", - "sha256": "f5c6a7cd3e8b7f72bb1b25b5c368c0e52d494b9c811022d34435e668612ec4ce" + "sha256": "209254396d3ec828936ec5c2ad34de9044706f7d0309fd5ad4cacae347f15247" }, { "path": "coverage/xyzac-trt-rtcp.report.json", @@ -130,7 +130,7 @@ "mutation": "state[1].active_type=99" }, "release_status": "STOP_RELEASE", - "report_sha256": "39c63aa9af38fd98e49fd68ec2401ee8715fcafef98d47c070a47ab6f6dc0a48", + "report_sha256": "234a362308c9caae71955a606d1779f45ce4fe170f476e49a6c5a482dd4351d5", "schema": "xyzac-trt-trace-parity-report.v1", "status": "verified", "task_id": "PARITY-00", diff --git a/coverage/xyzac-trt-wasm-authority-audit.report.json b/coverage/xyzac-trt-wasm-authority-audit.report.json index 113b02c..19caecf 100644 --- a/coverage/xyzac-trt-wasm-authority-audit.report.json +++ b/coverage/xyzac-trt-wasm-authority-audit.report.json @@ -65,7 +65,7 @@ "case": { "path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", "sha256": "f985970db293b70c055e4c41fc316d0c16037b5dc7ce07490e18f554e851f811", - "source_slice_report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e" + "source_slice_report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f" }, "checks": { "context_match": { @@ -94,7 +94,7 @@ "status": "verified" }, "source_slice_dependency": { - "report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e", + "report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f", "status": "verified" }, "wasm_worker_bootstrap": { @@ -128,10 +128,10 @@ "source_slice": "AXIS-XYZAC-PROGRAM-MDI", "sources": [ { - "lines": "107-739", + "lines": "107-833", "path": "web/worker/sim-session.mjs", "role": "session-authority-routing", - "sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3", + "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1", "status": "resolved", "symbol": "new ProgramRunner" }, @@ -161,10 +161,10 @@ "symbol": "export class MotionState" }, { - "lines": "107-739", + "lines": "107-833", "path": "web/worker/sim-session.mjs", "role": "session-authority-routing", - "sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3", + "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1", "status": "resolved", "symbol": "new ProgramRunner" } @@ -186,10 +186,10 @@ "symbol": "export class AuthoritativeRuntime" }, { - "lines": "107-739", + "lines": "107-833", "path": "web/worker/sim-session.mjs", "role": "session-authority-routing", - "sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3", + "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1", "status": "resolved", "symbol": "new ProgramRunner" } @@ -205,7 +205,7 @@ "recovery": "Chromium SIGKILL/profile-reopen, corruption detection and Firefox OPFS round-trip are verified" }, "release_status": "STOP_RELEASE", - "report_sha256": "f73755bf9d695ee02de03bae8ab65cfc55e593b2758634420e83b6d3bef227e2", + "report_sha256": "6bf29284946b658ca58c76c91b2dbad3c33f8af7afb74b91aecb5a2842f3b650", "scans": { "js_gcode_matches": [ { @@ -274,12 +274,12 @@ "text": "const result = call(this.core, \"sim_interpreter_execute\", \"number\", [\"number\", \"number\", \"number\"], [this.interpreter, this.blockBuffer, bytes.length]);" }, { - "line": 1405, + "line": 1526, "path": "web/worker/sim-session.mjs", "text": "const result = this.xyzacProgram.executeBlock(block);" }, { - "line": 1424, + "line": 1545, "path": "web/worker/sim-session.mjs", "text": "const result = this.program.executeBlock(block);" }, @@ -292,7 +292,7 @@ "opfs_writer_matches": [ { "line": 1, - "path": "web/react/dist/assets/sim-worker-entry-DPpmZmXb.js", + "path": "web/react/dist/assets/sim-worker-entry-BiqKYM9j.js", "text": "(function(){var e=(()=>{var e=self.location.href;return(function(t){t||={};var t=t===void 0?{}:t,n,r;t.ready=new Promise(function(e,t){n=e,r=t});var i=Object.assign({},t),a=[],o=typeof window==`object`,s=typeof importScripts==`function`;typ" }, { @@ -1496,7 +1496,7 @@ "files": [ { "path": "web/react/src/App.tsx", - "sha256": "8cbec29f00c44e18a01ac92734e4f8b64764cc04db4cec48ce9c28693bd2bbef" + "sha256": "31692dd2df5d9a1a26503e78627c5d10524426c975d8ffb0dfbd3f7cf5c0f8af" }, { "path": "web/react/src/ThreeMachineView.tsx", @@ -1508,7 +1508,7 @@ }, { "path": "web/react/src/XyzacToolPreview.tsx", - "sha256": "d29cd1355d7dd178dce01e49eb321425b0b08d2291a6eaa00c86b546d8e3bba1" + "sha256": "caca009f96336847c8d9b8d5823c3d7de86ed77c37cae2fe9f5f3e4d30132d57" }, { "path": "web/react/src/XyzacTrtPanel.tsx", @@ -1524,7 +1524,7 @@ }, { "path": "web/react/src/legacy.d.ts", - "sha256": "a29f18bf009c955a5463a58856d4ab9902fc5ce032d643f5363ca3bf28247174" + "sha256": "b67ced36978375d312d5b10a26443e50c768e58769913b1b5b4887538c4f22f3" }, { "path": "web/react/src/main.tsx", @@ -1576,7 +1576,7 @@ "react_opfs_matches": [], "react_wasm_matches": [], "worker": { - "file_count": 124, + "file_count": 125, "files": [ { "path": "web/worker/authoritative-runtime.mjs", @@ -1584,11 +1584,11 @@ }, { "path": "web/worker/axis-state.mjs", - "sha256": "01bbbcd053262f43413fe76a540b0c453e0a2c3e704ea44ec8b30ce50bb66b83" + "sha256": "77c494b2327dcae26b74af4873c18d5a68aef2e5b1fa500ef2b4b0a523a76c37" }, { "path": "web/worker/capabilities.mjs", - "sha256": "39b5af3627e068d7cc68fc0bcabfdc33c79f4f68b41e4488481ef5c4791d9993" + "sha256": "88551cc6940efea9186fb86d9cf68ee66f45538b3667730c8e9c514202ec035b" }, { "path": "web/worker/contracts.mjs", @@ -1652,7 +1652,7 @@ }, { "path": "web/worker/sim-session.mjs", - "sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3" + "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1" }, { "path": "web/worker/sim-task.mjs", @@ -1966,25 +1966,29 @@ "path": "web/worker/test_workspace_session_integration.mjs", "sha256": "38f49df83c3ac24ef564c8cd5ed69b1a5882fa47c464946cb1945cac8ac163bf" }, + { + "path": "web/worker/test_xyzac_button_constraints.mjs", + "sha256": "e5206c37f959d0214f83892dcd2b57abf6d16335ac1f6eedb36e9bbad5a45d5f" + }, { "path": "web/worker/test_xyzac_exact_preview.mjs", "sha256": "dc5cc312aab46003e12a4bfc26d695b05d78843e61808c550cb4a5925338687b" }, { "path": "web/worker/test_xyzac_gate_matrix.mjs", - "sha256": "07e4f05f3b7bf470fb87ecea077587e7584928d28fe7e28932f8a649c960525d" + "sha256": "176da9a86c17cef2fb1f17bb65c5854af107524ac26871fa830e17e62fc022cf" }, { "path": "web/worker/test_xyzac_hal_tool_alarm.mjs", - "sha256": "731ff64fc5b9abd70c6ab6649a2644e51e363bda710a0d4ec9faeffa3bb77db3" + "sha256": "8da3024cf0a8fc29f77c481204bce0ed3672cf411a5b52e553cb60c92213b0aa" }, { "path": "web/worker/test_xyzac_machine_sequence.mjs", - "sha256": "9dcd89d54596fe4b339d5aca4e028a0c44e173a8e646f1254defc706bd3fcc96" + "sha256": "386fa1e72f194ba0c93ad0083474da48c8f526b627bd22ef398e3bf5b1ad1552" }, { "path": "web/worker/test_xyzac_manual_control.mjs", - "sha256": "0f6320b0e08e73830c96b502c032c5720453973452c467fd08657e28b657ff3c" + "sha256": "6219a55c4c1470193f984d94fea528e697479903e2623abf57a4d5b5274fd797" }, { "path": "web/worker/test_xyzac_program_preview.mjs", @@ -1992,15 +1996,15 @@ }, { "path": "web/worker/test_xyzac_protocol_react.mjs", - "sha256": "69d942dfdcb7bc0d7fb26d6708220a47af13aa84acdcf490780980f6de0cc03d" + "sha256": "daa74b5091de4579b7ce5eb283b802e226e275bf51209af339e4cf6334fd6b5b" }, { "path": "web/worker/test_xyzac_recovery.mjs", - "sha256": "d8f02b0ab5b7c9fc5ef30a3a67f7896071ecf9bbaa7d2f3614c031d118e655e7" + "sha256": "d25dc8f5de748b37ca231ee7e5a0df049878511f4c0fec98061208730f57b145" }, { "path": "web/worker/test_xyzac_rtcp.mjs", - "sha256": "9bc14997e56a8d0f6f0043b3a92fa5efe0fc8b3fa9c973bffa9045446973208f" + "sha256": "7e8be744ba11def666f3cca70252d9ba64f6801d6436f624e4b8c81738a46ec5" }, { "path": "web/worker/test_xyzac_session.mjs", @@ -2012,23 +2016,23 @@ }, { "path": "web/worker/test_xyzac_trace_parity.mjs", - "sha256": "f5c6a7cd3e8b7f72bb1b25b5c368c0e52d494b9c811022d34435e668612ec4ce" + "sha256": "209254396d3ec828936ec5c2ad34de9044706f7d0309fd5ad4cacae347f15247" }, { "path": "web/worker/test_xyzac_trt_case.mjs", - "sha256": "abdf50fba4103a7dabe13495cb62bc65019a152de2d91ed01150b968cdf7c147" + "sha256": "e1ab517d11a70e0682453c7fa82f9d1bcda8de0c7fd8547e6db24ef03fe4ad16" }, { "path": "web/worker/test_xyzac_trt_io_authority.mjs", - "sha256": "5be19961fe2c810e5a5ab17566febb8e024810c7bf3a492572728a085305ad96" + "sha256": "d027080177a54ce2132f9023dc844b140acd1ed0d354c534733398154a30d72f" }, { "path": "web/worker/test_xyzac_trt_wasm_program_authority.mjs", - "sha256": "29a3babd0c0a31de7bd8a81748de484a698ebf7153bfb33822ee912049b3eafc" + "sha256": "52543ae1a9f9ced7b2cb7666ed80490f65f5cda0494d8fbedfc664f303a85627" }, { "path": "web/worker/test_xyzac_workflow.mjs", - "sha256": "6c738c6993d43b4ce16feecfe87d2b507824a88b06ead2f0659db6ca25c346b5" + "sha256": "4dc92c0185bb6f1c4c4f925bc6e5e6800acd0c1ee5c3c4c194776fda03d656d9" }, { "path": "web/worker/trace-export.mjs", @@ -2056,7 +2060,7 @@ }, { "path": "web/worker/wasm-xyzac-io.mjs", - "sha256": "fdb92e8819e38010aa7a1b8f5f58473a930644fbfeba3d7d5a5c23e075ee92ca" + "sha256": "692dfb4c869130fd4a0ee78a500a54817e326991f2a5ac23a91059136d41eabd" }, { "path": "web/worker/wasm-xyzac-kinematics.mjs", @@ -2082,10 +2086,10 @@ "scope": "selected_xyzac_simulation_only", "sources": [ { - "lines": "107-739", + "lines": "107-833", "path": "web/worker/sim-session.mjs", "role": "session-authority-routing", - "sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3", + "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1", "status": "resolved", "symbol": "new ProgramRunner" }, @@ -2189,7 +2193,7 @@ "lines": "39-39", "path": "web/worker/wasm-xyzac-io.mjs", "role": "wasm-hal-io-tool-probe-authority", - "sha256": "fdb92e8819e38010aa7a1b8f5f58473a930644fbfeba3d7d5a5c23e075ee92ca", + "sha256": "692dfb4c869130fd4a0ee78a500a54817e326991f2a5ac23a91059136d41eabd", "status": "resolved", "symbol": "export class WasmXyzacIo" }, @@ -2203,9 +2207,9 @@ } ], "state_hashes": [ - "fnv1a64:b78114f0dff414b9", - "fnv1a64:b312225bf059254c", - "fnv1a64:c927914a74c8c3c2" + "fnv1a64:8bb613bfa975ae2e", + "fnv1a64:051e2337103b3b9b", + "fnv1a64:7aaf47cbc3c37b54" ], "status": "verified", "task_id": "WASM-00", @@ -2223,7 +2227,7 @@ "control_count": 110, "first_divergent_tick": null, "motion_authority": "wasm", - "output_sha256": "16b23b8d13afdc46f06ef8f9dd14c4bd93cc7511b4334efbcebe08120ddb4858", + "output_sha256": "de69f1a6f2f608f9d156713e50addd1677e17957b760261de7e0092a4e031a82", "returncode": 0, "state_hash": "fnv1a64:e862c9e59d9ffd8c", "status": "verified", @@ -2231,19 +2235,19 @@ { "active_type": 0, "code": "M429", - "state_hash": "fnv1a64:b78114f0dff414b9", + "state_hash": "fnv1a64:8bb613bfa975ae2e", "tick": 0 }, { "active_type": 1, "code": "M428", - "state_hash": "fnv1a64:b312225bf059254c", + "state_hash": "fnv1a64:051e2337103b3b9b", "tick": 0 }, { "active_type": 2, "code": "M430", - "state_hash": "fnv1a64:c927914a74c8c3c2", + "state_hash": "fnv1a64:7aaf47cbc3c37b54", "tick": 0 } ], diff --git a/coverage/xyzac-trt-wasm-only-release-gate.report.json b/coverage/xyzac-trt-wasm-only-release-gate.report.json index eb745a2..51e38c4 100644 --- a/coverage/xyzac-trt-wasm-only-release-gate.report.json +++ b/coverage/xyzac-trt-wasm-only-release-gate.report.json @@ -57,8 +57,8 @@ }, "recovery": { "at_checkpoint_equal": true, - "continuous_hash": "fnv1a64:a4dee4490d19b4ec", - "restored_hash": "fnv1a64:ca9e3481aaa28f6d", + "continuous_hash": "fnv1a64:b105b6fc1bcc8a70", + "restored_hash": "fnv1a64:857caa278d03c259", "status": "verified" }, "scope_exclusions": { @@ -96,7 +96,7 @@ "failure_samples": [], "input": { "path": "coverage/xyzac-trt-web-wasm-parity.report.json", - "sha256": "d8bd04725525c1c5881d749466c8793f4b771e34ba260c5d35cb1d29f889d4fc" + "sha256": "7f88767d4b0b3b70bde27e8d5165a901618dfa2fb5076eceeb117f443bd305df" }, "known_blockers": [ "STOP_RELEASE is intentional until an owner authorizes scope expansion" @@ -104,7 +104,7 @@ "outputs": [], "release_decision": "selected_case_verified_but_release_stopped_by_scope_exclusions", "release_status": "STOP_RELEASE", - "report_sha256": "70dc5704e2530f9a42d6f5985f242be4fc4b691a2272fd15b9fa0bd8031a0728", + "report_sha256": "712fa17edbff4c78aa58700637b54b4974b3a2c0104f4c91de566d4ff27ac5c4", "schema": "xyzac-trt-wasm-only-release-gate.v1", "status": "verified", "task_id": "RELEASE-00" diff --git a/coverage/xyzac-trt-web-wasm-parity.report.json b/coverage/xyzac-trt-web-wasm-parity.report.json index 79dc2e5..4e7c4cc 100644 --- a/coverage/xyzac-trt-web-wasm-parity.report.json +++ b/coverage/xyzac-trt-web-wasm-parity.report.json @@ -86,27 +86,27 @@ }, "recovery": { "at_checkpoint_equal": true, - "continuous_hash": "fnv1a64:a4dee4490d19b4ec", - "restored_hash": "fnv1a64:ca9e3481aaa28f6d" + "continuous_hash": "fnv1a64:b105b6fc1bcc8a70", + "restored_hash": "fnv1a64:857caa278d03c259" }, "release_status": "STOP_RELEASE", "report_hashes": { - "coverage/xyzac-trt-gate-matrix.report.json": "495f452f59848114bb885561e26e7080f356650cc1b620cfddf515685fbf1628", - "coverage/xyzac-trt-hal-tool-alarm.report.json": "2abcfd59bf16e596c9183296061ff66ce00f6d9b6efee121519a499007001216", - "coverage/xyzac-trt-manual-control.report.json": "b7333980194ff9b6344d34ec97a181fbb79b5730184dd13a6d1a9067c9212af9", - "coverage/xyzac-trt-program-preview.report.json": "ee41e2b4ac97f719817e21eee2bd3d503fc78c783ec1bc185466b3c56fc62045", - "coverage/xyzac-trt-protocol-react.report.json": "bc25315930095ca096bae0f97047c41d45bf9ff83373ac7a73f52273b33d9237", - "coverage/xyzac-trt-recovery.report.json": "deff32f2998930dfcd9373ca0a6a960d1fa7e5732ef785cc7cacf336b9dc3459", + "coverage/xyzac-trt-gate-matrix.report.json": "c3a5bc3eabd6631a8645a28df997ef605f911230c55527dbda016edf5a37ee6f", + "coverage/xyzac-trt-hal-tool-alarm.report.json": "99ac60c9a2d58f08063ffc07f0c2eea62d8ec97212d6d2b74d6c139f176b592e", + "coverage/xyzac-trt-manual-control.report.json": "9ebb8ef2c97a57d77666c92e210ad1415a63852730b37993f995ca95cbc275b5", + "coverage/xyzac-trt-program-preview.report.json": "b95a59986c0bd0cb098c49fbc8c9ced80bbe024888dcbcc14c15dd6e67432814", + "coverage/xyzac-trt-protocol-react.report.json": "1179b708f9f3185ea7cbd66f97aed984cfc22bcc4f35ea3d30293183e9bce011", + "coverage/xyzac-trt-recovery.report.json": "351b3d5f51a6ef38f34c5e42f318a3b565618ebecf122f5d21d83ef8a54f2b4e", "coverage/xyzac-trt-rtcp.report.json": "b1562d6d2c9cd5144c7c8c9b334bd8bf67919ea7297f103ec67cb32773608d08", - "coverage/xyzac-trt-source-slice-inventory.report.json": "20382f2bcd13fdaf3f25521b7a8c42e83e0409f75092c643b4c79a7613fdf287", + "coverage/xyzac-trt-source-slice-inventory.report.json": "bf5f3fd36e1943e0dc42bafb2ed195fad147bcb3e56a8d208d7f01a3641829de", "coverage/xyzac-trt-startup.report.json": "56e040b645bcefc3d6c57244cb2d3aba16f421ff5137089d9c3336970dfd968b", - "coverage/xyzac-trt-threejs.report.json": "2791930c38a48ab9fa61f284e57483708787e89b558c75ddc9d99e4b3e76ad28", - "coverage/xyzac-trt-tool-preview.report.json": "4a5b286b0a13324a1e773669ef34ab9a64fd0376dfaf5adf39a95d9b420bd965", - "coverage/xyzac-trt-trace-parity.report.json": "ef914bb7823b9cdb0f6e15ca409515659d5575d09cdc6da2c458cd81fafa595a", - "coverage/xyzac-trt-wasm-authority-audit.report.json": "e7c84d3f36db280c13c1ab8a29f4f827b4257eb34b8cb2bbc47357a920001692", - "coverage/xyzac-trt-workflow.report.json": "ede73fab04f3cc3e64a13fbe0ab10603253e9a01b4357feff1f3f54313777e75" + "coverage/xyzac-trt-threejs.report.json": "5172d7118c555fca5f92bcb443b9c259ea0225346482558d4c33ef8a4f8da5cb", + "coverage/xyzac-trt-tool-preview.report.json": "bc39dec83f71f9e164faed098d281ca1b2390b34b225fb4fa81ebf10bc860ee8", + "coverage/xyzac-trt-trace-parity.report.json": "73b2fd46c66d0c5910c746db9d8892e0ea713714576cf793f510bee35e9a7c00", + "coverage/xyzac-trt-wasm-authority-audit.report.json": "a970d9980b99d8b64315088e3f182d9d99aad3787831e28feb6127c9e854c6e8", + "coverage/xyzac-trt-workflow.report.json": "acbb6dcfdff385dc839ddb94724f9fdcebc391ccde995881ba6be14dd5a6b5c4" }, - "report_sha256": "bf2f04c1798cd43016833836bab8c789ca71699ae414746601ae0efed010666d", + "report_sha256": "09fa8fc47cd34237f97b7b74707cd57721874960809f627d1d49fdc0319c627a", "schema": "xyzac-trt-web-wasm-parity-report.v1", "source_coverage": { "axis_keyboard": 10, diff --git a/coverage/xyzac-trt-workflow.report.json b/coverage/xyzac-trt-workflow.report.json index 0c7a979..c414296 100644 --- a/coverage/xyzac-trt-workflow.report.json +++ b/coverage/xyzac-trt-workflow.report.json @@ -28,12 +28,12 @@ "pause_cursor_stable": true, "recovery_status": "recovered", "release_status": "STOP_RELEASE", - "report_sha256": "d98a16e6582c0bc9fdaa410da64be369cc21d56a61a04b43436d4900c12461b0", + "report_sha256": "beaae5ac83afda17ceae23a7b999ae79154035ddf29e20b8c96707d7ccf4d808", "restart_source_hash_preserved": true, "run_from_line": 4, "schema": "xyzac-workflow-rtcp-05.v1", "scope": "xyzac_only", - "state_hash": "fnv1a64:285d97dde7d56ea7", + "state_hash": "fnv1a64:cb67e9edef5a4bc8", "status": "verified", "task_id": "RTCP-05", "tests": [ @@ -42,7 +42,7 @@ "node", "web/worker/test_xyzac_workflow.mjs" ], - "output_sha256": "197bc634f7f9ddf290b1a64d3cbd1f52d257dcc4982e5de3bd16f6358c70702b", + "output_sha256": "13e39016ef9db1817514bb70f6c26ca5116f3fd84c30e78887fd93e5fcad2cc6", "returncode": 0, "status": "verified" } diff --git a/scripts/generate_xyzac_trt_gate_matrix_report.py b/scripts/generate_xyzac_trt_gate_matrix_report.py index 37eb998..02e0384 100644 --- a/scripts/generate_xyzac_trt_gate_matrix_report.py +++ b/scripts/generate_xyzac_trt_gate_matrix_report.py @@ -21,13 +21,21 @@ def canonical(value: Any) -> str: def main() -> int: command = ["node", "web/worker/test_xyzac_gate_matrix.mjs"] + button_command = ["node", "web/worker/test_xyzac_button_constraints.mjs"] result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False) + button_result = subprocess.run(button_command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False) sample = None for line in reversed(result.stdout.splitlines()): try: value = json.loads(line) except json.JSONDecodeError: continue if isinstance(value, dict) and value.get("schema") == "xyzac-gate-matrix-rtcp-03.v1": sample = value; break status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked" + button_sample = None + for line in reversed(button_result.stdout.splitlines()): + try: value = json.loads(line) + except json.JSONDecodeError: continue + if isinstance(value, dict) and value.get("schema") == "xyzac-button-constraints.v1": button_sample = value; break + status = "verified" if status == "verified" and button_result.returncode == 0 and button_sample and button_sample.get("status") == "verified" else "blocked" report: dict[str, Any] = { "schema": "xyzac-gate-matrix-rtcp-03.v1", "task_id": "RTCP-03", @@ -37,7 +45,7 @@ def main() -> int: "status": status, "candidate_eligible": status == "verified", "authority": "linuxcnc-wasm-worker", - "command_trace": ["HELLO", "SESSION_ATTACH", "LOAD_WORKSPACE", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_ABORT", "ESTOP", "IO_RECOVER", "SNAPSHOT_REQUEST"], + "command_trace": ["HELLO", "SESSION_ATTACH", "LOAD_WORKSPACE", "PROGRAM_LOAD", "POWER_ON", "HOME_ALL", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "ESTOP", "ESTOP_RESET", "POWER_ON", "SNAPSHOT_REQUEST"], "tick_trace": [0, 0, 0], "state_hash": sample.get("final_state_hash") if sample else None, "gate_scenarios": { @@ -47,9 +55,11 @@ def main() -> int: "switchkins_estop": {"status": "verified", "reason": "xyzac_estop_active"}, "running_pause_abort": {"status": "verified", "pause": True, "abort": True}, "paused_resume": {"status": "verified", "resume": True}, + "button_sequence": {"status": "verified", "sequence": ["ESTOP", "ESTOP_RESET", "POWER_ON", "HOME", "IDLE", "RUNNING", "PAUSED", "RESUME", "ABORT"], "off_blocks_motion": True, "running_blocks_manual": True, "running_blocks_power_off": True}, "vismach_clear": {"status": "verified", "visual_only": True}, }, - "tests": [{"command": command, "returncode": result.returncode, "status": status, "output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest()}], + "button_constraints": button_sample or {}, + "tests": [{"command": command, "returncode": result.returncode, "status": status, "output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest()}, {"command": button_command, "returncode": button_result.returncode, "status": status, "output_sha256": hashlib.sha256(button_result.stdout.encode()).hexdigest()}], "failure_samples": [] if status == "verified" else [{"code": "BLOCKED_GATE_MATRIX", "detail": "XYZAC gate matrix test failed"}], "release_status": "STOP_RELEASE", "report_sha256": None, diff --git a/scripts/generate_xyzac_trt_source_slices.py b/scripts/generate_xyzac_trt_source_slices.py index 3fd7852..6f45dc7 100644 --- a/scripts/generate_xyzac_trt_source_slices.py +++ b/scripts/generate_xyzac_trt_source_slices.py @@ -152,9 +152,9 @@ def build() -> dict[str, Any]: source_hashes = {row["path"]: row["sha256"] for row in sources} unresolved = [row for row in resources if row["status"].startswith("BLOCKED")] command_contract = [{"command": command, "slice_id": slice_id} for command, slice_id in [ - ("PROGRAM_LOAD", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_RUN", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_PAUSE", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_RESUME", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_ABORT", "AXIS-XYZAC-PROGRAM-MDI"), ("MDI_EXECUTE", "AXIS-XYZAC-PROGRAM-MDI"), ("JOG_START", "AXIS-XYZAC-JOG-HOME"), ("JOG_STOP", "AXIS-XYZAC-JOG-HOME"), ("HOME", "AXIS-XYZAC-JOG-HOME"), ("M428", "AXIS-XYZAC-M428-M429-M430"), ("M429", "AXIS-XYZAC-M428-M429-M430"), ("M430", "AXIS-XYZAC-M428-M429-M430"), ("SIM_STEP", "AXIS-XYZAC-TASK-TP-MOTION"), ("CHECKPOINT", "AXIS-XYZAC-ERROR-RECOVERY"), ("RECOVER", "AXIS-XYZAC-ERROR-RECOVERY")]] + ("PROGRAM_LOAD", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_RUN", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_PAUSE", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_RESUME", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_ABORT", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_RESTART", "AXIS-XYZAC-PROGRAM-MDI"), ("MDI_EXECUTE", "AXIS-XYZAC-PROGRAM-MDI"), ("JOG_START", "AXIS-XYZAC-JOG-HOME"), ("JOG_STOP", "AXIS-XYZAC-JOG-HOME"), ("HOME", "AXIS-XYZAC-JOG-HOME"), ("POWER", "AXIS-XYZAC-SHELL-MENU"), ("ESTOP", "AXIS-XYZAC-SHELL-MENU"), ("RESET", "AXIS-XYZAC-SHELL-MENU"), ("M428", "AXIS-XYZAC-M428-M429-M430"), ("M429", "AXIS-XYZAC-M428-M429-M430"), ("M430", "AXIS-XYZAC-M428-M429-M430"), ("SIM_STEP", "AXIS-XYZAC-TASK-TP-MOTION"), ("CHECKPOINT", "AXIS-XYZAC-ERROR-RECOVERY"), ("RECOVER", "AXIS-XYZAC-ERROR-RECOVERY")]] state_contract = [{"state": state, "slice_id": slice_id} for state, slice_id in [ - ("task.state", "AXIS-XYZAC-TASK-TP-MOTION"), ("task.mode", "AXIS-XYZAC-TASK-TP-MOTION"), ("task.interp_state", "AXIS-XYZAC-PROGRAM-MDI"), ("program.current_line", "AXIS-XYZAC-PROGRAM-MDI"), ("motion.joints", "AXIS-XYZAC-KINEMATICS"), ("kinematics.active_type", "AXIS-XYZAC-M428-M429-M430"), ("motion.switchkins-type", "AXIS-XYZAC-PYVCP-SWITCHKINS"), ("pose", "AXIS-XYZAC-KINEMATICS"), ("state_hash", "AXIS-XYZAC-ERROR-RECOVERY")]] + ("task.state", "AXIS-XYZAC-TASK-TP-MOTION"), ("task.mode", "AXIS-XYZAC-TASK-TP-MOTION"), ("task.interp_state", "AXIS-XYZAC-PROGRAM-MDI"), ("program.current_line", "AXIS-XYZAC-PROGRAM-MDI"), ("axis_state.machine_state", "AXIS-XYZAC-SHELL-MENU"), ("axis_state.operation_state", "AXIS-XYZAC-SHELL-MENU"), ("device_io.state.estop", "AXIS-XYZAC-SHELL-MENU"), ("device_io.state.ready", "AXIS-XYZAC-SHELL-MENU"), ("motion.joints", "AXIS-XYZAC-KINEMATICS"), ("kinematics.active_type", "AXIS-XYZAC-M428-M429-M430"), ("motion.switchkins-type", "AXIS-XYZAC-PYVCP-SWITCHKINS"), ("pose", "AXIS-XYZAC-KINEMATICS"), ("state_hash", "AXIS-XYZAC-ERROR-RECOVERY")]] error_contract = [{"error": error, "slice_id": slice_id} for error, slice_id in [("axis_gate", "AXIS-XYZAC-SHELL-MENU"), ("M428_HAL_PIN_MISSING", "AXIS-XYZAC-M428-M429-M430"), ("kinematics_singular", "AXIS-XYZAC-KINEMATICS"), ("following_error", "AXIS-XYZAC-TASK-TP-MOTION"), ("ledger_reconcile_required", "AXIS-XYZAC-ERROR-RECOVERY")]] visual_contract = [{"visual": visual, "slice_id": "AXIS-XYZAC-PREVIEW-VISUAL"} for visual in ["canonical_toolpath", "wasm_pose", "wasm_joints", "vismach_clear"]] recovery_contract = [{"recovery": recovery, "slice_id": "AXIS-XYZAC-ERROR-RECOVERY"} for recovery in ["pause_resume", "abort", "checkpoint", "worker_restart_reconcile", "no_duplicate_mcode"]] @@ -190,7 +190,7 @@ def write() -> dict[str, Any]: REPORT.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8") public = {"schema": report["schema"], "context_id": CONTEXT_ID, "case": report["case"], "axis_controls": report["axis_controls"], "slices": report["slices"], "command_contract": report["command_contract"], "state_contract": report["state_contract"], "error_contract": report["error_contract"], "visual_contract": report["visual_contract"], "recovery_contract": report["recovery_contract"], "counts": report["counts"], "authority": report["authority"], "report_sha256": report["report_sha256"]} SLICE_MODULE.write_text("export const XYZAC_TRT_SOURCE_SLICES = Object.freeze(" + json.dumps(public, ensure_ascii=False, sort_keys=True, indent=2) + ");\n", encoding="utf-8") - contract = {"schema": "xyzac-trt-case-contract.v1", "context_id": CONTEXT_ID, "case": report["case"], "resources": report["exact_resources"], "commands": ["MDI_EXECUTE", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "JOG_START", "JOG_STOP", "HOME", "KINEMATICS_SELECT", "SNAPSHOT_REQUEST"], "authority": "linuxcnc-wasm-worker", "js_authority": 0, "report_sha256": report["report_sha256"]} + contract = {"schema": "xyzac-trt-case-contract.v1", "context_id": CONTEXT_ID, "case": report["case"], "resources": report["exact_resources"], "commands": ["POWER", "RESET", "ESTOP", "MDI_EXECUTE", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", "JOG_START", "JOG_STOP", "HOME", "KINEMATICS_SELECT", "SNAPSHOT_REQUEST"], "authority": "linuxcnc-wasm-worker", "js_authority": 0, "report_sha256": report["report_sha256"]} CONTRACT_MODULE.write_text("export const XYZAC_TRT_CASE_CONTRACT = Object.freeze(" + json.dumps(contract, ensure_ascii=False, sort_keys=True, indent=2) + ");\n", encoding="utf-8") return report diff --git a/wasm-core/CMakeLists.txt b/wasm-core/CMakeLists.txt index 25b5bb7..d0c19da 100644 --- a/wasm-core/CMakeLists.txt +++ b/wasm-core/CMakeLists.txt @@ -250,6 +250,7 @@ if(EMSCRIPTEN) string(REPLACE "_sim_hal_set_pin" "_sim_hal_set_pin_scalar','_sim_hal_set_pin" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_hal_set_param" "_sim_hal_set_param_scalar','_sim_hal_set_param" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_io_init" "_sim_io_size','_sim_io_init" _sim_module_link_flags "${_sim_module_link_flags}") + string(REPLACE "_sim_io_set_estop','_sim_io_recover" "_sim_io_set_estop','_sim_io_set_power','_sim_io_recover" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_device_init" "_sim_device_io_size','_sim_device_init" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_motion_step','_sim_motion_read_state" "_sim_motion_step','_sim_motion_set_profile','_sim_motion_set_lookahead','_sim_motion_peek','_sim_motion_read_state" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_motion_state_hash','_sim_motion_canonical" "_sim_motion_state_hash','_sim_motion_plan_hash','_sim_motion_canonical" _sim_module_link_flags "${_sim_module_link_flags}") diff --git a/wasm-core/include/sim_io.h b/wasm-core/include/sim_io.h index febd547..4196dad 100644 --- a/wasm-core/include/sim_io.h +++ b/wasm-core/include/sim_io.h @@ -140,6 +140,7 @@ int sim_io_probe_update(sim_io *io, const sim_posemath_pose *pose, uint32_t input); int sim_io_probe_disarm(sim_io *io); int sim_io_set_estop(sim_io *io, uint32_t estop); +int sim_io_set_power(sim_io *io, uint32_t active); int sim_io_recover(sim_io *io); int sim_io_step(sim_io *io, uint32_t ticks); int sim_io_read_state(const sim_io *io, sim_io_state *state); diff --git a/wasm-core/src/sim_io.c b/wasm-core/src/sim_io.c index 2f430ca..af90f62 100644 --- a/wasm-core/src/sim_io.c +++ b/wasm-core/src/sim_io.c @@ -454,6 +454,33 @@ int sim_io_set_estop(sim_io *io, uint32_t estop) { return SIM_IO_OK; } +int sim_io_set_power(sim_io *io, uint32_t active) { + if (!valid_io(io) || active > 1U) return SIM_IO_ERR_ARGUMENT; + if (io->state.estop) { set_last(io, SIM_IO_ERR_ESTOP); return SIM_IO_ERR_ESTOP; } + if (!active) { + io->state.ready = 0; + io->state.tool_prepare = 0; + io->state.tool_change = 0; + io->state.tool_prepared = 0; + io->state.tool_changed = 0; + io->state.tool_state = SIM_IO_TOOL_ABORTED; + for (uint32_t index = 0; index < io->state.spindle_count; ++index) { + io->state.spindles[index].command_rpm = 0.0; + io->state.spindles[index].enabled = 0; + io->state.spindles[index].brake = 1; + } + } else { + if (io->state.fault || io->state.toolchanger_fault) { set_last(io, SIM_IO_ERR_FAULT); return SIM_IO_ERR_FAULT; } + if (!spindle_stopped(io)) { set_last(io, SIM_IO_ERR_BUSY); return SIM_IO_ERR_BUSY; } + io->state.ready = 1; + io->state.recovery_epoch += 1; + io->state.tool_state = SIM_IO_TOOL_IDLE; + } + set_last(io, SIM_IO_OK); + mirror_pins(io); + return SIM_IO_OK; +} + int sim_io_recover(sim_io *io) { if (!valid_io(io)) return SIM_IO_ERR_ARGUMENT; if (io->state.estop) return SIM_IO_ERR_ESTOP; diff --git a/wasm-core/tests/sim_io_test.c b/wasm-core/tests/sim_io_test.c index 8346f11..b3a956d 100644 --- a/wasm-core/tests/sim_io_test.c +++ b/wasm-core/tests/sim_io_test.c @@ -63,6 +63,9 @@ int main(void) { assert(sim_io_set_estop(&io, 0) == SIM_IO_OK); recover_status = sim_io_recover(&io); assert(recover_status == SIM_IO_OK && io.state.ready == 1 && io.state.recovery_epoch == 1); + assert(sim_io_set_power(&io, 0) == SIM_IO_OK && io.state.ready == 0); + assert(sim_io_spindle_command(&io, 0, 100.0, 1) == SIM_IO_ERR_NOT_READY); + assert(sim_io_set_power(&io, 1) == SIM_IO_OK && io.state.ready == 1 && io.state.recovery_epoch == 2); assert(sim_io_tool_set_fault(&io, 1, 42) == SIM_IO_OK); fault_clear_status = sim_io_tool_clear_fault(&io); diff --git a/web/app/axis-contract.mjs b/web/app/axis-contract.mjs index 45a5131..64a9d25 100644 --- a/web/app/axis-contract.mjs +++ b/web/app/axis-contract.mjs @@ -28,8 +28,15 @@ export function normalizeAxisState(input = {}) { const program = input.program ?? input.snapshot?.program ?? {}; const motion = input.motion ?? input.snapshot?.motion ?? {}; const devices = input.devices ?? input.snapshot?.devices ?? {}; + const ioSource = input.io ?? input.snapshot?.io ?? input.device_io ?? input.deviceIo ?? input.snapshot?.device_io ?? {}; + const ioState = ioSource?.state ?? ioSource ?? {}; const rawTaskState = string(task.state ?? input.task_state ?? input.state, "OFF"); - const taskState = rawTaskState === "ESTOP" ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED", "ON"].includes(rawTaskState) ? "ON" : "OFF"; + const ioHasEstop = typeof ioState.estop === "boolean" || typeof ioState.estop === "number"; + const ioEstop = ioHasEstop && Boolean(ioState.estop); + const ioHasReady = typeof ioState.ready === "boolean" || typeof ioState.ready === "number"; + const ioReady = ioHasReady && Boolean(ioState.ready); + const ioEstopReset = ioHasEstop && !ioEstop && ioHasReady && !ioReady && Number(ioState.last_error) === -904; + const taskState = ioEstop ? "ESTOP" : ioEstopReset ? "ESTOP_RESET" : ioHasReady && !ioReady ? "OFF" : rawTaskState === "ESTOP" ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED", "ON"].includes(rawTaskState) ? "ON" : "OFF"; const inferredInterp = rawTaskState === "RUNNING" ? "READING" : rawTaskState === "PAUSED" ? "PAUSED" : "IDLE"; const interpState = string(task.interp_state ?? input.interp_state ?? input.interpreter_state, inferredInterp); const mode = string(task.mode ?? input.task_mode ?? input.mode, "MANUAL"); @@ -47,7 +54,7 @@ export function normalizeAxisState(input = {}) { ? [true, true, true] : []; const limits = input.on_any_limit ?? motion.on_any_limit ?? motion.limit ?? false; - const fault = rawTaskState === "FAULT" || Boolean(motion.fault || devices.fault || input.fault); + const fault = rawTaskState === "FAULT" || Boolean(motion.fault || devices.fault || ioState.fault || input.fault); const tool = devices.tool ?? input.tool ?? {}; const toolNumber = number(tool.current ?? input.tool_in_spindle); const taskFile = Boolean(input.taskfile ?? program.path ?? program.source_hash ?? program.loaded ?? (program.program_status && String(program.program_status).toLowerCase() !== "idle")); @@ -73,7 +80,12 @@ export function axisGate(action, input = {}) { switch (action) { case "estop": enabled = true; reason = estop ? "axis_estop_reset_available" : "axis_estop_available"; break; case "recover": enabled = true; reason = "axis_recover_available"; break; - case "power": enabled = state.taskState !== "ESTOP"; reason = enabled ? "axis_power_available" : "axis_estop_active"; break; + case "power": { + const requested = input.payload?.active; + enabled = requested === false ? machineOn && !running : requested === true ? !estop && !machineOn && !running : !estop && !running; + reason = estop ? "axis_estop_active" : running ? "axis_interp_running" : enabled ? "axis_power_available" : requested === false ? "axis_machine_off" : "axis_machine_on"; + break; + } case "override": enabled = machineOn && !estop; reason = enabled ? "axis_override_available" : estop ? "axis_estop_active" : "axis_machine_off"; break; case "reset": enabled = machineOn && !running && !state.onAnyLimit; reason = enabled ? "axis_reset_available" : state.onAnyLimit ? "axis_limit_active" : "axis_reset_state_gate"; break; case "open": enabled = idle; reason = enabled ? "axis_interp_idle" : "axis_interp_running"; break; diff --git a/web/app/axis-operation-controller.mjs b/web/app/axis-operation-controller.mjs index aaf9bcf..3b515a9 100644 --- a/web/app/axis-operation-controller.mjs +++ b/web/app/axis-operation-controller.mjs @@ -1,16 +1,7 @@ -import { AXIS_COMMANDS, XYZAC_GATE_ACTIONS, axisGate, xyzacGate } from "./axis-contract.mjs"; +import { AXIS_COMMANDS } from "./axis-contract.mjs"; export const COMMAND_ACTIONS = Object.freeze({ ...Object.fromEntries(Object.entries(AXIS_COMMANDS).map(([action, command]) => [command, action])), VISUAL_CLEAR: "vismachClear" }); -export class AxisGateError extends Error { - constructor(result) { - super(result.reason); - this.name = "AxisGateError"; - this.code = result.reason; - this.gate = result; - } -} - export class AxisOperationController { constructor(client) { if (!client || typeof client.send !== "function") throw new Error("axis_operation_client_required"); @@ -26,17 +17,13 @@ export class AxisOperationController { async execute(kind, payload = {}, { state = {}, action = COMMAND_ACTIONS[kind] ?? null, gate = true } = {}) { const operationId = this.nextOperationId(kind); - const gateResult = action ? (XYZAC_GATE_ACTIONS.includes(action) ? xyzacGate(action, state) : axisGate(action, state)) : { action: null, enabled: true, reason: "axis_unrestricted_internal_command" }; - const authoritativeGate = action && state?.gates?.[action]; - const effectiveGate = authoritativeGate ?? gateResult; - if (gate && !effectiveGate.enabled) { - this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "blocked", reason: effectiveGate.reason })); - throw new AxisGateError(effectiveGate); - } + const effectiveGate = state?.gates?.[action] ?? { action, enabled: true, reason: "linuxcnc_wasm_worker_authority" }; this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "pending", reason: effectiveGate.reason })); try { const response = await this.client.send(kind, payload, operationId); - this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "result", response_kind: response?.kind ?? null })); + const responsePayload = response?.payload && typeof response.payload === "object" ? response.payload : {}; + const workerBlocked = response?.kind === "ERROR" || responsePayload.status === "blocked"; + this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: workerBlocked ? "blocked" : "result", reason: responsePayload.gate_reason ?? responsePayload.message_key ?? responsePayload.code ?? response?.kind ?? null })); return Object.freeze({ operationId, response, gate: effectiveGate }); } catch (error) { this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "error", reason: error?.message ?? String(error) })); diff --git a/web/app/test_axis_operation_controller.mjs b/web/app/test_axis_operation_controller.mjs index e8ef521..46b3b1b 100644 --- a/web/app/test_axis_operation_controller.mjs +++ b/web/app/test_axis_operation_controller.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { AxisGateError, AxisOperationController } from "./axis-operation-controller.mjs"; +import { AxisOperationController } from "./axis-operation-controller.mjs"; const calls = []; const client = { send: async (kind, payload, operationId) => { calls.push({ kind, payload, operationId }); return { kind: "COMMAND_RESULT", payload: { status: "verified" } }; } }; @@ -7,10 +7,11 @@ const controller = new AxisOperationController(client); const ready = { task: { state: "READY", mode: "MANUAL" }, program: { path: "main.ngc", program_status: "loaded" } }; const run = await controller.execute("PROGRAM_RUN", {}, { state: ready }); assert.equal(run.operationId, "axis-program-run-000001"); -assert.equal(run.gate.reason, "axis_run_available"); +assert.equal(run.gate.reason, "linuxcnc_wasm_worker_authority"); assert.equal(calls[0].operationId, run.operationId); -await assert.rejects(() => controller.execute("JOG_START", { axis: "x" }, { state: { task: { state: "ESTOP" } } }), AxisGateError); +const jog = await controller.execute("JOG_START", { axis: "x" }, { state: { task: { state: "ESTOP" } } }); +assert.equal(jog.operationId, "axis-jog-start-000002"); const tick = await controller.execute("SIM_STEP", { cycles: 1 }, { state: ready, gate: false }); assert.equal(tick.operationId, "axis-sim-step-000003"); -assert.deepEqual(controller.exportTrace().map((entry) => entry.phase), ["pending", "result", "blocked", "pending", "result"]); +assert.deepEqual(controller.exportTrace().map((entry) => entry.phase), ["pending", "result", "pending", "result", "pending", "result"]); console.log(JSON.stringify({ schema: "axis-operation-controller-sample.v1", status: "verified", calls: calls.length, phases: controller.exportTrace().map((entry) => entry.phase), last_operation: tick.operationId })); diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index 11dd45e..c8d6f3f 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -5,8 +5,7 @@ import { XyzacToolPreviewFromSnapshot } from "./XyzacToolPreview"; import { SIM_CASE_CATALOG } from "../../workspace/sim-case-catalog.mjs"; import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs"; import { axisIcon } from "./sourceAssets"; -import { AxisGateError, AxisOperationController } from "../../app/axis-operation-controller.mjs"; -import { axisGate } from "../../app/axis-contract.mjs"; +import { AxisOperationController } from "../../app/axis-operation-controller.mjs"; import { XyzacDiagnostics, XyzacProgramPane, XyzacTrtPanel } from "./XyzacTrtPanel"; import { XYZAC_CASE, projectXyzacPanel, type XyzacOperationTrace } from "./xyzac-panel-contract"; @@ -32,21 +31,21 @@ function Menu({ label, children }: { label: string; children: React.ReactNode }) return
{label}
{children}
; } -function MenuAction({ children, onClick, shortcut }: { children: React.ReactNode; onClick?: () => void; shortcut?: string }) { - return ; +function MenuAction({ children, onClick, shortcut, disabled = false }: { children: React.ReactNode; onClick?: () => void; shortcut?: string; disabled?: boolean }) { + return ; } function AxisRadio({ axis, selected, onChange }: { axis: string; selected: string; onChange: (axis: string) => void }) { return ; } -function MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programRunning, canHome, canRun, onEstop, onRelease, onPower, onHomeAll, onRun, onSequence }: { isEstop: boolean; machineOn: boolean; allHomed: boolean; programLoaded: boolean; programRunning: boolean; canHome: boolean; canRun: boolean; onEstop: () => void; onRelease: () => void; onPower: () => void; onHomeAll: () => void; onRun: () => void; onSequence: () => void }) { +function MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programRunning, canPower, canHome, canRun, onEstop, onRelease, onPower, onHomeAll, onRun, onSequence }: { isEstop: boolean; machineOn: boolean; allHomed: boolean; programLoaded: boolean; programRunning: boolean; canPower: boolean; canHome: boolean; canRun: boolean; onEstop: () => void; onRelease: () => void; onPower: () => void; onHomeAll: () => void; onRun: () => void; onSequence: () => void }) { return
Machine sequence{isEstop ? "ESTOP" : machineOn ? "ON" : "OFF"} / {allHomed ? "HOME ALL" : "NOT HOMED"}
Emergency Stop Release E-stop - Power On + {machineOn ? "Power Off" : "Power On"} HOME ALL Run Program Run Startup Sequence @@ -148,7 +147,8 @@ export function App() { const programRunning = operationState === "RUNNING"; const programPaused = operationState === "PAUSED"; const idle = operationState === "IDLE"; - const gates = Object.keys(state.gates).length ? state.gates : { load: axisGate("load", axisState), run: axisGate("run", axisState), step: axisGate("step", axisState), pause: axisGate("pause", axisState), resume: axisGate("resume", axisState), abort: axisGate("abort", axisState), mdi: axisGate("mdi", axisState), jog: axisGate("jogStart", axisState), home: axisGate("home", axisState) }; + const wasmGate = (action: string) => state.gates[action] ?? { action, enabled: false, reason: "linuxcnc_wasm_snapshot_pending" }; + const gates = { load: wasmGate("load"), run: wasmGate("run"), step: wasmGate("step"), pause: wasmGate("pause"), resume: wasmGate("resume"), abort: wasmGate("abort"), mdi: wasmGate("mdi"), jog: wasmGate("jog"), home: wasmGate("home"), power: wasmGate("power"), reset: wasmGate("reset") }; const planner = state.snapshot?.authoritative?.planner; const trajectory = [...(planner?.queue ?? []), ...(planner?.active ? [planner.active] : [])]; const stopRunTimer = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } }; @@ -161,7 +161,7 @@ export function App() { setOperationTrace(operationController.exportTrace()); return pending.then(() => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN") startRunTimer(); if (kind === "PROGRAM_ABORT" || kind === "PROGRAM_RESTART") stopRunTimer(); if (kind === "JOG_START") startJogTimer(); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); - const code = error instanceof AxisGateError ? error.code : error.message; + const code = error.message; setCommandStatus(`${kind} blocked: ${code}`); throw error; }); @@ -170,14 +170,15 @@ export function App() { const runProgram = () => { void dispatch("PROGRAM_RUN").catch(() => undefined); }; const stopProgram = () => { const wasRunning = Boolean(runTimer.current) || gates.abort.enabled; stopRunTimer(); if (wasRunning) void dispatch("PROGRAM_ABORT", {}, gates.abort.enabled).catch(() => undefined); }; const emergencyStop = () => { stopRunTimer(); stopJogTimer(); void dispatch("ESTOP", { active: true }, false).catch(() => undefined); }; - const releaseEstop = () => dispatch("ESTOP", { active: false }, false).then(() => dispatch("IO_RECOVER", {}, false)); + const releaseEstop = () => dispatch("ESTOP", { active: false }, false); const toggleEstop = () => { if (!isEstop) { emergencyStop(); return; } void releaseEstop().catch(() => undefined); }; - const powerOn = () => { const power = () => dispatch("POWER", { active: true }, false); if (isEstop) void releaseEstop().then(power).catch(() => undefined); else void power().catch(() => undefined); }; - const resetMachine = () => { stopRunTimer(); stopJogTimer(); const reset = () => dispatch("RESET", {}, false); if (programRunning) void dispatch("PROGRAM_ABORT").then(() => isEstop ? dispatch("ESTOP", { active: false }) : Promise.resolve()).then(reset).catch(() => undefined); else if (isEstop) void dispatch("ESTOP", { active: false }).then(reset).catch(() => undefined); else void reset().catch(() => undefined); }; + const powerOn = () => { if (isEstop || programRunning || !gates.power.enabled) return; void dispatch("POWER", { active: !machineOn }, false).catch(() => undefined); }; + const resetMachine = () => { stopRunTimer(); stopJogTimer(); if (isEstop || programRunning || !machineOn) return; void dispatch("RESET").catch(() => undefined); }; const homeAll = () => { void dispatch("HOME", { all: true }).catch(() => undefined); }; const runStartupSequence = () => { const release = isEstop ? releaseEstop() : Promise.resolve(); - void release.then(() => dispatch("POWER", { active: true }, false)) + const power = () => machineOn ? Promise.resolve() : dispatch("POWER", { active: true }, false); + void release.then(power) .then(() => dispatch("HOME", { all: true }, false)) .then(() => dispatch("PROGRAM_RUN", {}, false)) .catch(() => undefined); @@ -224,10 +225,10 @@ export function App() { const position = `X${Number(xyzacView.pose?.x ?? axes[0] ?? 0).toFixed(3)} Y${Number(xyzacView.pose?.y ?? axes[1] ?? 0).toFixed(3)} Z${Number(xyzacView.pose?.z ?? axes[2] ?? 0).toFixed(3)} A${Number(xyzacView.pose?.a ?? 0).toFixed(3)} C${Number(xyzacView.pose?.c ?? 0).toFixed(3)}`; return
AXIS AXISLinuxCNC Web/WASM
{state.status.toUpperCase()}{taskState}
- -
{ setProgramText(DEFAULT_PROGRAM); setPlotCleared(false); }} /> execute("PROGRAM_STEP")} /> execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /> setBlockDelete((value) => !value)} /> setOptionalStop((value) => !value)} /> setZoom((value) => Math.min(2, value + .2))} /> setZoom((value) => Math.max(.5, value - .2))} /> { setView("z"); setRightTab("preview"); }} /> { setView("z2"); setRightTab("preview"); }} /> { setView("x"); setRightTab("preview"); }} /> { setView("y"); setRightTab("preview"); }} /> { setView("y2"); setRightTab("preview"); }} /> { setView("p"); setRightTab("preview"); }} /> setRotateMode((value) => !value)} /> xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} />
+ +
{ setProgramText(DEFAULT_PROGRAM); setPlotCleared(false); }} /> execute("PROGRAM_STEP")} /> execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /> setBlockDelete((value) => !value)} /> setOptionalStop((value) => !value)} /> setZoom((value) => Math.min(2, value + .2))} /> setZoom((value) => Math.max(.5, value - .2))} /> { setView("z"); setRightTab("preview"); }} /> { setView("z2"); setRightTab("preview"); }} /> { setView("x"); setRightTab("preview"); }} /> { setView("y"); setRightTab("preview"); }} /> { setView("y2"); setRightTab("preview"); }} /> { setView("p"); setRightTab("preview"); }} /> setRotateMode((value) => !value)} /> xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} />
Load case{caseStatus}{commandStatus}
- { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} /> + { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} />
{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => )}
diff --git a/web/react/src/legacy.d.ts b/web/react/src/legacy.d.ts index be22613..c3f35f8 100644 --- a/web/react/src/legacy.d.ts +++ b/web/react/src/legacy.d.ts @@ -9,8 +9,6 @@ declare module "*.mjs" { execute(kind: string, payload?: Record, options?: { state?: unknown; action?: string | null; gate?: boolean }): Promise<{ operationId: string; response: unknown; gate: { enabled: boolean; reason: string } }>; exportTrace(): Array>; } - export class AxisGateError extends Error { code: string; gate: { enabled: boolean; reason: string }; } - export function axisGate(action: string, input?: unknown): { enabled: boolean; reason: string; action: string; taskState?: string; interpState?: string }; } declare module "*.js" { const createModule: (options?: unknown) => Promise; export default createModule; } diff --git a/web/worker/axis-state.mjs b/web/worker/axis-state.mjs index a414c4e..0e9ea46 100644 --- a/web/worker/axis-state.mjs +++ b/web/worker/axis-state.mjs @@ -31,19 +31,29 @@ function programActivity(task, program) { return "IDLE"; } -export function axisState({ task = {}, program = {}, action = null, payload = {} } = {}) { +export function axisState({ task = {}, program = {}, motion = {}, devices = {}, device_io = {}, io = {}, action = null, payload = {} } = {}) { const taskState = String(task.state ?? "NEW").toUpperCase(); - let machineState = ["ESTOP", "FAULT"].includes(taskState) ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(taskState) ? "ON" : "OFF"; + const ioState = io?.state ?? io ?? device_io?.state ?? device_io ?? {}; + const hasIoState = typeof ioState.estop === "boolean" || typeof ioState.estop === "number"; + const ioEstop = hasIoState && Boolean(ioState.estop); + const ioReady = typeof ioState.ready === "boolean" || typeof ioState.ready === "number" ? Boolean(ioState.ready) : null; + // LinuxCNC exposes the post-E-stop, powered-off state as + // STATE_ESTOP_RESET (STATE_OFF is an alias in the AXIS UI). The WASM IO + // ready bit is the authority for distinguishing it from STATE_ON. + const estopReset = hasIoState && !ioEstop && ioReady === false; + let machineState = ioEstop || ["ESTOP", "FAULT"].includes(taskState) ? "ESTOP" : estopReset ? "ESTOP_RESET" : ioReady === false ? "OFF" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(taskState) ? "ON" : "OFF"; let operationState = programActivity(task, program); if (action === "ESTOP" && payload.active === true) machineState = "ESTOP"; if (action === "ESTOP" && payload.active === false) machineState = "ESTOP_RESET"; if (action === "IO_RECOVER") machineState = "ON"; + if (action === "POWER" && payload.active === true) machineState = "ON"; + if (action === "POWER" && payload.active === false) machineState = "OFF"; if (action === "HOME") operationState = "HOME"; if (action === "PROGRAM_PAUSE") operationState = "PAUSED"; if (action === "PROGRAM_RUN" || action === "PROGRAM_RESUME") operationState = "RUNNING"; if (["PROGRAM_LOAD", "PROGRAM_ABORT", "PROGRAM_RESTART", "MDI_EXECUTE"].includes(action)) operationState = "IDLE"; const state = machineState === "ESTOP" || machineState === "ESTOP_RESET" ? machineState : operationState === "HOME" ? "HOME" : operationState === "RUNNING" ? "RUNNING" : operationState === "PAUSED" ? "PAUSED" : machineState === "OFF" ? "OFF" : "IDLE"; - const sourceTaskState = taskState === "ESTOP" ? "STATE_ESTOP" : machineState === "ESTOP_RESET" ? "STATE_ESTOP_RESET" : machineState === "OFF" ? "STATE_OFF" : "STATE_ON"; + const sourceTaskState = machineState === "ESTOP" ? "STATE_ESTOP" : machineState === "ESTOP_RESET" ? "STATE_ESTOP_RESET" : machineState === "OFF" ? "STATE_OFF" : "STATE_ON"; const sourceInterpState = operationState === "PAUSED" ? "INTERP_PAUSED" : operationState === "RUNNING" ? "INTERP_READING" : "INTERP_IDLE"; const sourceMode = String(task.mode ?? "MANUAL").toUpperCase(); return Object.freeze({ state, machine_state: machineState, operation_state: operationState, source_task_state: sourceTaskState, source_interp_state: sourceInterpState, source_mode: sourceMode, states: [...AXIS_STATES] }); diff --git a/web/worker/capabilities.mjs b/web/worker/capabilities.mjs index 02cfc1e..4a408e5 100644 --- a/web/worker/capabilities.mjs +++ b/web/worker/capabilities.mjs @@ -12,7 +12,7 @@ export const LIMITS = Object.freeze({ export const COMMANDS = Object.freeze([ "SESSION_ATTACH", "LOAD_WORKSPACE", "COMMAND", "SIM_STEP", "SNAPSHOT_REQUEST", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", - "PROGRAM_STEP", "PROGRAM_SEEK", "MDI_EXECUTE", "CHECKPOINT", "RECOVER", "DESTROY", + "PROGRAM_STEP", "PROGRAM_SEEK", "MDI_EXECUTE", "CHECKPOINT", "RECOVER", "DESTROY", "POWER", "RESET", "SET_MODE", "SET_OVERRIDE", "JOG_START", "JOG_STOP", "HOME", "ESTOP", "IO_RECOVER", "LIMIT_SET", "FOLLOWING_ERROR", "SPINDLE_COMMAND", "SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "TOOL_PREPARE", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_CHANGED", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT", "PROBE_CONFIG", "ENCODER_SET", "COOLANT_COMMAND", "COOLANT_FAULT", "DOOR_SET", "INTERLOCK_SET", "INTERLOCK_RESET", "DEVICE_ESTOP", "DEVICE_ACK_ISSUE", "DEVICE_ACK", "DEVICE_ACK_ABORT", diff --git a/web/worker/sim-session.mjs b/web/worker/sim-session.mjs index 40d9889..47a06af 100644 --- a/web/worker/sim-session.mjs +++ b/web/worker/sim-session.mjs @@ -149,7 +149,7 @@ export class SimSession { motion_authority: statePayload.motion_authority ?? this.motionAuthority, task_authority: statePayload.task_authority ?? this.task.authority(), ...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}), - axis_state: axisState({ task, program, action: input.kind, payload: input.payload ?? {} }), + axis_state: axisState({ task, program, motion: this.motionProjection(), devices: this.wasmDeviceProjection(), io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }), }, }; } @@ -220,17 +220,41 @@ export class SimSession { const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(); const wasmMotion = this.wasmMotion.read(); const homeLimit = this.wasmHomeLimit.read(); + const ioState = this.xyzacIo?.canonical?.() ?? {}; const devices = this.wasmDeviceProjection(); const deviceIo = this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(); const halRuntime = this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(); const authoritative = this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker", state_hash: fnv1a64({ motion: wasmMotion, kinematics: this.xyzacKinematics?.snapshot() ?? null, task }) } : this.authoritative.export(); - const axis = axisState({ task, program: program ?? {}, action: input.kind, payload: input.payload ?? {} }); + const axis = axisState({ task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, action: input.kind, payload: input.payload ?? {} }); const xyzac = this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null; - const gateInput = { task, program: program ?? {}, motion, devices, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" }; + const gateInput = { task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" }; 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 gates = { ...Object.fromEntries(gateActions.map((action) => [action, axisGate(action, gateInput)])), ...xyzacGateMatrix(gateInput) }; + const gates = this.xyzacProgram + ? { + load: this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }), + run: this.xyzacCommandGate({ kind: "PROGRAM_RUN", payload: {} }), + runLine: this.xyzacCommandGate({ kind: "PROGRAM_RUN", payload: {} }), + step: this.xyzacCommandGate({ kind: "PROGRAM_STEP", payload: {} }), + pause: this.xyzacCommandGate({ kind: "PROGRAM_PAUSE", payload: {} }), + resume: this.xyzacCommandGate({ kind: "PROGRAM_RESUME", payload: {} }), + abort: this.xyzacCommandGate({ kind: "PROGRAM_ABORT", payload: {} }), + restart: this.xyzacCommandGate({ kind: "PROGRAM_RESTART", payload: {} }), + mdi: this.xyzacCommandGate({ kind: "MDI_EXECUTE", payload: {} }), + jogStart: this.xyzacCommandGate({ kind: "JOG_START", payload: {} }), + jogStop: this.xyzacCommandGate({ kind: "JOG_STOP", payload: {} }), + home: this.xyzacCommandGate({ kind: "HOME", payload: {} }), + estop: this.xyzacCommandGate({ kind: "ESTOP", payload: { active: true } }), + recover: this.xyzacCommandGate({ kind: "IO_RECOVER", payload: {} }), + power: this.xyzacCommandGate({ kind: "POWER", payload: { active: !Boolean(ioState.ready) } }), + powerOn: this.xyzacCommandGate({ kind: "POWER", payload: { active: true } }), + powerOff: this.xyzacCommandGate({ kind: "POWER", payload: { active: false } }), + reset: this.xyzacCommandGate({ kind: "RESET", payload: {} }), + ...Object.fromEntries([0, 1, 2].map((type) => [type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : "switchkinsUserk", this.xyzacCommandGate({ kind: "KINEMATICS_SELECT", payload: { type } })])), + vismachClear: { action: "vismachClear", enabled: true, reason: "axis_visual_only", visual_only: true }, + } + : { ...Object.fromEntries(gateActions.map((action) => [action, axisGate(action, gateInput)])), ...xyzacGateMatrix(gateInput) }; const wasmPosition = this.motionAuthority === "wasm" && wasmMotion.status === "verified" ? wasmMotion.state.position : authoritative.kinematics.pose; @@ -308,7 +332,12 @@ export class SimSession { const program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : {}; const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(); const devices = this.wasmDeviceProjection(); + // The selected XYZAC case is governed by the native WASM task/IO/program + // handlers. JS only receives the result; it must not pre-reject a command + // using a duplicate gate implementation. + if (this.xyzacProgram && input.kind !== "VISUAL_CLEAR") return this.xyzacCommandGate(input); if (input.kind === "KINEMATICS_SELECT" || input.kind === "VISUAL_CLEAR") { + if (input.kind === "KINEMATICS_SELECT" && ![0, 1, 2].includes(Number(input.payload?.type))) return { action: null, enabled: false, reason: "xyzac_kinematics_type_invalid" }; const type = Number(input.payload?.type); const action = input.kind === "VISUAL_CLEAR" ? "vismachClear" : type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : type === 2 ? "switchkinsUserk" : null; if (!action) return { action: null, enabled: false, reason: "xyzac_kinematics_type_invalid" }; @@ -326,6 +355,71 @@ export class SimSession { }); } + xyzacCommandGate(input) { + const task = this.task.export(); + const program = this.xyzacProgram.snapshot(); + const io = this.xyzacIo?.canonical?.() ?? {}; + const wasmMotion = this.wasmMotion.read(); + const active = task.state === "RUNNING" || task.state === "PAUSED" || program.status === 2 || program.status === 3; + const estop = Boolean(io.estop); + const ready = Boolean(io.ready) && !estop; + const loaded = program.status !== 0; + const action = PANEL_GATE_ACTIONS[input.kind] ?? null; + let enabled = true; + let reason = "linuxcnc_wasm_authority"; + if (input.kind === "KINEMATICS_SELECT" && ![0, 1, 2].includes(Number(input.payload?.type))) { + enabled = false; + reason = "xyzac_kinematics_type_invalid"; + } else if (input.kind === "ESTOP") { + enabled = input.payload?.active === true || estop; + reason = enabled ? "linuxcnc_wasm_estop_transition" : "linuxcnc_wasm_estop_inactive"; + } else if (input.kind === "POWER") { + const requested = input.payload?.active === true; + enabled = requested ? !estop && !ready && !active : !estop && ready && !active; + reason = estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : enabled ? "linuxcnc_wasm_power_transition" : requested ? "xyzac_power_already_on" : "xyzac_power_already_off"; + } else if (input.kind === "IO_RECOVER") { + enabled = !estop && ready && !active; + reason = enabled ? "linuxcnc_wasm_io_recover" : estop ? "xyzac_estop_active" : !ready ? "xyzac_power_required" : "xyzac_interp_running"; + } else if (input.kind === "LOAD_WORKSPACE") { + enabled = !estop && !active; + reason = enabled ? "linuxcnc_wasm_workspace_available" : estop ? "xyzac_estop_active" : "xyzac_interp_running"; + } else if (["PROGRAM_LOAD", "PROGRAM_RESTART"].includes(input.kind)) { + enabled = !estop && !active && (input.kind !== "PROGRAM_RESTART" || loaded); + reason = estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : enabled ? "linuxcnc_wasm_program_idle" : "xyzac_taskfile_missing"; + } else if (input.kind === "PROGRAM_RUN") { + enabled = ready && !active && loaded; + reason = !ready ? (estop ? "xyzac_estop_active" : "xyzac_machine_off") : active ? "xyzac_interp_running" : loaded ? "linuxcnc_wasm_run_available" : "axis_taskfile_missing"; + } else if (input.kind === "PROGRAM_PAUSE") { + enabled = ready && program.status === 2; + reason = enabled ? "linuxcnc_wasm_pause_available" : "xyzac_pause_state_gate"; + } else if (input.kind === "PROGRAM_RESUME") { + enabled = ready && program.status === 3; + reason = enabled ? "linuxcnc_wasm_resume_available" : "xyzac_resume_state_gate"; + } else if (input.kind === "PROGRAM_ABORT") { + enabled = active; + reason = enabled ? "linuxcnc_wasm_abort_available" : "xyzac_abort_state_gate"; + } else if (input.kind === "PROGRAM_STEP") { + enabled = ready && loaded && !active; + reason = !ready ? "xyzac_machine_off" : loaded ? "linuxcnc_wasm_step_available" : "axis_taskfile_missing"; + } else if (input.kind === "SET_MODE") { + enabled = ready && !active; + reason = enabled ? "linuxcnc_wasm_mode_available" : estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : "xyzac_machine_off"; + } else if (input.kind === "RESET") { + enabled = ready && !active; + reason = enabled ? "linuxcnc_wasm_reset_available" : estop ? "xyzac_estop_active" : !ready ? "xyzac_machine_off" : "xyzac_interp_running"; + } else if (input.kind === "HOME" || ["JOG_START", "JOG_STOP", "SET_OVERRIDE"].includes(input.kind)) { + enabled = ready && !active; + reason = enabled ? "linuxcnc_wasm_manual_available" : estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : "xyzac_machine_off"; + } else if (input.kind === "MDI_EXECUTE") { + enabled = ready && !active; + reason = enabled ? "linuxcnc_wasm_mdi_available" : estop ? "xyzac_estop_active" : active ? "mdi_while_program_active" : "xyzac_machine_off"; + } else if (["KINEMATICS_SELECT", "SPINDLE_COMMAND", "SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "TOOL_PREPARE", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_CHANGED", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT", "PROBE_CONFIG"].includes(input.kind)) { + enabled = ready && !active; + reason = enabled ? "linuxcnc_wasm_io_available" : estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : "xyzac_machine_off"; + } + return { action, enabled, reason, taskState: task.state, interpState: active ? (program.status === 3 ? "PAUSED" : "READING") : "IDLE", wasm: { io_ready: ready, io_estop: estop, program_status: program.program_status, queue_depth: wasmMotion.state.queue_depth } }; + } + stateForCheckpoint() { const state = { context_id: this.contextId, @@ -887,6 +981,7 @@ export class SimSession { } try { if (kind === "ESTOP" && payload?.active === true) this.task.estop(-904); + else if (kind === "ESTOP" && payload?.active === false && result.status === "verified" && this.xyzacProgram && this.task.state.state === "ESTOP") this.task.transition("STOPPED"); else if (kind === "IO_RECOVER" && result.status === "verified") this.task.recover(); else if (kind === "TOOL_FAULT" && payload?.fault === true) this.task.fault(-905); } catch (error) { @@ -945,14 +1040,31 @@ export class SimSession { try { this.task.transition("READY"); } catch (error) { return finish(this.error(input, "task_state", String(error?.message ?? error)), "failed"); } return finish(this.envelope("SESSION_READY", input, { state_hash: this.task.hash(), task: this.task.export(), subscriptions: [] }), "verified"); case "SET_MODE": { + if (this.xyzacProgram) { + const gateResult = this.xyzacCommandGate(input); + if (!gateResult.enabled) return finish(this.envelope("ERROR", input, { status: "blocked", code: "axis_gate", gate_reason: gateResult.reason, message_key: gateResult.reason, operation_id: input.operation_id ?? null }), "failed"); + } const mode = String(input.payload?.mode ?? "").toUpperCase(); 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"); } 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": { + if (this.xyzacProgram) { + const result = this.xyzacIo.command("POWER", input.payload ?? {}); + if (result.status === "verified") { + try { + if (input.payload?.active === true) this.task.recover(); + else if (["READY", "STOPPED"].includes(this.task.state.state)) { + if (this.task.state.state === "READY") this.task.transition("STOPPED"); + } + } catch { /* the WASM IO state remains authoritative */ } + if (this.xyzacDeviceIo) this.xyzacDeviceIo.command("INTERLOCK_RESET", input.payload ?? {}, input.operation_id); + } + return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, active: input.payload?.active === true, task: this.task.export(), device_io: this.xyzacIo.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); + } if (input.payload?.active !== true) return finish(this.error(input, "power_state", "power_active_required"), "failed"); - const result = this.xyzacIo ? this.xyzacIo.command("IO_RECOVER", input.payload ?? {}) : this.devices.command("IO_RECOVER", input.payload ?? {}); + const result = this.xyzacIo ? this.xyzacIo.command("POWER", input.payload ?? {}) : this.devices.command("IO_RECOVER", input.payload ?? {}); if (result.status === "verified") { try { this.task.recover(); } catch { /* READY is already powered for the selected simulation. */ } if (this.xyzacDeviceIo) this.xyzacDeviceIo.command("INTERLOCK_RESET", input.payload ?? {}, input.operation_id); @@ -960,6 +1072,13 @@ export class SimSession { return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, active: true, task: this.task.export(), device_io: this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); } case "RESET": { + if (this.xyzacProgram) { + const result = this.xyzacIo.command("RESET"); + if (result.status === "verified" && ["FAULT", "STOPPED"].includes(this.task.state.state)) { + try { this.task.recover(); } catch { /* retain the WASM IO result */ } + } + return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, task: this.task.export(), device_io: this.xyzacIo.export(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); + } if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "reset_while_program_active"), "failed"); const result = this.motion.resetFault(); if (result.status !== "verified") return finish(this.envelope("ERROR", input, { ...result, command: input.kind, task: this.task.export() }), "failed"); @@ -1023,6 +1142,8 @@ export class SimSession { this.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, limits: configuration.home_limits }); this.xyzacIo?.destroy(); this.xyzacIo = new WasmXyzacIo(this.core, { spindleCount: 2, toolCapacity: 1001 }); + const initialPower = this.xyzacIo.command("POWER", { active: false }); + if (initialPower.status !== "verified") throw new Error(initialPower.code ?? "wasm_power_off_init"); this.xyzacDeviceIo?.destroy(); this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: 2 }); this.xyzacProgram?.destroy(); diff --git a/web/worker/test_xyzac_button_constraints.mjs b/web/worker/test_xyzac_button_constraints.mjs new file mode 100644 index 0000000..3b040da --- /dev/null +++ b/web/worker/test_xyzac_button_constraints.mjs @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import createModule from "../../build-wasm/linuxcnc-sim-core.js"; +import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs"; +import { SimSession, CONTEXT_ID } from "./sim-session.mjs"; + +const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"; +const PROGRAM = "G21 G90\nG0 X0 Y0 Z10 A0 C0\nG1 X10 Y5 Z5 A30 C45 F100\nM2\n"; +const fetchImpl = async (url) => { + try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; } + catch { return { ok: false, text: async () => "" }; } +}; +const bundle = await loadSimulationCase({ path: CASE, fetchImpl }); +const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) }); +const session = new SimSession(core, { sessionId: "xyzac-button-constraints" }); +const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId }; +let seq = 0; +const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: `button-${seq}`, payload }); +send("HELLO"); +send("SESSION_ATTACH"); +const loaded = send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }); +assert.equal(loaded.payload.panel_snapshot.axis_state.machine_state, "ESTOP_RESET"); +assert.equal(send("PROGRAM_LOAD", { source: PROGRAM, path: "button.ngc" }).payload.status, "verified"); + +for (const [kind, payload, reason] of [["PROGRAM_RUN", {}, "xyzac_machine_off"], ["HOME", { all: true }, "xyzac_machine_off"], ["JOG_START", { axis: "x", direction: 1, speed: 1 }, "xyzac_machine_off"], ["MDI_EXECUTE", { block: "G0 X1" }, "xyzac_machine_off"], ["KINEMATICS_SELECT", { type: 1 }, "xyzac_machine_off"], ["POWER", { active: false }, "xyzac_power_already_off"]]) { + const blocked = send(kind, payload); + assert.equal(blocked.kind, "ERROR", kind); + assert.equal(blocked.payload.gate_reason, reason, kind); +} + +const powered = send("POWER", { active: true }); +assert.equal(powered.payload.panel_snapshot.axis_state.machine_state, "ON"); +assert.equal(send("KINEMATICS_SELECT", { type: 1 }).payload.kinematics.active_type, 1); +assert.equal(send("HOME", { all: true }).payload.wasm_home_limit.state.homed, true); +assert.equal(send("POWER", { active: false }).payload.panel_snapshot.axis_state.machine_state, "OFF"); +assert.equal(send("PROGRAM_RUN").payload.gate_reason, "xyzac_machine_off"); +assert.equal(send("POWER", { active: true }).payload.panel_snapshot.axis_state.machine_state, "ON"); + +assert.equal(send("PROGRAM_RUN").payload.task.state, "RUNNING"); +for (const [kind, payload, reason] of [["HOME", { all: true }, "xyzac_interp_running"], ["MDI_EXECUTE", { block: "G0 X1" }, "mdi_while_program_active"], ["KINEMATICS_SELECT", { type: 0 }, "xyzac_interp_running"], ["POWER", { active: false }, "xyzac_interp_running"]]) { + const blocked = send(kind, payload); + assert.equal(blocked.kind, "ERROR", kind); + assert.equal(blocked.payload.gate_reason, reason, kind); +} +assert.equal(send("PROGRAM_PAUSE").payload.task.state, "PAUSED"); +assert.equal(send("PROGRAM_RESUME").payload.task.state, "RUNNING"); +assert.equal(send("PROGRAM_ABORT").payload.task.state, "STOPPED"); + +assert.equal(send("ESTOP", { active: true }).payload.panel_snapshot.axis_state.machine_state, "ESTOP"); +const reset = send("ESTOP", { active: false }); +assert.equal(reset.payload.panel_snapshot.axis_state.machine_state, "ESTOP_RESET"); +assert.equal(send("POWER", { active: true }).payload.panel_snapshot.axis_state.machine_state, "ON"); + +console.log(JSON.stringify({ + schema: "xyzac-button-constraints.v1", + status: "verified", + case: CASE, + sequence: ["ESTOP", "ESTOP_RESET", "POWER_ON", "HOME", "IDLE", "RUNNING", "PAUSED", "RESUME", "ABORT", "ESTOP"], + authority: "linuxcnc-wasm-worker", + gates: { off_blocks_motion: true, running_blocks_manual: true, running_blocks_power_off: true, pause_resume_exclusive: true }, + state_hash: reset.payload.panel_snapshot.state_hash, +})); diff --git a/web/worker/test_xyzac_gate_matrix.mjs b/web/worker/test_xyzac_gate_matrix.mjs index 6e2eed6..4018e73 100644 --- a/web/worker/test_xyzac_gate_matrix.mjs +++ b/web/worker/test_xyzac_gate_matrix.mjs @@ -21,12 +21,17 @@ const initial = loaded.payload.panel_snapshot; assert.equal(initial.xyzac.coordinates, "XYZAC"); assert.equal(initial.xyzac.joint_count, 5); assert.equal(initial.gates.run.enabled, false); -assert.equal(initial.gates.run.reason, "axis_taskfile_missing"); -assert.equal(initial.gates.runLine.reason, "axis_taskfile_missing"); -assert.equal(initial.gates.switchkinsIdentity.enabled, true); -assert.equal(initial.gates.switchkinsXyzac.enabled, true); -assert.equal(initial.gates.switchkinsUserk.enabled, true); +assert.equal(initial.gates.run.reason, "xyzac_machine_off"); +assert.equal(initial.gates.runLine.reason, "xyzac_machine_off"); +assert.equal(initial.gates.switchkinsIdentity.enabled, false); +assert.equal(initial.gates.switchkinsXyzac.enabled, false); +assert.equal(initial.gates.switchkinsUserk.enabled, false); assert.equal(initial.gates.vismachClear.visual_only, true); +assert.equal(send("POWER", "power-on", { active: true }).payload.panel_snapshot.axis_state.machine_state, "ON"); +const powered = send("SNAPSHOT_REQUEST", "powered-snapshot").payload.panel_snapshot; +assert.equal(powered.gates.switchkinsIdentity.enabled, true); +assert.equal(powered.gates.switchkinsXyzac.enabled, true); +assert.equal(powered.gates.switchkinsUserk.enabled, true); const noFile = axisGate("runLine", { task: { state: "READY", mode: "MANUAL" }, program: {}, motion: {}, taskfile: false, highlight_line: null }); assert.equal(noFile.enabled, false); assert.equal(noFile.reason, "axis_taskfile_missing"); @@ -49,8 +54,9 @@ send("PROGRAM_ABORT", "abort"); const estopGates = xyzacGateMatrix({ task: { state: "ESTOP", mode: "MANUAL" }, program: { source_hash: "fnv1a64:file" }, motion: {}, taskfile: true, xyzac: { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true } }); assert.equal(estopGates.switchkinsXyzac.enabled, false); assert.equal(estopGates.switchkinsXyzac.reason, "xyzac_estop_active"); -send("ESTOP", "estop-reset", { active: false }); -send("IO_RECOVER", "recover"); +send("ESTOP", "estop-on", { active: true }); +assert.equal(send("ESTOP", "estop-reset", { active: false }).payload.panel_snapshot.axis_state.machine_state, "ESTOP_RESET"); +send("POWER", "power-on-again", { active: true }); const recovered = send("SNAPSHOT_REQUEST", "recovered-snapshot"); assert.equal(typeof recovered.payload.panel_snapshot.state_hash, "string"); assert.equal(recovered.payload.panel_snapshot.gates.vismachClear.visual_only, true); diff --git a/web/worker/test_xyzac_hal_tool_alarm.mjs b/web/worker/test_xyzac_hal_tool_alarm.mjs index dfdc029..b8ae679 100644 --- a/web/worker/test_xyzac_hal_tool_alarm.mjs +++ b/web/worker/test_xyzac_hal_tool_alarm.mjs @@ -14,6 +14,7 @@ let seq = 0; const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload }); send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); +assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); const xml = await readFile(new URL("../../linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml", import.meta.url), "utf8"); const postgui = await readFile(new URL("../../linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal", import.meta.url), "utf8"); diff --git a/web/worker/test_xyzac_machine_sequence.mjs b/web/worker/test_xyzac_machine_sequence.mjs index e858467..8034c12 100644 --- a/web/worker/test_xyzac_machine_sequence.mjs +++ b/web/worker/test_xyzac_machine_sequence.mjs @@ -49,19 +49,15 @@ const released = send("ESTOP", { active: false }); assert.equal(released.payload.status, "verified"); assert.equal(released.payload.device_io.state.estop, 0); assert.equal(released.payload.device_io.state.ready, 0); -assert.equal(released.payload.task.state, "ESTOP"); -const recovered = send("IO_RECOVER"); -assert.equal(recovered.payload.task.state, "READY"); -assert.equal(recovered.payload.device_io.state.estop, 0); -assert.equal(recovered.payload.device_io.state.ready, 1); -const recoveryEpoch = recovered.payload.device_io.state.recovery_epoch; +assert.equal(released.payload.task.state, "STOPPED"); +assert.equal(released.payload.panel_snapshot.axis_state.machine_state, "ESTOP_RESET"); const powered = send("POWER", { active: true }); assert.equal(powered.payload.status, "verified"); assert.equal(powered.payload.task.state, "READY"); assert.equal(powered.payload.device_io.source, "wasm_io"); assert.equal(powered.payload.device_io.state.ready, 1); -assert.ok(powered.payload.device_io.state.recovery_epoch > recoveryEpoch); +assert.equal(powered.payload.panel_snapshot.axis_state.machine_state, "ON"); const homed = send("HOME", { all: true }); assert.equal(homed.payload.status, "verified"); @@ -113,7 +109,7 @@ console.log(JSON.stringify({ schema: "xyzac-machine-sequence.v1", status: "verified", case: CASE, - sequence: ["ESTOP", "ESTOP_RELEASE", "IO_RECOVER", "POWER", "HOME_ALL", "PROGRAM_RUN", "SIM_STEP"], + sequence: ["ESTOP", "ESTOP_RESET", "POWER", "HOME_ALL", "PROGRAM_RUN", "SIM_STEP"], authority: { motion: "wasm", io: "wasm_io", home_limit: "wasm_home_limit", program: "linuxcnc-wasm-sim_program", visual: "wasm_canonical_motion" }, estop_queue_depth: emergency.payload.panel_snapshot.wasm_motion.state.queue_depth, home_joint_count: homed.payload.wasm_home_limit.state.joint_count, diff --git a/web/worker/test_xyzac_manual_control.mjs b/web/worker/test_xyzac_manual_control.mjs index f40e4d4..1c4f4f6 100644 --- a/web/worker/test_xyzac_manual_control.mjs +++ b/web/worker/test_xyzac_manual_control.mjs @@ -16,6 +16,7 @@ function setup(id) { const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload }); send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); + assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); return { session, send }; } @@ -69,7 +70,8 @@ const estop = mdiCase.send("ESTOP", "mdi-estop", { active: true }); assert.equal(estop.kind, "COMMAND_RESULT"); assert.equal(estop.payload.task.state, "ESTOP"); assert.equal(estop.payload.device_io.source, "wasm_io"); -const recovered = mdiCase.send("IO_RECOVER", "mdi-recover"); +mdiCase.send("ESTOP", "mdi-estop-reset", { active: false }); +const recovered = mdiCase.send("POWER", "mdi-power-after-estop", { active: true }); assert.equal(recovered.payload.motion_authority, "wasm"); console.log(JSON.stringify({ schema: "xyzac-manual-control-rtcp-06.v1", status: "verified", case: CASE, home: { authority: "wasm_home_limit", joints: 5 }, jog: jogRows, boundary: { out_of_range: outOfRange.payload.code, nonfinite_speed: nonFinite.payload.code, following_error: ferror.payload.code }, mdi: "verified", mdi_running_gate: mdiWhileRunning.payload.message_key, estop_recovery: "verified", state_hash: recovered.payload.panel_snapshot.state_hash })); diff --git a/web/worker/test_xyzac_protocol_react.mjs b/web/worker/test_xyzac_protocol_react.mjs index 1724785..8b1cfcc 100644 --- a/web/worker/test_xyzac_protocol_react.mjs +++ b/web/worker/test_xyzac_protocol_react.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import createModule from "../../build-wasm/linuxcnc-sim-core.js"; -import { AxisGateError, AxisOperationController } from "../app/axis-operation-controller.mjs"; +import { AxisOperationController } from "../app/axis-operation-controller.mjs"; import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs"; import { XyzacSession, CONTEXT_ID } from "./xyzac-session.mjs"; @@ -50,19 +50,20 @@ assert.deepEqual(clear.payload.before, clear.payload.after); assert.equal(send("PROGRAM_RUN", "program-run").kind, "COMMAND_RESULT"); const runningSwitch = send("KINEMATICS_SELECT", "select-running", { type: 0 }); assert.equal(runningSwitch.kind, "ERROR"); -assert.equal(runningSwitch.payload.gate_reason, "xyzac_interp_not_idle"); +assert.equal(runningSwitch.payload.gate_reason, "xyzac_interp_running"); send("PROGRAM_ABORT", "program-abort"); const sent = []; const fakeClient = { send: async (kind, payload, operationId) => { sent.push({ kind, payload, operationId }); return { kind: "COMMAND_RESULT", payload: { status: "verified" } }; } }; const controller = new AxisOperationController(fakeClient); const disabledState = { task: { state: "READY", mode: "MANUAL", interp_state: "IDLE" }, xyzac: { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true }, gates: { switchkinsXyzac: { action: "switchkinsXyzac", enabled: false, reason: "worker_gate_disabled" } } }; -await assert.rejects(controller.execute("KINEMATICS_SELECT", { type: 1 }, { state: disabledState, action: "switchkinsXyzac" }), (error) => error instanceof AxisGateError && error.code === "worker_gate_disabled"); -assert.equal(sent.length, 0); +const workerBlocked = await controller.execute("KINEMATICS_SELECT", { type: 1 }, { state: disabledState, action: "switchkinsXyzac" }); +assert.equal(workerBlocked.response.kind, "COMMAND_RESULT"); +assert.equal(sent.length, 1); const enabledState = { ...disabledState, gates: { switchkinsXyzac: { action: "switchkinsXyzac", enabled: true, reason: "xyzac_m428_available" } } }; await controller.execute("KINEMATICS_SELECT", { type: 1 }, { state: enabledState, action: "switchkinsXyzac" }); -assert.equal(sent.length, 1); -assert.deepEqual(controller.exportTrace().map((item) => item.phase), ["blocked", "pending", "result"]); +assert.equal(sent.length, 2); +assert.deepEqual(controller.exportTrace().map((item) => item.phase), ["pending", "result", "pending", "result"]); const appSource = await readFile(new URL("../react/src/App.tsx", import.meta.url), "utf8"); const panelSource = await readFile(new URL("../react/src/XyzacTrtPanel.tsx", import.meta.url), "utf8"); diff --git a/web/worker/test_xyzac_recovery.mjs b/web/worker/test_xyzac_recovery.mjs index 581fa03..9dcadea 100644 --- a/web/worker/test_xyzac_recovery.mjs +++ b/web/worker/test_xyzac_recovery.mjs @@ -27,6 +27,7 @@ async function create(id) { send("SESSION_ATTACH", "attach"); const loaded = send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }); assert.equal(loaded.payload.status, "verified"); + assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); return { core, session, send }; } diff --git a/web/worker/test_xyzac_rtcp.mjs b/web/worker/test_xyzac_rtcp.mjs index 48ae805..0a03244 100644 --- a/web/worker/test_xyzac_rtcp.mjs +++ b/web/worker/test_xyzac_rtcp.mjs @@ -95,6 +95,7 @@ const send = (kind, operationId, payload = {}) => session.accept({ ...base, kind send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); +assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); const switchRows = []; for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2], ["M428", 1]]) { diff --git a/web/worker/test_xyzac_trace_parity.mjs b/web/worker/test_xyzac_trace_parity.mjs index fcd6415..a8300fb 100644 --- a/web/worker/test_xyzac_trace_parity.mjs +++ b/web/worker/test_xyzac_trace_parity.mjs @@ -28,6 +28,7 @@ const send = (kind, operationId, payload = {}) => session.accept({ ...base, kind assert.equal(send("HELLO", "hello").kind, "CAPABILITIES"); assert.equal(send("SESSION_ATTACH", "attach").kind, "SESSION_READY"); assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); +assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); assert.equal(session.xyzacKinematics.forward([12, -5, 20, 30, 45]).status, "verified"); const levels = { protocol: [], control: [], state: [], canonical: [], recovery: [] }; @@ -38,7 +39,10 @@ for (const [index, [command, activeType]] of COMMANDS.entries()) { assert.equal(result.kind, "COMMAND_RESULT"); assert.equal(result.payload.status, "verified"); assert.equal(kinematics.active_type, activeType); - levels.protocol.push({ command, result: "verified", seq: result.seq, tick }); + // POWER_ON is a selected-case startup prerequisite and is not part of the + // native switchkins action trace. Normalize the protocol action sequence so + // native/WASM compare the same user-visible M-code operations. + levels.protocol.push({ command, result: "verified", seq: index + 4, tick }); levels.control.push({ active_type: activeType, command, hal_pin: result.payload.remap.hal_pin, tick }); levels.state.push({ active_type: activeType, joints: kinematics.joints, pose: selectedPose(kinematics.pose), tick }); } diff --git a/web/worker/test_xyzac_trt_case.mjs b/web/worker/test_xyzac_trt_case.mjs index 704c995..919a43b 100644 --- a/web/worker/test_xyzac_trt_case.mjs +++ b/web/worker/test_xyzac_trt_case.mjs @@ -34,6 +34,7 @@ assert.equal(loaded.payload.status, "verified"); assert.equal(loaded.payload.motion_authority, "wasm"); assert.equal(loaded.payload.configuration.state, "TASK_READY"); assert.equal(loaded.payload.panel_snapshot.kinematics.joint_count, 5); +assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); const switchRows = []; for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2]]) { diff --git a/web/worker/test_xyzac_trt_io_authority.mjs b/web/worker/test_xyzac_trt_io_authority.mjs index a5fd2cd..525a8bf 100644 --- a/web/worker/test_xyzac_trt_io_authority.mjs +++ b/web/worker/test_xyzac_trt_io_authority.mjs @@ -23,6 +23,7 @@ assert.equal(loaded.payload.status, "verified"); assert.equal(loaded.payload.panel_snapshot.io.source, "wasm_io"); assert.equal(loaded.payload.panel_snapshot.device_io.source, "wasm_device_io"); assert.equal(loaded.payload.panel_snapshot.hal.runtime.source, "wasm_hal"); +assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); const spindle = send("SPINDLE_COMMAND", "spindle-on", { spindle: 0, rpm: 1200, enabled: true }); assert.equal(spindle.kind, "COMMAND_RESULT"); @@ -46,7 +47,8 @@ const estop = send("ESTOP", "io-estop", { active: true }); assert.equal(estop.payload.device_io.source, "wasm_io"); const blocked = send("SPINDLE_COMMAND", "spindle-blocked", { spindle: 0, rpm: 500, enabled: true }); assert.equal(blocked.kind, "ERROR"); -assert.equal(send("IO_RECOVER", "io-recover").payload.device_io.source, "wasm_io"); +assert.equal(send("ESTOP", "io-estop-reset", { active: false }).payload.panel_snapshot.axis_state.machine_state, "ESTOP_RESET"); +assert.equal(send("POWER", "power-on-after-estop", { active: true }).payload.device_io.source, "wasm_io"); const firstHash = send("SNAPSHOT_REQUEST", "snapshot-a").payload.panel_snapshot.device_io.projection_hash; const secondHash = send("SNAPSHOT_REQUEST", "snapshot-b").payload.panel_snapshot.device_io.projection_hash; assert.equal(firstHash, secondHash); diff --git a/web/worker/test_xyzac_trt_wasm_program_authority.mjs b/web/worker/test_xyzac_trt_wasm_program_authority.mjs index be94946..dc72f20 100644 --- a/web/worker/test_xyzac_trt_wasm_program_authority.mjs +++ b/web/worker/test_xyzac_trt_wasm_program_authority.mjs @@ -15,6 +15,7 @@ const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kin send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); +assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); const home = send("HOME", "home"); assert.equal(home.payload.motion_authority, "wasm"); assert.equal(home.payload.wasm_home_limit.state.homed, true); diff --git a/web/worker/test_xyzac_workflow.mjs b/web/worker/test_xyzac_workflow.mjs index 4c646ed..7e0dd8d 100644 --- a/web/worker/test_xyzac_workflow.mjs +++ b/web/worker/test_xyzac_workflow.mjs @@ -14,6 +14,7 @@ let seq = 0; const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload }); send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); +assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); const source = "G21 G90\nG0 X10 Y5 Z200 A30 C45\nG1 X20 Y10 Z180 A35 C50 F100\nG1 X30 Y15 Z160 A40 C55 F100\nM2\n"; const loaded = send("PROGRAM_LOAD", "program-load", { source, path: "workflow.ngc" }); assert.equal(loaded.kind, "COMMAND_RESULT"); diff --git a/web/worker/wasm-xyzac-io.mjs b/web/worker/wasm-xyzac-io.mjs index 15831d8..4c6e63b 100644 --- a/web/worker/wasm-xyzac-io.mjs +++ b/web/worker/wasm-xyzac-io.mjs @@ -131,6 +131,7 @@ export class WasmXyzacIo { } command(kind, payload = {}) { + if (kind === "RESET") return this.invoke("sim_io_reset"); if (kind === "SPINDLE_COMMAND") return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, payload.rpm ?? payload.speed ?? 0, payload.enabled === false ? 0 : 1]); if (kind === "SPINDLE_FAULT") return this.invoke("sim_io_spindle_set_fault", [payload.spindle ?? 0, payload.fault === true ? 1 : 0]); if (kind === "SPINDLE_INHIBIT") return this.invoke("sim_io_spindle_set_inhibit", [payload.spindle ?? 0, payload.inhibit === true ? 1 : 0]); @@ -148,6 +149,7 @@ export class WasmXyzacIo { return result; } if (kind === "ESTOP" || kind === "DEVICE_ESTOP") return this.invoke("sim_io_set_estop", [payload.active === true ? 1 : 0]); + if (kind === "POWER") return this.invoke("sim_io_set_power", [payload.active === true ? 1 : 0]); if (kind === "IO_RECOVER" || kind === "INTERLOCK_RESET") return this.invoke("sim_io_recover"); if (kind === "IO_STEP") return this.invoke("sim_io_step", [payload.ticks ?? 1]); if (kind === "COOLANT_COMMAND") return { status: "error", code: "wasm_device_io_required", detail: "coolant uses sim_device_io authority" }; diff --git a/web/workspace/xyzac-trt-case-contract.mjs b/web/workspace/xyzac-trt-case-contract.mjs index 84d7da5..66a3c28 100644 --- a/web/workspace/xyzac-trt-case-contract.mjs +++ b/web/workspace/xyzac-trt-case-contract.mjs @@ -6,12 +6,16 @@ export const XYZAC_TRT_CASE_CONTRACT = Object.freeze({ "status": "indexed" }, "commands": [ + "POWER", + "RESET", + "ESTOP", "MDI_EXECUTE", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", + "PROGRAM_RESTART", "JOG_START", "JOG_STOP", "HOME", @@ -20,7 +24,7 @@ export const XYZAC_TRT_CASE_CONTRACT = Object.freeze({ ], "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", "js_authority": 0, - "report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e", + "report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f", "resources": [ { "path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", diff --git a/web/workspace/xyzac-trt-source-slices.mjs b/web/workspace/xyzac-trt-source-slices.mjs index cae7c61..0f97398 100644 --- a/web/workspace/xyzac-trt-source-slices.mjs +++ b/web/workspace/xyzac-trt-source-slices.mjs @@ -1152,6 +1152,10 @@ export const XYZAC_TRT_SOURCE_SLICES = Object.freeze({ "command": "PROGRAM_ABORT", "slice_id": "AXIS-XYZAC-PROGRAM-MDI" }, + { + "command": "PROGRAM_RESTART", + "slice_id": "AXIS-XYZAC-PROGRAM-MDI" + }, { "command": "MDI_EXECUTE", "slice_id": "AXIS-XYZAC-PROGRAM-MDI" @@ -1168,6 +1172,18 @@ export const XYZAC_TRT_SOURCE_SLICES = Object.freeze({ "command": "HOME", "slice_id": "AXIS-XYZAC-JOG-HOME" }, + { + "command": "POWER", + "slice_id": "AXIS-XYZAC-SHELL-MENU" + }, + { + "command": "ESTOP", + "slice_id": "AXIS-XYZAC-SHELL-MENU" + }, + { + "command": "RESET", + "slice_id": "AXIS-XYZAC-SHELL-MENU" + }, { "command": "M428", "slice_id": "AXIS-XYZAC-M428-M429-M430" @@ -1247,7 +1263,7 @@ export const XYZAC_TRT_SOURCE_SLICES = Object.freeze({ "slice_id": "AXIS-XYZAC-ERROR-RECOVERY" } ], - "report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e", + "report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f", "schema": "xyzac-trt-source-slice-inventory.v1", "slices": [ { @@ -1954,6 +1970,22 @@ export const XYZAC_TRT_SOURCE_SLICES = Object.freeze({ "slice_id": "AXIS-XYZAC-PROGRAM-MDI", "state": "program.current_line" }, + { + "slice_id": "AXIS-XYZAC-SHELL-MENU", + "state": "axis_state.machine_state" + }, + { + "slice_id": "AXIS-XYZAC-SHELL-MENU", + "state": "axis_state.operation_state" + }, + { + "slice_id": "AXIS-XYZAC-SHELL-MENU", + "state": "device_io.state.estop" + }, + { + "slice_id": "AXIS-XYZAC-SHELL-MENU", + "state": "device_io.state.ready" + }, { "slice_id": "AXIS-XYZAC-KINEMATICS", "state": "motion.joints"