Enforce LinuxCNC WASM XYZAC command authority

This commit is contained in:
2026-08-28 13:04:47 +08:00
parent 1a7995fa9b
commit 78d884bc67
43 changed files with 567 additions and 186 deletions

View File

@@ -1,5 +1,30 @@
{ {
"authority": "linuxcnc-wasm-worker", "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, "candidate_eligible": true,
"case": { "case": {
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", "path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
@@ -10,16 +35,37 @@
"SESSION_ATTACH", "SESSION_ATTACH",
"LOAD_WORKSPACE", "LOAD_WORKSPACE",
"PROGRAM_LOAD", "PROGRAM_LOAD",
"POWER_ON",
"HOME_ALL",
"PROGRAM_RUN", "PROGRAM_RUN",
"PROGRAM_PAUSE", "PROGRAM_PAUSE",
"PROGRAM_RESUME",
"PROGRAM_ABORT", "PROGRAM_ABORT",
"ESTOP", "ESTOP",
"IO_RECOVER", "ESTOP_RESET",
"POWER_ON",
"SNAPSHOT_REQUEST" "SNAPSHOT_REQUEST"
], ],
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"failure_samples": [], "failure_samples": [],
"gate_scenarios": { "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": { "highlight_missing": {
"reason": "axis_highlight_missing", "reason": "axis_highlight_missing",
"status": "verified" "status": "verified"
@@ -51,10 +97,10 @@
} }
}, },
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "ae3ebd07146e75e1fa50c90d37e56ca195e2c6b82069718aedf9b63338ad2219", "report_sha256": "fd24448f6f46d484a2e27f711383db6d0695feafb73c18365082975ed33c8f97",
"schema": "xyzac-gate-matrix-rtcp-03.v1", "schema": "xyzac-gate-matrix-rtcp-03.v1",
"scope": "xyzac_only", "scope": "xyzac_only",
"state_hash": "fnv1a64:9abaa158693c6300", "state_hash": "fnv1a64:7b5f8d0ed1c175dd",
"status": "verified", "status": "verified",
"task_id": "RTCP-03", "task_id": "RTCP-03",
"tests": [ "tests": [
@@ -63,7 +109,16 @@
"node", "node",
"web/worker/test_xyzac_gate_matrix.mjs" "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, "returncode": 0,
"status": "verified" "status": "verified"
} }

View File

@@ -54,11 +54,11 @@
"vismach-clear" "vismach-clear"
], ],
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "75c368880a881da1230d3e66ec5265ad693053dc9eecce462d92c84f5a025872", "report_sha256": "37026591b5875615e8a68374672fe7a7b433ef53504a3d52bc922a735f6fd140",
"schema": "xyzac-hal-tool-alarm-rtcp-07.v1", "schema": "xyzac-hal-tool-alarm-rtcp-07.v1",
"scope": "xyzac_only", "scope": "xyzac_only",
"spindle_rpm": 1200, "spindle_rpm": 1200,
"state_hash": "fnv1a64:184c713588cb828d", "state_hash": "fnv1a64:198fc8da045d3e0f",
"status": "verified", "status": "verified",
"task_id": "RTCP-07", "task_id": "RTCP-07",
"tests": [ "tests": [
@@ -67,7 +67,7 @@
"node", "node",
"web/worker/test_xyzac_hal_tool_alarm.mjs" "web/worker/test_xyzac_hal_tool_alarm.mjs"
], ],
"output_sha256": "02c7e7eace9b8be9d3a3144f259fdfbf870569df26ba01c22b4146e1bc3bc9e2", "output_sha256": "831953b1e6712c0187d8029facbc07cacf0a1290e17165019801a2000b8b4e40",
"returncode": 0, "returncode": 0,
"status": "verified" "status": "verified"
} }

View File

@@ -61,10 +61,10 @@
}, },
"mdi": "verified", "mdi": "verified",
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "0660bca7d4ae6d68e584346606911c98aaacb14922f1b5fc9a7a44d782936be6", "report_sha256": "a4ca2d5637e68c387e2ca5be460a2c9340d6b7198715cb1aa95d90055c623058",
"schema": "xyzac-manual-control-rtcp-06.v1", "schema": "xyzac-manual-control-rtcp-06.v1",
"scope": "xyzac_only", "scope": "xyzac_only",
"state_hash": "fnv1a64:fac98a76f1a41fbc", "state_hash": "fnv1a64:8e9acc7cf9c8cec1",
"status": "verified", "status": "verified",
"task_id": "RTCP-06", "task_id": "RTCP-06",
"tests": [ "tests": [
@@ -73,7 +73,7 @@
"node", "node",
"web/worker/test_xyzac_manual_control.mjs" "web/worker/test_xyzac_manual_control.mjs"
], ],
"output_sha256": "930fe1e249389de9b0c0460f59c36e46820e3e006dc4675ffdf2528891731066", "output_sha256": "c484daae09f135544f0a55376c79377e251e416651ab7d338e8d4d20f0d20768",
"returncode": 0, "returncode": 0,
"status": "verified" "status": "verified"
} }

View File

@@ -37,12 +37,12 @@
"replay_same_operation": "verified" "replay_same_operation": "verified"
}, },
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "bde1a054c9a633a62595ec19e5097d73499ca8fbcf07c817c01265dbf643f04a", "report_sha256": "f44c782f09851b87a90e90583657a95c7ea30bffc5fd254fa8e3e16f87ed985e",
"schema": "xyzac-program-preview-rtcp-04.v1", "schema": "xyzac-program-preview-rtcp-04.v1",
"scope": "xyzac_only", "scope": "xyzac_only",
"segment_count": 2, "segment_count": 2,
"source_hash": "fnv1a64:b7f4678312207b31", "source_hash": "fnv1a64:b7f4678312207b31",
"state_hash": "fnv1a64:fda625a2e7b00ac7", "state_hash": "fnv1a64:dadc751fbbb70835",
"status": "verified", "status": "verified",
"task_id": "RTCP-04", "task_id": "RTCP-04",
"tests": [ "tests": [
@@ -51,7 +51,7 @@
"node", "node",
"web/worker/test_xyzac_program_preview.mjs" "web/worker/test_xyzac_program_preview.mjs"
], ],
"output_sha256": "79dd24d44e07958fd86484197c4502d3bdadd6d6971b7e74727b594d1d6b630f", "output_sha256": "96e5c2c6203d26162a49206d5e9416b532bf880065a4a3418c863ea0c32bdb28",
"returncode": 0, "returncode": 0,
"status": "verified" "status": "verified"
} }

View File

@@ -56,16 +56,18 @@
"failure_samples": [], "failure_samples": [],
"gates": { "gates": {
"controller": [ "controller": [
"blocked", "pending",
"result",
"pending", "pending",
"result" "result"
], ],
"invalid": "xyzac_kinematics_type_invalid", "invalid": "xyzac_kinematics_type_invalid",
"running": "xyzac_interp_not_idle" "running": "xyzac_interp_running"
}, },
"known_blockers": [], "known_blockers": [],
"operation_phases": [ "operation_phases": [
"blocked", "pending",
"result",
"pending", "pending",
"result" "result"
], ],
@@ -80,13 +82,13 @@
}, },
{ {
"path": "web/worker/test_xyzac_protocol_react.mjs", "path": "web/worker/test_xyzac_protocol_react.mjs",
"sha256": "69d942dfdcb7bc0d7fb26d6708220a47af13aa84acdcf490780980f6de0cc03d" "sha256": "daa74b5091de4579b7ce5eb283b802e226e275bf51209af339e4cf6334fd6b5b"
} }
], ],
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "a8ee87b6cb3ccbd683e9888c85a063dabbe17fafdd2708636bd48be592b7b520", "report_sha256": "f25c81c8aa94762e563bdfacceef0bf8b211ca632b2eed5556c88fd408b1c5b5",
"schema": "xyzac-trt-protocol-react-report.v1", "schema": "xyzac-trt-protocol-react-report.v1",
"state_hash": "fnv1a64:2de42fa3ac15497a", "state_hash": "fnv1a64:4cb77e59ba75a666",
"status": "verified", "status": "verified",
"switchkins": [ "switchkins": [
{ {
@@ -117,7 +119,7 @@
"node", "node",
"web/worker/test_xyzac_protocol_react.mjs" "web/worker/test_xyzac_protocol_react.mjs"
], ],
"output_sha256": "cf72a973132e3e79c155fcb0feb58f07259c082970287127d786e94a92cc4327", "output_sha256": "862f3c71bccb137c1551a5bfb4ed85014a7cbb2a2506f265193bbe29944c36a1",
"returncode": 0 "returncode": 0
}, },
{ {

View File

@@ -24,8 +24,8 @@
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"endpoint": { "endpoint": {
"at_checkpoint_equal": true, "at_checkpoint_equal": true,
"continuous_hash": "fnv1a64:a4dee4490d19b4ec", "continuous_hash": "fnv1a64:b105b6fc1bcc8a70",
"restored_hash": "fnv1a64:ca9e3481aaa28f6d" "restored_hash": "fnv1a64:857caa278d03c259"
}, },
"exclusions": [ "exclusions": [
"xyzbc", "xyzbc",
@@ -61,11 +61,11 @@
"outputs": [ "outputs": [
{ {
"path": "web/worker/test_xyzac_recovery.mjs", "path": "web/worker/test_xyzac_recovery.mjs",
"sha256": "d8f02b0ab5b7c9fc5ef30a3a67f7896071ecf9bbaa7d2f3614c031d118e655e7" "sha256": "d25dc8f5de748b37ca231ee7e5a0df049878511f4c0fec98061208730f57b145"
}, },
{ {
"path": "web/worker/sim-session.mjs", "path": "web/worker/sim-session.mjs",
"sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3" "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1"
}, },
{ {
"path": "web/worker/wasm-program-runtime.mjs", "path": "web/worker/wasm-program-runtime.mjs",
@@ -73,7 +73,7 @@
} }
], ],
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "6c38c4e541868934f7a8231d36314b7e736daeb6d685f1dad48809fa49ef65ae", "report_sha256": "be2d59e86990e26858d11e3220cfa63d4b8f5d84f8b81f1067d89b645f78a9ae",
"required_checkpoint_fields": [ "required_checkpoint_fields": [
"context_id", "context_id",
"simulation_tick", "simulation_tick",
@@ -95,7 +95,7 @@
"node", "node",
"web/worker/test_xyzac_recovery.mjs" "web/worker/test_xyzac_recovery.mjs"
], ],
"output_sha256": "d13944363157b9b97a440adb6135ec8977332f31778fb7ae40383e52ba2f1f35", "output_sha256": "3649f6971bb8ed8857dd5cab886834485a3d82c8dec71365853481c0f12a46c9",
"returncode": 0 "returncode": 0
} }
] ]

View File

@@ -1160,6 +1160,10 @@
"command": "PROGRAM_ABORT", "command": "PROGRAM_ABORT",
"slice_id": "AXIS-XYZAC-PROGRAM-MDI" "slice_id": "AXIS-XYZAC-PROGRAM-MDI"
}, },
{
"command": "PROGRAM_RESTART",
"slice_id": "AXIS-XYZAC-PROGRAM-MDI"
},
{ {
"command": "MDI_EXECUTE", "command": "MDI_EXECUTE",
"slice_id": "AXIS-XYZAC-PROGRAM-MDI" "slice_id": "AXIS-XYZAC-PROGRAM-MDI"
@@ -1176,6 +1180,18 @@
"command": "HOME", "command": "HOME",
"slice_id": "AXIS-XYZAC-JOG-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", "command": "M428",
"slice_id": "AXIS-XYZAC-M428-M429-M430" "slice_id": "AXIS-XYZAC-M428-M429-M430"
@@ -1772,7 +1788,7 @@
} }
], ],
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e", "report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f",
"runtime_evidence": { "runtime_evidence": {
"native": { "native": {
"command": [ "command": [
@@ -2622,6 +2638,22 @@
"slice_id": "AXIS-XYZAC-PROGRAM-MDI", "slice_id": "AXIS-XYZAC-PROGRAM-MDI",
"state": "program.current_line" "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", "slice_id": "AXIS-XYZAC-KINEMATICS",
"state": "motion.joints" "state": "motion.joints"

View File

@@ -74,7 +74,7 @@
}, },
{ {
"path": "web/worker/sim-session.mjs", "path": "web/worker/sim-session.mjs",
"sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3" "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1"
}, },
{ {
"path": "web/worker/test_xyzac_exact_preview.mjs", "path": "web/worker/test_xyzac_exact_preview.mjs",
@@ -90,7 +90,7 @@
}, },
{ {
"path": "web/worker/wasm-xyzac-io.mjs", "path": "web/worker/wasm-xyzac-io.mjs",
"sha256": "fdb92e8819e38010aa7a1b8f5f58473a930644fbfeba3d7d5a5c23e075ee92ca" "sha256": "692dfb4c869130fd4a0ee78a500a54817e326991f2a5ac23a91059136d41eabd"
}, },
{ {
"path": "web/worker/wasm-xyzac-device-io.mjs", "path": "web/worker/wasm-xyzac-device-io.mjs",
@@ -98,11 +98,11 @@
}, },
{ {
"path": "web/worker/test_xyzac_machine_sequence.mjs", "path": "web/worker/test_xyzac_machine_sequence.mjs",
"sha256": "9dcd89d54596fe4b339d5aca4e028a0c44e173a8e646f1254defc706bd3fcc96" "sha256": "386fa1e72f194ba0c93ad0083474da48c8f526b627bd22ef398e3bf5b1ad1552"
}, },
{ {
"path": "web/react/src/App.tsx", "path": "web/react/src/App.tsx",
"sha256": "8cbec29f00c44e18a01ac92734e4f8b64764cc04db4cec48ce9c28693bd2bbef" "sha256": "31692dd2df5d9a1a26503e78627c5d10524426c975d8ffb0dfbd3f7cf5c0f8af"
}, },
{ {
"path": "web/react/src/styles.css", "path": "web/react/src/styles.css",
@@ -289,7 +289,7 @@
"schema": "xyzac-exact-tool-preview.v1", "schema": "xyzac-exact-tool-preview.v1",
"segment_count": 29, "segment_count": 29,
"source_hash": "fnv1a64:522e219683194ad2", "source_hash": "fnv1a64:522e219683194ad2",
"state_hash": "fnv1a64:c7e53444cce4a142", "state_hash": "fnv1a64:1b46405fa36a5abe",
"status": "verified", "status": "verified",
"visual_segments": 29 "visual_segments": 29
}, },
@@ -344,14 +344,13 @@
"schema": "xyzac-machine-sequence.v1", "schema": "xyzac-machine-sequence.v1",
"sequence": [ "sequence": [
"ESTOP", "ESTOP",
"ESTOP_RELEASE", "ESTOP_RESET",
"IO_RECOVER",
"POWER", "POWER",
"HOME_ALL", "HOME_ALL",
"PROGRAM_RUN", "PROGRAM_RUN",
"SIM_STEP" "SIM_STEP"
], ],
"state_hash": "fnv1a64:c8f6326c97e651f9", "state_hash": "fnv1a64:3fe83d3f4768868c",
"status": "verified" "status": "verified"
}, },
"static": { "static": {
@@ -384,7 +383,7 @@
} }
}, },
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "94b18227ec72689934629a5b428cf6a9f16500d30b200d4b36a10d03c84c56b9", "report_sha256": "0127822355f55c04898dcb0f9333d291d5fa8fbb116d7199cbdf02694b97fe87",
"schema": "xyzac-trt-tool-preview-report.v1", "schema": "xyzac-trt-tool-preview-report.v1",
"status": "verified", "status": "verified",
"task_id": "RTCP-10-TOOL-PREVIEW", "task_id": "RTCP-10-TOOL-PREVIEW",
@@ -403,7 +402,7 @@
"node", "node",
"web/worker/test_xyzac_exact_preview.mjs" "web/worker/test_xyzac_exact_preview.mjs"
], ],
"output_sha256": "60cec025efd2f29de45fb6139f0929f329a505bd331212e6b01ab5fbb5f8fca8", "output_sha256": "625aaa1f95578666d70390a782745ff1e74862f6ef11bbf2911b1870a2b864dd",
"returncode": 0 "returncode": 0
}, },
{ {
@@ -411,7 +410,7 @@
"node", "node",
"web/worker/test_xyzac_machine_sequence.mjs" "web/worker/test_xyzac_machine_sequence.mjs"
], ],
"output_sha256": "2a72e723603080bb3eff8169b8be5352fa46d6b0c8ae04dbeeb2e5e1371e1c47", "output_sha256": "a361d9e4f695d048093e7ff4b7b5e6688534071c5b3811a40bbe69e00d114133",
"returncode": 0 "returncode": 0
} }
], ],

View File

@@ -46,7 +46,7 @@
}, },
{ {
"path": "web/worker/test_xyzac_trace_parity.mjs", "path": "web/worker/test_xyzac_trace_parity.mjs",
"sha256": "f5c6a7cd3e8b7f72bb1b25b5c368c0e52d494b9c811022d34435e668612ec4ce" "sha256": "209254396d3ec828936ec5c2ad34de9044706f7d0309fd5ad4cacae347f15247"
}, },
{ {
"path": "coverage/xyzac-trt-rtcp.report.json", "path": "coverage/xyzac-trt-rtcp.report.json",
@@ -130,7 +130,7 @@
"mutation": "state[1].active_type=99" "mutation": "state[1].active_type=99"
}, },
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "39c63aa9af38fd98e49fd68ec2401ee8715fcafef98d47c070a47ab6f6dc0a48", "report_sha256": "234a362308c9caae71955a606d1779f45ce4fe170f476e49a6c5a482dd4351d5",
"schema": "xyzac-trt-trace-parity-report.v1", "schema": "xyzac-trt-trace-parity-report.v1",
"status": "verified", "status": "verified",
"task_id": "PARITY-00", "task_id": "PARITY-00",

View File

@@ -65,7 +65,7 @@
"case": { "case": {
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", "path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
"sha256": "f985970db293b70c055e4c41fc316d0c16037b5dc7ce07490e18f554e851f811", "sha256": "f985970db293b70c055e4c41fc316d0c16037b5dc7ce07490e18f554e851f811",
"source_slice_report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e" "source_slice_report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f"
}, },
"checks": { "checks": {
"context_match": { "context_match": {
@@ -94,7 +94,7 @@
"status": "verified" "status": "verified"
}, },
"source_slice_dependency": { "source_slice_dependency": {
"report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e", "report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f",
"status": "verified" "status": "verified"
}, },
"wasm_worker_bootstrap": { "wasm_worker_bootstrap": {
@@ -128,10 +128,10 @@
"source_slice": "AXIS-XYZAC-PROGRAM-MDI", "source_slice": "AXIS-XYZAC-PROGRAM-MDI",
"sources": [ "sources": [
{ {
"lines": "107-739", "lines": "107-833",
"path": "web/worker/sim-session.mjs", "path": "web/worker/sim-session.mjs",
"role": "session-authority-routing", "role": "session-authority-routing",
"sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3", "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1",
"status": "resolved", "status": "resolved",
"symbol": "new ProgramRunner" "symbol": "new ProgramRunner"
}, },
@@ -161,10 +161,10 @@
"symbol": "export class MotionState" "symbol": "export class MotionState"
}, },
{ {
"lines": "107-739", "lines": "107-833",
"path": "web/worker/sim-session.mjs", "path": "web/worker/sim-session.mjs",
"role": "session-authority-routing", "role": "session-authority-routing",
"sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3", "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1",
"status": "resolved", "status": "resolved",
"symbol": "new ProgramRunner" "symbol": "new ProgramRunner"
} }
@@ -186,10 +186,10 @@
"symbol": "export class AuthoritativeRuntime" "symbol": "export class AuthoritativeRuntime"
}, },
{ {
"lines": "107-739", "lines": "107-833",
"path": "web/worker/sim-session.mjs", "path": "web/worker/sim-session.mjs",
"role": "session-authority-routing", "role": "session-authority-routing",
"sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3", "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1",
"status": "resolved", "status": "resolved",
"symbol": "new ProgramRunner" "symbol": "new ProgramRunner"
} }
@@ -205,7 +205,7 @@
"recovery": "Chromium SIGKILL/profile-reopen, corruption detection and Firefox OPFS round-trip are verified" "recovery": "Chromium SIGKILL/profile-reopen, corruption detection and Firefox OPFS round-trip are verified"
}, },
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "f73755bf9d695ee02de03bae8ab65cfc55e593b2758634420e83b6d3bef227e2", "report_sha256": "6bf29284946b658ca58c76c91b2dbad3c33f8af7afb74b91aecb5a2842f3b650",
"scans": { "scans": {
"js_gcode_matches": [ "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]);" "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", "path": "web/worker/sim-session.mjs",
"text": "const result = this.xyzacProgram.executeBlock(block);" "text": "const result = this.xyzacProgram.executeBlock(block);"
}, },
{ {
"line": 1424, "line": 1545,
"path": "web/worker/sim-session.mjs", "path": "web/worker/sim-session.mjs",
"text": "const result = this.program.executeBlock(block);" "text": "const result = this.program.executeBlock(block);"
}, },
@@ -292,7 +292,7 @@
"opfs_writer_matches": [ "opfs_writer_matches": [
{ {
"line": 1, "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" "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": [ "files": [
{ {
"path": "web/react/src/App.tsx", "path": "web/react/src/App.tsx",
"sha256": "8cbec29f00c44e18a01ac92734e4f8b64764cc04db4cec48ce9c28693bd2bbef" "sha256": "31692dd2df5d9a1a26503e78627c5d10524426c975d8ffb0dfbd3f7cf5c0f8af"
}, },
{ {
"path": "web/react/src/ThreeMachineView.tsx", "path": "web/react/src/ThreeMachineView.tsx",
@@ -1508,7 +1508,7 @@
}, },
{ {
"path": "web/react/src/XyzacToolPreview.tsx", "path": "web/react/src/XyzacToolPreview.tsx",
"sha256": "d29cd1355d7dd178dce01e49eb321425b0b08d2291a6eaa00c86b546d8e3bba1" "sha256": "caca009f96336847c8d9b8d5823c3d7de86ed77c37cae2fe9f5f3e4d30132d57"
}, },
{ {
"path": "web/react/src/XyzacTrtPanel.tsx", "path": "web/react/src/XyzacTrtPanel.tsx",
@@ -1524,7 +1524,7 @@
}, },
{ {
"path": "web/react/src/legacy.d.ts", "path": "web/react/src/legacy.d.ts",
"sha256": "a29f18bf009c955a5463a58856d4ab9902fc5ce032d643f5363ca3bf28247174" "sha256": "b67ced36978375d312d5b10a26443e50c768e58769913b1b5b4887538c4f22f3"
}, },
{ {
"path": "web/react/src/main.tsx", "path": "web/react/src/main.tsx",
@@ -1576,7 +1576,7 @@
"react_opfs_matches": [], "react_opfs_matches": [],
"react_wasm_matches": [], "react_wasm_matches": [],
"worker": { "worker": {
"file_count": 124, "file_count": 125,
"files": [ "files": [
{ {
"path": "web/worker/authoritative-runtime.mjs", "path": "web/worker/authoritative-runtime.mjs",
@@ -1584,11 +1584,11 @@
}, },
{ {
"path": "web/worker/axis-state.mjs", "path": "web/worker/axis-state.mjs",
"sha256": "01bbbcd053262f43413fe76a540b0c453e0a2c3e704ea44ec8b30ce50bb66b83" "sha256": "77c494b2327dcae26b74af4873c18d5a68aef2e5b1fa500ef2b4b0a523a76c37"
}, },
{ {
"path": "web/worker/capabilities.mjs", "path": "web/worker/capabilities.mjs",
"sha256": "39b5af3627e068d7cc68fc0bcabfdc33c79f4f68b41e4488481ef5c4791d9993" "sha256": "88551cc6940efea9186fb86d9cf68ee66f45538b3667730c8e9c514202ec035b"
}, },
{ {
"path": "web/worker/contracts.mjs", "path": "web/worker/contracts.mjs",
@@ -1652,7 +1652,7 @@
}, },
{ {
"path": "web/worker/sim-session.mjs", "path": "web/worker/sim-session.mjs",
"sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3" "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1"
}, },
{ {
"path": "web/worker/sim-task.mjs", "path": "web/worker/sim-task.mjs",
@@ -1966,25 +1966,29 @@
"path": "web/worker/test_workspace_session_integration.mjs", "path": "web/worker/test_workspace_session_integration.mjs",
"sha256": "38f49df83c3ac24ef564c8cd5ed69b1a5882fa47c464946cb1945cac8ac163bf" "sha256": "38f49df83c3ac24ef564c8cd5ed69b1a5882fa47c464946cb1945cac8ac163bf"
}, },
{
"path": "web/worker/test_xyzac_button_constraints.mjs",
"sha256": "e5206c37f959d0214f83892dcd2b57abf6d16335ac1f6eedb36e9bbad5a45d5f"
},
{ {
"path": "web/worker/test_xyzac_exact_preview.mjs", "path": "web/worker/test_xyzac_exact_preview.mjs",
"sha256": "dc5cc312aab46003e12a4bfc26d695b05d78843e61808c550cb4a5925338687b" "sha256": "dc5cc312aab46003e12a4bfc26d695b05d78843e61808c550cb4a5925338687b"
}, },
{ {
"path": "web/worker/test_xyzac_gate_matrix.mjs", "path": "web/worker/test_xyzac_gate_matrix.mjs",
"sha256": "07e4f05f3b7bf470fb87ecea077587e7584928d28fe7e28932f8a649c960525d" "sha256": "176da9a86c17cef2fb1f17bb65c5854af107524ac26871fa830e17e62fc022cf"
}, },
{ {
"path": "web/worker/test_xyzac_hal_tool_alarm.mjs", "path": "web/worker/test_xyzac_hal_tool_alarm.mjs",
"sha256": "731ff64fc5b9abd70c6ab6649a2644e51e363bda710a0d4ec9faeffa3bb77db3" "sha256": "8da3024cf0a8fc29f77c481204bce0ed3672cf411a5b52e553cb60c92213b0aa"
}, },
{ {
"path": "web/worker/test_xyzac_machine_sequence.mjs", "path": "web/worker/test_xyzac_machine_sequence.mjs",
"sha256": "9dcd89d54596fe4b339d5aca4e028a0c44e173a8e646f1254defc706bd3fcc96" "sha256": "386fa1e72f194ba0c93ad0083474da48c8f526b627bd22ef398e3bf5b1ad1552"
}, },
{ {
"path": "web/worker/test_xyzac_manual_control.mjs", "path": "web/worker/test_xyzac_manual_control.mjs",
"sha256": "0f6320b0e08e73830c96b502c032c5720453973452c467fd08657e28b657ff3c" "sha256": "6219a55c4c1470193f984d94fea528e697479903e2623abf57a4d5b5274fd797"
}, },
{ {
"path": "web/worker/test_xyzac_program_preview.mjs", "path": "web/worker/test_xyzac_program_preview.mjs",
@@ -1992,15 +1996,15 @@
}, },
{ {
"path": "web/worker/test_xyzac_protocol_react.mjs", "path": "web/worker/test_xyzac_protocol_react.mjs",
"sha256": "69d942dfdcb7bc0d7fb26d6708220a47af13aa84acdcf490780980f6de0cc03d" "sha256": "daa74b5091de4579b7ce5eb283b802e226e275bf51209af339e4cf6334fd6b5b"
}, },
{ {
"path": "web/worker/test_xyzac_recovery.mjs", "path": "web/worker/test_xyzac_recovery.mjs",
"sha256": "d8f02b0ab5b7c9fc5ef30a3a67f7896071ecf9bbaa7d2f3614c031d118e655e7" "sha256": "d25dc8f5de748b37ca231ee7e5a0df049878511f4c0fec98061208730f57b145"
}, },
{ {
"path": "web/worker/test_xyzac_rtcp.mjs", "path": "web/worker/test_xyzac_rtcp.mjs",
"sha256": "9bc14997e56a8d0f6f0043b3a92fa5efe0fc8b3fa9c973bffa9045446973208f" "sha256": "7e8be744ba11def666f3cca70252d9ba64f6801d6436f624e4b8c81738a46ec5"
}, },
{ {
"path": "web/worker/test_xyzac_session.mjs", "path": "web/worker/test_xyzac_session.mjs",
@@ -2012,23 +2016,23 @@
}, },
{ {
"path": "web/worker/test_xyzac_trace_parity.mjs", "path": "web/worker/test_xyzac_trace_parity.mjs",
"sha256": "f5c6a7cd3e8b7f72bb1b25b5c368c0e52d494b9c811022d34435e668612ec4ce" "sha256": "209254396d3ec828936ec5c2ad34de9044706f7d0309fd5ad4cacae347f15247"
}, },
{ {
"path": "web/worker/test_xyzac_trt_case.mjs", "path": "web/worker/test_xyzac_trt_case.mjs",
"sha256": "abdf50fba4103a7dabe13495cb62bc65019a152de2d91ed01150b968cdf7c147" "sha256": "e1ab517d11a70e0682453c7fa82f9d1bcda8de0c7fd8547e6db24ef03fe4ad16"
}, },
{ {
"path": "web/worker/test_xyzac_trt_io_authority.mjs", "path": "web/worker/test_xyzac_trt_io_authority.mjs",
"sha256": "5be19961fe2c810e5a5ab17566febb8e024810c7bf3a492572728a085305ad96" "sha256": "d027080177a54ce2132f9023dc844b140acd1ed0d354c534733398154a30d72f"
}, },
{ {
"path": "web/worker/test_xyzac_trt_wasm_program_authority.mjs", "path": "web/worker/test_xyzac_trt_wasm_program_authority.mjs",
"sha256": "29a3babd0c0a31de7bd8a81748de484a698ebf7153bfb33822ee912049b3eafc" "sha256": "52543ae1a9f9ced7b2cb7666ed80490f65f5cda0494d8fbedfc664f303a85627"
}, },
{ {
"path": "web/worker/test_xyzac_workflow.mjs", "path": "web/worker/test_xyzac_workflow.mjs",
"sha256": "6c738c6993d43b4ce16feecfe87d2b507824a88b06ead2f0659db6ca25c346b5" "sha256": "4dc92c0185bb6f1c4c4f925bc6e5e6800acd0c1ee5c3c4c194776fda03d656d9"
}, },
{ {
"path": "web/worker/trace-export.mjs", "path": "web/worker/trace-export.mjs",
@@ -2056,7 +2060,7 @@
}, },
{ {
"path": "web/worker/wasm-xyzac-io.mjs", "path": "web/worker/wasm-xyzac-io.mjs",
"sha256": "fdb92e8819e38010aa7a1b8f5f58473a930644fbfeba3d7d5a5c23e075ee92ca" "sha256": "692dfb4c869130fd4a0ee78a500a54817e326991f2a5ac23a91059136d41eabd"
}, },
{ {
"path": "web/worker/wasm-xyzac-kinematics.mjs", "path": "web/worker/wasm-xyzac-kinematics.mjs",
@@ -2082,10 +2086,10 @@
"scope": "selected_xyzac_simulation_only", "scope": "selected_xyzac_simulation_only",
"sources": [ "sources": [
{ {
"lines": "107-739", "lines": "107-833",
"path": "web/worker/sim-session.mjs", "path": "web/worker/sim-session.mjs",
"role": "session-authority-routing", "role": "session-authority-routing",
"sha256": "7ce91549f0f3a06ff77db9ee281fd9544848f6e41e08f9da302ee2e7827194e3", "sha256": "adf00f12f4408bcbe00e296ce3d8003f764e4cdd0849c86039586fecdc822af1",
"status": "resolved", "status": "resolved",
"symbol": "new ProgramRunner" "symbol": "new ProgramRunner"
}, },
@@ -2189,7 +2193,7 @@
"lines": "39-39", "lines": "39-39",
"path": "web/worker/wasm-xyzac-io.mjs", "path": "web/worker/wasm-xyzac-io.mjs",
"role": "wasm-hal-io-tool-probe-authority", "role": "wasm-hal-io-tool-probe-authority",
"sha256": "fdb92e8819e38010aa7a1b8f5f58473a930644fbfeba3d7d5a5c23e075ee92ca", "sha256": "692dfb4c869130fd4a0ee78a500a54817e326991f2a5ac23a91059136d41eabd",
"status": "resolved", "status": "resolved",
"symbol": "export class WasmXyzacIo" "symbol": "export class WasmXyzacIo"
}, },
@@ -2203,9 +2207,9 @@
} }
], ],
"state_hashes": [ "state_hashes": [
"fnv1a64:b78114f0dff414b9", "fnv1a64:8bb613bfa975ae2e",
"fnv1a64:b312225bf059254c", "fnv1a64:051e2337103b3b9b",
"fnv1a64:c927914a74c8c3c2" "fnv1a64:7aaf47cbc3c37b54"
], ],
"status": "verified", "status": "verified",
"task_id": "WASM-00", "task_id": "WASM-00",
@@ -2223,7 +2227,7 @@
"control_count": 110, "control_count": 110,
"first_divergent_tick": null, "first_divergent_tick": null,
"motion_authority": "wasm", "motion_authority": "wasm",
"output_sha256": "16b23b8d13afdc46f06ef8f9dd14c4bd93cc7511b4334efbcebe08120ddb4858", "output_sha256": "de69f1a6f2f608f9d156713e50addd1677e17957b760261de7e0092a4e031a82",
"returncode": 0, "returncode": 0,
"state_hash": "fnv1a64:e862c9e59d9ffd8c", "state_hash": "fnv1a64:e862c9e59d9ffd8c",
"status": "verified", "status": "verified",
@@ -2231,19 +2235,19 @@
{ {
"active_type": 0, "active_type": 0,
"code": "M429", "code": "M429",
"state_hash": "fnv1a64:b78114f0dff414b9", "state_hash": "fnv1a64:8bb613bfa975ae2e",
"tick": 0 "tick": 0
}, },
{ {
"active_type": 1, "active_type": 1,
"code": "M428", "code": "M428",
"state_hash": "fnv1a64:b312225bf059254c", "state_hash": "fnv1a64:051e2337103b3b9b",
"tick": 0 "tick": 0
}, },
{ {
"active_type": 2, "active_type": 2,
"code": "M430", "code": "M430",
"state_hash": "fnv1a64:c927914a74c8c3c2", "state_hash": "fnv1a64:7aaf47cbc3c37b54",
"tick": 0 "tick": 0
} }
], ],

View File

@@ -57,8 +57,8 @@
}, },
"recovery": { "recovery": {
"at_checkpoint_equal": true, "at_checkpoint_equal": true,
"continuous_hash": "fnv1a64:a4dee4490d19b4ec", "continuous_hash": "fnv1a64:b105b6fc1bcc8a70",
"restored_hash": "fnv1a64:ca9e3481aaa28f6d", "restored_hash": "fnv1a64:857caa278d03c259",
"status": "verified" "status": "verified"
}, },
"scope_exclusions": { "scope_exclusions": {
@@ -96,7 +96,7 @@
"failure_samples": [], "failure_samples": [],
"input": { "input": {
"path": "coverage/xyzac-trt-web-wasm-parity.report.json", "path": "coverage/xyzac-trt-web-wasm-parity.report.json",
"sha256": "d8bd04725525c1c5881d749466c8793f4b771e34ba260c5d35cb1d29f889d4fc" "sha256": "7f88767d4b0b3b70bde27e8d5165a901618dfa2fb5076eceeb117f443bd305df"
}, },
"known_blockers": [ "known_blockers": [
"STOP_RELEASE is intentional until an owner authorizes scope expansion" "STOP_RELEASE is intentional until an owner authorizes scope expansion"
@@ -104,7 +104,7 @@
"outputs": [], "outputs": [],
"release_decision": "selected_case_verified_but_release_stopped_by_scope_exclusions", "release_decision": "selected_case_verified_but_release_stopped_by_scope_exclusions",
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "70dc5704e2530f9a42d6f5985f242be4fc4b691a2272fd15b9fa0bd8031a0728", "report_sha256": "712fa17edbff4c78aa58700637b54b4974b3a2c0104f4c91de566d4ff27ac5c4",
"schema": "xyzac-trt-wasm-only-release-gate.v1", "schema": "xyzac-trt-wasm-only-release-gate.v1",
"status": "verified", "status": "verified",
"task_id": "RELEASE-00" "task_id": "RELEASE-00"

View File

@@ -86,27 +86,27 @@
}, },
"recovery": { "recovery": {
"at_checkpoint_equal": true, "at_checkpoint_equal": true,
"continuous_hash": "fnv1a64:a4dee4490d19b4ec", "continuous_hash": "fnv1a64:b105b6fc1bcc8a70",
"restored_hash": "fnv1a64:ca9e3481aaa28f6d" "restored_hash": "fnv1a64:857caa278d03c259"
}, },
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_hashes": { "report_hashes": {
"coverage/xyzac-trt-gate-matrix.report.json": "495f452f59848114bb885561e26e7080f356650cc1b620cfddf515685fbf1628", "coverage/xyzac-trt-gate-matrix.report.json": "c3a5bc3eabd6631a8645a28df997ef605f911230c55527dbda016edf5a37ee6f",
"coverage/xyzac-trt-hal-tool-alarm.report.json": "2abcfd59bf16e596c9183296061ff66ce00f6d9b6efee121519a499007001216", "coverage/xyzac-trt-hal-tool-alarm.report.json": "99ac60c9a2d58f08063ffc07f0c2eea62d8ec97212d6d2b74d6c139f176b592e",
"coverage/xyzac-trt-manual-control.report.json": "b7333980194ff9b6344d34ec97a181fbb79b5730184dd13a6d1a9067c9212af9", "coverage/xyzac-trt-manual-control.report.json": "9ebb8ef2c97a57d77666c92e210ad1415a63852730b37993f995ca95cbc275b5",
"coverage/xyzac-trt-program-preview.report.json": "ee41e2b4ac97f719817e21eee2bd3d503fc78c783ec1bc185466b3c56fc62045", "coverage/xyzac-trt-program-preview.report.json": "b95a59986c0bd0cb098c49fbc8c9ced80bbe024888dcbcc14c15dd6e67432814",
"coverage/xyzac-trt-protocol-react.report.json": "bc25315930095ca096bae0f97047c41d45bf9ff83373ac7a73f52273b33d9237", "coverage/xyzac-trt-protocol-react.report.json": "1179b708f9f3185ea7cbd66f97aed984cfc22bcc4f35ea3d30293183e9bce011",
"coverage/xyzac-trt-recovery.report.json": "deff32f2998930dfcd9373ca0a6a960d1fa7e5732ef785cc7cacf336b9dc3459", "coverage/xyzac-trt-recovery.report.json": "351b3d5f51a6ef38f34c5e42f318a3b565618ebecf122f5d21d83ef8a54f2b4e",
"coverage/xyzac-trt-rtcp.report.json": "b1562d6d2c9cd5144c7c8c9b334bd8bf67919ea7297f103ec67cb32773608d08", "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-startup.report.json": "56e040b645bcefc3d6c57244cb2d3aba16f421ff5137089d9c3336970dfd968b",
"coverage/xyzac-trt-threejs.report.json": "2791930c38a48ab9fa61f284e57483708787e89b558c75ddc9d99e4b3e76ad28", "coverage/xyzac-trt-threejs.report.json": "5172d7118c555fca5f92bcb443b9c259ea0225346482558d4c33ef8a4f8da5cb",
"coverage/xyzac-trt-tool-preview.report.json": "4a5b286b0a13324a1e773669ef34ab9a64fd0376dfaf5adf39a95d9b420bd965", "coverage/xyzac-trt-tool-preview.report.json": "bc39dec83f71f9e164faed098d281ca1b2390b34b225fb4fa81ebf10bc860ee8",
"coverage/xyzac-trt-trace-parity.report.json": "ef914bb7823b9cdb0f6e15ca409515659d5575d09cdc6da2c458cd81fafa595a", "coverage/xyzac-trt-trace-parity.report.json": "73b2fd46c66d0c5910c746db9d8892e0ea713714576cf793f510bee35e9a7c00",
"coverage/xyzac-trt-wasm-authority-audit.report.json": "e7c84d3f36db280c13c1ab8a29f4f827b4257eb34b8cb2bbc47357a920001692", "coverage/xyzac-trt-wasm-authority-audit.report.json": "a970d9980b99d8b64315088e3f182d9d99aad3787831e28feb6127c9e854c6e8",
"coverage/xyzac-trt-workflow.report.json": "ede73fab04f3cc3e64a13fbe0ab10603253e9a01b4357feff1f3f54313777e75" "coverage/xyzac-trt-workflow.report.json": "acbb6dcfdff385dc839ddb94724f9fdcebc391ccde995881ba6be14dd5a6b5c4"
}, },
"report_sha256": "bf2f04c1798cd43016833836bab8c789ca71699ae414746601ae0efed010666d", "report_sha256": "09fa8fc47cd34237f97b7b74707cd57721874960809f627d1d49fdc0319c627a",
"schema": "xyzac-trt-web-wasm-parity-report.v1", "schema": "xyzac-trt-web-wasm-parity-report.v1",
"source_coverage": { "source_coverage": {
"axis_keyboard": 10, "axis_keyboard": 10,

View File

@@ -28,12 +28,12 @@
"pause_cursor_stable": true, "pause_cursor_stable": true,
"recovery_status": "recovered", "recovery_status": "recovered",
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "d98a16e6582c0bc9fdaa410da64be369cc21d56a61a04b43436d4900c12461b0", "report_sha256": "beaae5ac83afda17ceae23a7b999ae79154035ddf29e20b8c96707d7ccf4d808",
"restart_source_hash_preserved": true, "restart_source_hash_preserved": true,
"run_from_line": 4, "run_from_line": 4,
"schema": "xyzac-workflow-rtcp-05.v1", "schema": "xyzac-workflow-rtcp-05.v1",
"scope": "xyzac_only", "scope": "xyzac_only",
"state_hash": "fnv1a64:285d97dde7d56ea7", "state_hash": "fnv1a64:cb67e9edef5a4bc8",
"status": "verified", "status": "verified",
"task_id": "RTCP-05", "task_id": "RTCP-05",
"tests": [ "tests": [
@@ -42,7 +42,7 @@
"node", "node",
"web/worker/test_xyzac_workflow.mjs" "web/worker/test_xyzac_workflow.mjs"
], ],
"output_sha256": "197bc634f7f9ddf290b1a64d3cbd1f52d257dcc4982e5de3bd16f6358c70702b", "output_sha256": "13e39016ef9db1817514bb70f6c26ca5116f3fd84c30e78887fd93e5fcad2cc6",
"returncode": 0, "returncode": 0,
"status": "verified" "status": "verified"
} }

View File

@@ -21,13 +21,21 @@ def canonical(value: Any) -> str:
def main() -> int: def main() -> int:
command = ["node", "web/worker/test_xyzac_gate_matrix.mjs"] 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) 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 sample = None
for line in reversed(result.stdout.splitlines()): for line in reversed(result.stdout.splitlines()):
try: value = json.loads(line) try: value = json.loads(line)
except json.JSONDecodeError: continue except json.JSONDecodeError: continue
if isinstance(value, dict) and value.get("schema") == "xyzac-gate-matrix-rtcp-03.v1": sample = value; break 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" 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] = { report: dict[str, Any] = {
"schema": "xyzac-gate-matrix-rtcp-03.v1", "schema": "xyzac-gate-matrix-rtcp-03.v1",
"task_id": "RTCP-03", "task_id": "RTCP-03",
@@ -37,7 +45,7 @@ def main() -> int:
"status": status, "status": status,
"candidate_eligible": status == "verified", "candidate_eligible": status == "verified",
"authority": "linuxcnc-wasm-worker", "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], "tick_trace": [0, 0, 0],
"state_hash": sample.get("final_state_hash") if sample else None, "state_hash": sample.get("final_state_hash") if sample else None,
"gate_scenarios": { "gate_scenarios": {
@@ -47,9 +55,11 @@ def main() -> int:
"switchkins_estop": {"status": "verified", "reason": "xyzac_estop_active"}, "switchkins_estop": {"status": "verified", "reason": "xyzac_estop_active"},
"running_pause_abort": {"status": "verified", "pause": True, "abort": True}, "running_pause_abort": {"status": "verified", "pause": True, "abort": True},
"paused_resume": {"status": "verified", "resume": 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}, "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"}], "failure_samples": [] if status == "verified" else [{"code": "BLOCKED_GATE_MATRIX", "detail": "XYZAC gate matrix test failed"}],
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": None, "report_sha256": None,

View File

@@ -152,9 +152,9 @@ def build() -> dict[str, Any]:
source_hashes = {row["path"]: row["sha256"] for row in sources} source_hashes = {row["path"]: row["sha256"] for row in sources}
unresolved = [row for row in resources if row["status"].startswith("BLOCKED")] unresolved = [row for row in resources if row["status"].startswith("BLOCKED")]
command_contract = [{"command": command, "slice_id": slice_id} for command, slice_id in [ 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 [ 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")]] 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"]] 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"]] 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") 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"]} 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") 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") 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 return report

View File

@@ -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_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_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_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_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_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}") 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}")

View File

@@ -140,6 +140,7 @@ int sim_io_probe_update(sim_io *io, const sim_posemath_pose *pose,
uint32_t input); uint32_t input);
int sim_io_probe_disarm(sim_io *io); int sim_io_probe_disarm(sim_io *io);
int sim_io_set_estop(sim_io *io, uint32_t estop); 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_recover(sim_io *io);
int sim_io_step(sim_io *io, uint32_t ticks); int sim_io_step(sim_io *io, uint32_t ticks);
int sim_io_read_state(const sim_io *io, sim_io_state *state); int sim_io_read_state(const sim_io *io, sim_io_state *state);

View File

@@ -454,6 +454,33 @@ int sim_io_set_estop(sim_io *io, uint32_t estop) {
return SIM_IO_OK; 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) { int sim_io_recover(sim_io *io) {
if (!valid_io(io)) return SIM_IO_ERR_ARGUMENT; if (!valid_io(io)) return SIM_IO_ERR_ARGUMENT;
if (io->state.estop) return SIM_IO_ERR_ESTOP; if (io->state.estop) return SIM_IO_ERR_ESTOP;

View File

@@ -63,6 +63,9 @@ int main(void) {
assert(sim_io_set_estop(&io, 0) == SIM_IO_OK); assert(sim_io_set_estop(&io, 0) == SIM_IO_OK);
recover_status = sim_io_recover(&io); recover_status = sim_io_recover(&io);
assert(recover_status == SIM_IO_OK && io.state.ready == 1 && io.state.recovery_epoch == 1); 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); assert(sim_io_tool_set_fault(&io, 1, 42) == SIM_IO_OK);
fault_clear_status = sim_io_tool_clear_fault(&io); fault_clear_status = sim_io_tool_clear_fault(&io);

View File

@@ -28,8 +28,15 @@ export function normalizeAxisState(input = {}) {
const program = input.program ?? input.snapshot?.program ?? {}; const program = input.program ?? input.snapshot?.program ?? {};
const motion = input.motion ?? input.snapshot?.motion ?? {}; const motion = input.motion ?? input.snapshot?.motion ?? {};
const devices = input.devices ?? input.snapshot?.devices ?? {}; 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 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 inferredInterp = rawTaskState === "RUNNING" ? "READING" : rawTaskState === "PAUSED" ? "PAUSED" : "IDLE";
const interpState = string(task.interp_state ?? input.interp_state ?? input.interpreter_state, inferredInterp); const interpState = string(task.interp_state ?? input.interp_state ?? input.interpreter_state, inferredInterp);
const mode = string(task.mode ?? input.task_mode ?? input.mode, "MANUAL"); const mode = string(task.mode ?? input.task_mode ?? input.mode, "MANUAL");
@@ -47,7 +54,7 @@ export function normalizeAxisState(input = {}) {
? [true, true, true] ? [true, true, true]
: []; : [];
const limits = input.on_any_limit ?? motion.on_any_limit ?? motion.limit ?? false; 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 tool = devices.tool ?? input.tool ?? {};
const toolNumber = number(tool.current ?? input.tool_in_spindle); 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")); 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) { switch (action) {
case "estop": enabled = true; reason = estop ? "axis_estop_reset_available" : "axis_estop_available"; break; case "estop": enabled = true; reason = estop ? "axis_estop_reset_available" : "axis_estop_available"; break;
case "recover": enabled = true; reason = "axis_recover_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 "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 "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; case "open": enabled = idle; reason = enabled ? "axis_interp_idle" : "axis_interp_running"; break;

View File

@@ -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 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 { export class AxisOperationController {
constructor(client) { constructor(client) {
if (!client || typeof client.send !== "function") throw new Error("axis_operation_client_required"); 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 } = {}) { async execute(kind, payload = {}, { state = {}, action = COMMAND_ACTIONS[kind] ?? null, gate = true } = {}) {
const operationId = this.nextOperationId(kind); 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 effectiveGate = state?.gates?.[action] ?? { action, enabled: true, reason: "linuxcnc_wasm_worker_authority" };
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);
}
this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "pending", reason: effectiveGate.reason })); this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "pending", reason: effectiveGate.reason }));
try { try {
const response = await this.client.send(kind, payload, operationId); 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 }); return Object.freeze({ operationId, response, gate: effectiveGate });
} catch (error) { } catch (error) {
this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "error", reason: error?.message ?? String(error) })); this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "error", reason: error?.message ?? String(error) }));

View File

@@ -1,5 +1,5 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { AxisGateError, AxisOperationController } from "./axis-operation-controller.mjs"; import { AxisOperationController } from "./axis-operation-controller.mjs";
const calls = []; const calls = [];
const client = { send: async (kind, payload, operationId) => { calls.push({ kind, payload, operationId }); return { kind: "COMMAND_RESULT", payload: { status: "verified" } }; } }; 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 ready = { task: { state: "READY", mode: "MANUAL" }, program: { path: "main.ngc", program_status: "loaded" } };
const run = await controller.execute("PROGRAM_RUN", {}, { state: ready }); const run = await controller.execute("PROGRAM_RUN", {}, { state: ready });
assert.equal(run.operationId, "axis-program-run-000001"); 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); 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 }); const tick = await controller.execute("SIM_STEP", { cycles: 1 }, { state: ready, gate: false });
assert.equal(tick.operationId, "axis-sim-step-000003"); 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 })); 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 }));

View File

@@ -5,8 +5,7 @@ import { XyzacToolPreviewFromSnapshot } from "./XyzacToolPreview";
import { SIM_CASE_CATALOG } from "../../workspace/sim-case-catalog.mjs"; import { SIM_CASE_CATALOG } from "../../workspace/sim-case-catalog.mjs";
import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs"; import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs";
import { axisIcon } from "./sourceAssets"; import { axisIcon } from "./sourceAssets";
import { AxisGateError, AxisOperationController } from "../../app/axis-operation-controller.mjs"; import { AxisOperationController } from "../../app/axis-operation-controller.mjs";
import { axisGate } from "../../app/axis-contract.mjs";
import { XyzacDiagnostics, XyzacProgramPane, XyzacTrtPanel } from "./XyzacTrtPanel"; import { XyzacDiagnostics, XyzacProgramPane, XyzacTrtPanel } from "./XyzacTrtPanel";
import { XYZAC_CASE, projectXyzacPanel, type XyzacOperationTrace } from "./xyzac-panel-contract"; import { XYZAC_CASE, projectXyzacPanel, type XyzacOperationTrace } from "./xyzac-panel-contract";
@@ -32,21 +31,21 @@ function Menu({ label, children }: { label: string; children: React.ReactNode })
return <details className="axis-menu-item" data-axis-feature="AXIS-UI-02"><summary>{label}</summary><div className="axis-menu-popup">{children}</div></details>; return <details className="axis-menu-item" data-axis-feature="AXIS-UI-02"><summary>{label}</summary><div className="axis-menu-popup">{children}</div></details>;
} }
function MenuAction({ children, onClick, shortcut }: { children: React.ReactNode; onClick?: () => void; shortcut?: string }) { function MenuAction({ children, onClick, shortcut, disabled = false }: { children: React.ReactNode; onClick?: () => void; shortcut?: string; disabled?: boolean }) {
return <button type="button" onClick={onClick}><span>{children}</span>{shortcut && <kbd>{shortcut}</kbd>}</button>; return <button type="button" onClick={onClick} disabled={disabled}><span>{children}</span>{shortcut && <kbd>{shortcut}</kbd>}</button>;
} }
function AxisRadio({ axis, selected, onChange }: { axis: string; selected: string; onChange: (axis: string) => void }) { function AxisRadio({ axis, selected, onChange }: { axis: string; selected: string; onChange: (axis: string) => void }) {
return <label className={`axis-radio${selected === axis ? " selected" : ""}`}><input type="radio" name="axis" value={axis} checked={selected === axis} onChange={() => onChange(axis)} /><span>{axis.toUpperCase()}</span></label>; return <label className={`axis-radio${selected === axis ? " selected" : ""}`}><input type="radio" name="axis" value={axis} checked={selected === axis} onChange={() => onChange(axis)} /><span>{axis.toUpperCase()}</span></label>;
} }
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 <section className="machine-sequence" data-axis-feature="AXIS-MACHINE-SEQUENCE" aria-label="Machine sequence"> return <section className="machine-sequence" data-axis-feature="AXIS-MACHINE-SEQUENCE" aria-label="Machine sequence">
<div className="machine-sequence-heading"><strong>Machine sequence</strong><span>{isEstop ? "ESTOP" : machineOn ? "ON" : "OFF"} / {allHomed ? "HOME ALL" : "NOT HOMED"}</span></div> <div className="machine-sequence-heading"><strong>Machine sequence</strong><span>{isEstop ? "ESTOP" : machineOn ? "ON" : "OFF"} / {allHomed ? "HOME ALL" : "NOT HOMED"}</span></div>
<div className="machine-sequence-actions" role="group" aria-label="Emergency stop, power, homing and program controls"> <div className="machine-sequence-actions" role="group" aria-label="Emergency stop, power, homing and program controls">
<TextButton danger disabled={isEstop} onClick={onEstop} ariaLabel="Emergency stop">Emergency Stop</TextButton> <TextButton danger disabled={isEstop} onClick={onEstop} ariaLabel="Emergency stop">Emergency Stop</TextButton>
<TextButton disabled={!isEstop} onClick={onRelease} ariaLabel="Release emergency stop">Release E-stop</TextButton> <TextButton disabled={!isEstop} onClick={onRelease} ariaLabel="Release emergency stop">Release E-stop</TextButton>
<TextButton disabled={isEstop} onClick={onPower} ariaLabel="Power on">Power On</TextButton> <TextButton disabled={isEstop || programRunning || !canPower} onClick={onPower} ariaLabel={machineOn ? "Power off" : "Power on"}>{machineOn ? "Power Off" : "Power On"}</TextButton>
<TextButton disabled={!canHome} onClick={onHomeAll} ariaLabel="Home all axes">HOME ALL</TextButton> <TextButton disabled={!canHome} onClick={onHomeAll} ariaLabel="Home all axes">HOME ALL</TextButton>
<TextButton disabled={!canRun} onClick={onRun} ariaLabel="Run program">Run Program</TextButton> <TextButton disabled={!canRun} onClick={onRun} ariaLabel="Run program">Run Program</TextButton>
<TextButton feature="AXIS-MACHINE-SEQUENCE" disabled={!programLoaded || programRunning} onClick={onSequence} ariaLabel="Run startup sequence">Run Startup Sequence</TextButton> <TextButton feature="AXIS-MACHINE-SEQUENCE" disabled={!programLoaded || programRunning} onClick={onSequence} ariaLabel="Run startup sequence">Run Startup Sequence</TextButton>
@@ -148,7 +147,8 @@ export function App() {
const programRunning = operationState === "RUNNING"; const programRunning = operationState === "RUNNING";
const programPaused = operationState === "PAUSED"; const programPaused = operationState === "PAUSED";
const idle = operationState === "IDLE"; 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 planner = state.snapshot?.authoritative?.planner;
const trajectory = [...(planner?.queue ?? []), ...(planner?.active ? [planner.active] : [])]; const trajectory = [...(planner?.queue ?? []), ...(planner?.active ? [planner.active] : [])];
const stopRunTimer = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } }; const stopRunTimer = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } };
@@ -161,7 +161,7 @@ export function App() {
setOperationTrace(operationController.exportTrace()); 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) => { 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()); setOperationTrace(operationController.exportTrace());
const code = error instanceof AxisGateError ? error.code : error.message; const code = error.message;
setCommandStatus(`${kind} blocked: ${code}`); setCommandStatus(`${kind} blocked: ${code}`);
throw error; throw error;
}); });
@@ -170,14 +170,15 @@ export function App() {
const runProgram = () => { void dispatch("PROGRAM_RUN").catch(() => undefined); }; 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 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 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 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 powerOn = () => { if (isEstop || programRunning || !gates.power.enabled) return; void dispatch("POWER", { active: !machineOn }, false).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 resetMachine = () => { stopRunTimer(); stopJogTimer(); if (isEstop || programRunning || !machineOn) return; void dispatch("RESET").catch(() => undefined); };
const homeAll = () => { void dispatch("HOME", { all: true }).catch(() => undefined); }; const homeAll = () => { void dispatch("HOME", { all: true }).catch(() => undefined); };
const runStartupSequence = () => { const runStartupSequence = () => {
const release = isEstop ? releaseEstop() : Promise.resolve(); 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("HOME", { all: true }, false))
.then(() => dispatch("PROGRAM_RUN", {}, false)) .then(() => dispatch("PROGRAM_RUN", {}, false))
.catch(() => undefined); .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)}`; 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 <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-run-enabled={String(gates.run.enabled)} data-axis-homed={JSON.stringify(state.snapshot?.motion?.homed_axes ?? state.snapshot?.motion?.homed ?? null)} data-axis-program-status={String(state.program?.program_status ?? "idle")}> return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-run-enabled={String(gates.run.enabled)} data-axis-homed={JSON.stringify(state.snapshot?.motion?.homed_axes ?? state.snapshot?.motion?.homed ?? null)} data-axis-program-status={String(state.program?.program_status ?? "idle")}>
<header className="axis-window-header" data-axis-feature="AXIS-SHELL-01"><div className="axis-brand"><img src={axisIcon("axis-24x24.png")} alt="AXIS" /> <strong>AXIS</strong><span>LinuxCNC Web/WASM</span></div><span className={`connection-state ${state.status}`} data-axis-feature="AXIS-SHELL-01" role="status">{state.status.toUpperCase()}</span><span className="axis-state-badge" data-axis-state={taskState} aria-label={`Machine ${machineState}, operation ${operationState}`}>{taskState}</span></header> <header className="axis-window-header" data-axis-feature="AXIS-SHELL-01"><div className="axis-brand"><img src={axisIcon("axis-24x24.png")} alt="AXIS" /> <strong>AXIS</strong><span>LinuxCNC Web/WASM</span></div><span className={`connection-state ${state.status}`} data-axis-feature="AXIS-SHELL-01" role="status">{state.status.toUpperCase()}</span><span className="axis-state-badge" data-axis-state={taskState} aria-label={`Machine ${machineState}, operation ${operationState}`}>{taskState}</span></header>
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={openProgramFile}>Open G-code...</MenuAction><MenuAction onClick={() => setProgramText(DEFAULT_PROGRAM)}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn}>Power on</MenuAction><MenuAction onClick={resetMachine}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav> <nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={openProgramFile} disabled={programRunning}>Open G-code...</MenuAction><MenuAction onClick={() => setProgramText(DEFAULT_PROGRAM)} disabled={programRunning}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn} disabled={isEstop || programRunning || !gates.power.enabled}>{machineOn ? "Power off" : "Power on"}</MenuAction><MenuAction onClick={resetMachine} disabled={!gates.reset.enabled}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram} disabled={!gates.run.enabled}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram} disabled={!gates.abort.enabled}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode} disabled={programRunning}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")} disabled={programRunning}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label="Power on / recover [F2]" asset="tool_power.gif" onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" pressed={blockDelete} onClick={() => setBlockDelete((value) => !value)} /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" pressed={optionalStop} onClick={() => setOptionalStop((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /></section> <section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label={machineOn ? "Power off [F2]" : "Power on [F2]"} asset="tool_power.gif" disabled={isEstop || programRunning || !gates.power.enabled} onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" disabled={!gates.reset.enabled} onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" pressed={blockDelete} onClick={() => setBlockDelete((value) => !value)} /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" pressed={optionalStop} onClick={() => setOptionalStop((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /></section>
<section className="axis-case-strip" data-axis-feature="AXIS-EDIT-03"><label>Simulation case<select data-axis-feature="AXIS-EDIT-03" value={casePath} onChange={(event) => setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path === SELECTED_XYZAC_CASE).map((entry) => <option key={entry.path} value={entry.path}>{entry.label} | XYZAC TRT</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase} disabled={!gates.load.enabled}>Load case</TextButton><span className="case-status" role="status">{caseStatus}</span><span className="command-status" role="status">{commandStatus}</span></section> <section className="axis-case-strip" data-axis-feature="AXIS-EDIT-03"><label>Simulation case<select data-axis-feature="AXIS-EDIT-03" value={casePath} onChange={(event) => setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path === SELECTED_XYZAC_CASE).map((entry) => <option key={entry.path} value={entry.path}>{entry.label} | XYZAC TRT</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase} disabled={!gates.load.enabled}>Load case</TextButton><span className="case-status" role="status">{caseStatus}</span><span className="command-status" role="status">{commandStatus}</span></section>
<MachineSequence isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} canHome={gates.home.enabled} canRun={gates.run.enabled} onEstop={emergencyStop} onRelease={() => { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} /> <MachineSequence isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} canPower={gates.power.enabled} canHome={gates.home.enabled} canRun={gates.run.enabled} onEstop={emergencyStop} onRelease={() => { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} />
<XyzacTrtPanel view={xyzacView} command={xyzacCommand} /> <XyzacTrtPanel view={xyzacView} command={xyzacCommand} />
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section> <section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
<section className="axis-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" /></label><label>Feed Override:<output>{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" onChange={(event) => execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section> <section className="axis-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" /></label><label>Feed Override:<output>{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" onChange={(event) => execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section>

View File

@@ -9,8 +9,6 @@ declare module "*.mjs" {
execute(kind: string, payload?: Record<string, unknown>, options?: { state?: unknown; action?: string | null; gate?: boolean }): Promise<{ operationId: string; response: unknown; gate: { enabled: boolean; reason: string } }>; execute(kind: string, payload?: Record<string, unknown>, options?: { state?: unknown; action?: string | null; gate?: boolean }): Promise<{ operationId: string; response: unknown; gate: { enabled: boolean; reason: string } }>;
exportTrace(): Array<Record<string, unknown>>; exportTrace(): Array<Record<string, unknown>>;
} }
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<unknown>; export default createModule; } declare module "*.js" { const createModule: (options?: unknown) => Promise<unknown>; export default createModule; }

View File

@@ -31,19 +31,29 @@ function programActivity(task, program) {
return "IDLE"; 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(); 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); let operationState = programActivity(task, program);
if (action === "ESTOP" && payload.active === true) machineState = "ESTOP"; if (action === "ESTOP" && payload.active === true) machineState = "ESTOP";
if (action === "ESTOP" && payload.active === false) machineState = "ESTOP_RESET"; if (action === "ESTOP" && payload.active === false) machineState = "ESTOP_RESET";
if (action === "IO_RECOVER") machineState = "ON"; 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 === "HOME") operationState = "HOME";
if (action === "PROGRAM_PAUSE") operationState = "PAUSED"; if (action === "PROGRAM_PAUSE") operationState = "PAUSED";
if (action === "PROGRAM_RUN" || action === "PROGRAM_RESUME") operationState = "RUNNING"; if (action === "PROGRAM_RUN" || action === "PROGRAM_RESUME") operationState = "RUNNING";
if (["PROGRAM_LOAD", "PROGRAM_ABORT", "PROGRAM_RESTART", "MDI_EXECUTE"].includes(action)) operationState = "IDLE"; 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 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 sourceInterpState = operationState === "PAUSED" ? "INTERP_PAUSED" : operationState === "RUNNING" ? "INTERP_READING" : "INTERP_IDLE";
const sourceMode = String(task.mode ?? "MANUAL").toUpperCase(); 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] }); return Object.freeze({ state, machine_state: machineState, operation_state: operationState, source_task_state: sourceTaskState, source_interp_state: sourceInterpState, source_mode: sourceMode, states: [...AXIS_STATES] });

View File

@@ -12,7 +12,7 @@ export const LIMITS = Object.freeze({
export const COMMANDS = Object.freeze([ export const COMMANDS = Object.freeze([
"SESSION_ATTACH", "LOAD_WORKSPACE", "COMMAND", "SIM_STEP", "SNAPSHOT_REQUEST", "SESSION_ATTACH", "LOAD_WORKSPACE", "COMMAND", "SIM_STEP", "SNAPSHOT_REQUEST",
"PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", "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", "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", "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", "ENCODER_SET", "COOLANT_COMMAND", "COOLANT_FAULT", "DOOR_SET", "INTERLOCK_SET", "INTERLOCK_RESET", "DEVICE_ESTOP", "DEVICE_ACK_ISSUE", "DEVICE_ACK", "DEVICE_ACK_ABORT",

View File

@@ -149,7 +149,7 @@ export class SimSession {
motion_authority: statePayload.motion_authority ?? this.motionAuthority, motion_authority: statePayload.motion_authority ?? this.motionAuthority,
task_authority: statePayload.task_authority ?? this.task.authority(), task_authority: statePayload.task_authority ?? this.task.authority(),
...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}), ...(!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 motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export();
const wasmMotion = this.wasmMotion.read(); const wasmMotion = this.wasmMotion.read();
const homeLimit = this.wasmHomeLimit.read(); const homeLimit = this.wasmHomeLimit.read();
const ioState = this.xyzacIo?.canonical?.() ?? {};
const devices = this.wasmDeviceProjection(); const devices = this.wasmDeviceProjection();
const deviceIo = this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(); 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 halRuntime = this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export();
const authoritative = this.xyzacProgram const authoritative = this.xyzacProgram
? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker", state_hash: fnv1a64({ motion: wasmMotion, kinematics: this.xyzacKinematics?.snapshot() ?? null, task }) } ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker", state_hash: fnv1a64({ motion: wasmMotion, kinematics: this.xyzacKinematics?.snapshot() ?? null, task }) }
: this.authoritative.export(); : 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 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 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" const wasmPosition = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
? wasmMotion.state.position ? wasmMotion.state.position
: authoritative.kinematics.pose; : authoritative.kinematics.pose;
@@ -308,7 +332,12 @@ export class SimSession {
const program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : {}; const program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : {};
const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(); const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export();
const devices = this.wasmDeviceProjection(); 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" || 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 type = Number(input.payload?.type);
const action = input.kind === "VISUAL_CLEAR" ? "vismachClear" : type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : type === 2 ? "switchkinsUserk" : null; 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" }; 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() { stateForCheckpoint() {
const state = { const state = {
context_id: this.contextId, context_id: this.contextId,
@@ -887,6 +981,7 @@ export class SimSession {
} }
try { try {
if (kind === "ESTOP" && payload?.active === true) this.task.estop(-904); 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 === "IO_RECOVER" && result.status === "verified") this.task.recover();
else if (kind === "TOOL_FAULT" && payload?.fault === true) this.task.fault(-905); else if (kind === "TOOL_FAULT" && payload?.fault === true) this.task.fault(-905);
} catch (error) { } 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"); } 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"); return finish(this.envelope("SESSION_READY", input, { state_hash: this.task.hash(), task: this.task.export(), subscriptions: [] }), "verified");
case "SET_MODE": { 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(); const mode = String(input.payload?.mode ?? "").toUpperCase();
if (!["MANUAL", "MDI", "AUTO"].includes(mode)) return finish(this.error(input, "task_mode", "task_mode_invalid"), "failed"); if (!["MANUAL", "MDI", "AUTO"].includes(mode)) return finish(this.error(input, "task_mode", "task_mode_invalid"), "failed");
try { this.task.setMode(mode); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); } try { this.task.setMode(mode); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, mode, task: this.task.export(), operation_id: input.operation_id ?? null })); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, mode, task: this.task.export(), operation_id: input.operation_id ?? null }));
} }
case "POWER": { case "POWER": {
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"); 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") { if (result.status === "verified") {
try { this.task.recover(); } catch { /* READY is already powered for the selected simulation. */ } 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); 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"); 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": { 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"); 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(); const result = this.motion.resetFault();
if (result.status !== "verified") return finish(this.envelope("ERROR", input, { ...result, command: input.kind, task: this.task.export() }), "failed"); 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.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, limits: configuration.home_limits });
this.xyzacIo?.destroy(); this.xyzacIo?.destroy();
this.xyzacIo = new WasmXyzacIo(this.core, { spindleCount: 2, toolCapacity: 1001 }); 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?.destroy();
this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: 2 }); this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: 2 });
this.xyzacProgram?.destroy(); this.xyzacProgram?.destroy();

View File

@@ -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,
}));

View File

@@ -21,12 +21,17 @@ const initial = loaded.payload.panel_snapshot;
assert.equal(initial.xyzac.coordinates, "XYZAC"); assert.equal(initial.xyzac.coordinates, "XYZAC");
assert.equal(initial.xyzac.joint_count, 5); assert.equal(initial.xyzac.joint_count, 5);
assert.equal(initial.gates.run.enabled, false); assert.equal(initial.gates.run.enabled, false);
assert.equal(initial.gates.run.reason, "axis_taskfile_missing"); assert.equal(initial.gates.run.reason, "xyzac_machine_off");
assert.equal(initial.gates.runLine.reason, "axis_taskfile_missing"); assert.equal(initial.gates.runLine.reason, "xyzac_machine_off");
assert.equal(initial.gates.switchkinsIdentity.enabled, true); assert.equal(initial.gates.switchkinsIdentity.enabled, false);
assert.equal(initial.gates.switchkinsXyzac.enabled, true); assert.equal(initial.gates.switchkinsXyzac.enabled, false);
assert.equal(initial.gates.switchkinsUserk.enabled, true); assert.equal(initial.gates.switchkinsUserk.enabled, false);
assert.equal(initial.gates.vismachClear.visual_only, true); 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 }); const noFile = axisGate("runLine", { task: { state: "READY", mode: "MANUAL" }, program: {}, motion: {}, taskfile: false, highlight_line: null });
assert.equal(noFile.enabled, false); assert.equal(noFile.enabled, false);
assert.equal(noFile.reason, "axis_taskfile_missing"); 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 } }); 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.enabled, false);
assert.equal(estopGates.switchkinsXyzac.reason, "xyzac_estop_active"); assert.equal(estopGates.switchkinsXyzac.reason, "xyzac_estop_active");
send("ESTOP", "estop-reset", { active: false }); send("ESTOP", "estop-on", { active: true });
send("IO_RECOVER", "recover"); 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"); const recovered = send("SNAPSHOT_REQUEST", "recovered-snapshot");
assert.equal(typeof recovered.payload.panel_snapshot.state_hash, "string"); assert.equal(typeof recovered.payload.panel_snapshot.state_hash, "string");
assert.equal(recovered.payload.panel_snapshot.gates.vismachClear.visual_only, true); assert.equal(recovered.payload.panel_snapshot.gates.vismachClear.visual_only, true);

View File

@@ -14,6 +14,7 @@ let seq = 0;
const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload }); const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload });
send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); 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("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 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"); const postgui = await readFile(new URL("../../linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal", import.meta.url), "utf8");

View File

@@ -49,19 +49,15 @@ const released = send("ESTOP", { active: false });
assert.equal(released.payload.status, "verified"); assert.equal(released.payload.status, "verified");
assert.equal(released.payload.device_io.state.estop, 0); assert.equal(released.payload.device_io.state.estop, 0);
assert.equal(released.payload.device_io.state.ready, 0); assert.equal(released.payload.device_io.state.ready, 0);
assert.equal(released.payload.task.state, "ESTOP"); assert.equal(released.payload.task.state, "STOPPED");
const recovered = send("IO_RECOVER"); assert.equal(released.payload.panel_snapshot.axis_state.machine_state, "ESTOP_RESET");
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;
const powered = send("POWER", { active: true }); const powered = send("POWER", { active: true });
assert.equal(powered.payload.status, "verified"); assert.equal(powered.payload.status, "verified");
assert.equal(powered.payload.task.state, "READY"); assert.equal(powered.payload.task.state, "READY");
assert.equal(powered.payload.device_io.source, "wasm_io"); assert.equal(powered.payload.device_io.source, "wasm_io");
assert.equal(powered.payload.device_io.state.ready, 1); 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 }); const homed = send("HOME", { all: true });
assert.equal(homed.payload.status, "verified"); assert.equal(homed.payload.status, "verified");
@@ -113,7 +109,7 @@ console.log(JSON.stringify({
schema: "xyzac-machine-sequence.v1", schema: "xyzac-machine-sequence.v1",
status: "verified", status: "verified",
case: CASE, 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" }, 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, estop_queue_depth: emergency.payload.panel_snapshot.wasm_motion.state.queue_depth,
home_joint_count: homed.payload.wasm_home_limit.state.joint_count, home_joint_count: homed.payload.wasm_home_limit.state.joint_count,

View File

@@ -16,6 +16,7 @@ function setup(id) {
const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload }); const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload });
send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); 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("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 }; 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.kind, "COMMAND_RESULT");
assert.equal(estop.payload.task.state, "ESTOP"); assert.equal(estop.payload.task.state, "ESTOP");
assert.equal(estop.payload.device_io.source, "wasm_io"); 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"); 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 })); 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 }));

View File

@@ -1,7 +1,7 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import createModule from "../../build-wasm/linuxcnc-sim-core.js"; 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 { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
import { XyzacSession, CONTEXT_ID } from "./xyzac-session.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"); assert.equal(send("PROGRAM_RUN", "program-run").kind, "COMMAND_RESULT");
const runningSwitch = send("KINEMATICS_SELECT", "select-running", { type: 0 }); const runningSwitch = send("KINEMATICS_SELECT", "select-running", { type: 0 });
assert.equal(runningSwitch.kind, "ERROR"); 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"); send("PROGRAM_ABORT", "program-abort");
const sent = []; const sent = [];
const fakeClient = { send: async (kind, payload, operationId) => { sent.push({ kind, payload, operationId }); return { kind: "COMMAND_RESULT", payload: { status: "verified" } }; } }; 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 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" } } }; 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"); const workerBlocked = await controller.execute("KINEMATICS_SELECT", { type: 1 }, { state: disabledState, action: "switchkinsXyzac" });
assert.equal(sent.length, 0); 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" } } }; const enabledState = { ...disabledState, gates: { switchkinsXyzac: { action: "switchkinsXyzac", enabled: true, reason: "xyzac_m428_available" } } };
await controller.execute("KINEMATICS_SELECT", { type: 1 }, { state: enabledState, action: "switchkinsXyzac" }); await controller.execute("KINEMATICS_SELECT", { type: 1 }, { state: enabledState, action: "switchkinsXyzac" });
assert.equal(sent.length, 1); assert.equal(sent.length, 2);
assert.deepEqual(controller.exportTrace().map((item) => item.phase), ["blocked", "pending", "result"]); 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 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"); const panelSource = await readFile(new URL("../react/src/XyzacTrtPanel.tsx", import.meta.url), "utf8");

View File

@@ -27,6 +27,7 @@ async function create(id) {
send("SESSION_ATTACH", "attach"); send("SESSION_ATTACH", "attach");
const loaded = send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }); const loaded = send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
assert.equal(loaded.payload.status, "verified"); assert.equal(loaded.payload.status, "verified");
assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified");
return { core, session, send }; return { core, session, send };
} }

View File

@@ -95,6 +95,7 @@ const send = (kind, operationId, payload = {}) => session.accept({ ...base, kind
send("HELLO", "hello"); send("HELLO", "hello");
send("SESSION_ATTACH", "attach"); send("SESSION_ATTACH", "attach");
assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); 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 = []; const switchRows = [];
for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2], ["M428", 1]]) { for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2], ["M428", 1]]) {

View File

@@ -28,6 +28,7 @@ const send = (kind, operationId, payload = {}) => session.accept({ ...base, kind
assert.equal(send("HELLO", "hello").kind, "CAPABILITIES"); assert.equal(send("HELLO", "hello").kind, "CAPABILITIES");
assert.equal(send("SESSION_ATTACH", "attach").kind, "SESSION_READY"); 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("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"); assert.equal(session.xyzacKinematics.forward([12, -5, 20, 30, 45]).status, "verified");
const levels = { protocol: [], control: [], state: [], canonical: [], recovery: [] }; 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.kind, "COMMAND_RESULT");
assert.equal(result.payload.status, "verified"); assert.equal(result.payload.status, "verified");
assert.equal(kinematics.active_type, activeType); 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.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 }); levels.state.push({ active_type: activeType, joints: kinematics.joints, pose: selectedPose(kinematics.pose), tick });
} }

View File

@@ -34,6 +34,7 @@ assert.equal(loaded.payload.status, "verified");
assert.equal(loaded.payload.motion_authority, "wasm"); assert.equal(loaded.payload.motion_authority, "wasm");
assert.equal(loaded.payload.configuration.state, "TASK_READY"); assert.equal(loaded.payload.configuration.state, "TASK_READY");
assert.equal(loaded.payload.panel_snapshot.kinematics.joint_count, 5); assert.equal(loaded.payload.panel_snapshot.kinematics.joint_count, 5);
assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified");
const switchRows = []; const switchRows = [];
for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2]]) { for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2]]) {

View File

@@ -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.io.source, "wasm_io");
assert.equal(loaded.payload.panel_snapshot.device_io.source, "wasm_device_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(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 }); const spindle = send("SPINDLE_COMMAND", "spindle-on", { spindle: 0, rpm: 1200, enabled: true });
assert.equal(spindle.kind, "COMMAND_RESULT"); 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"); assert.equal(estop.payload.device_io.source, "wasm_io");
const blocked = send("SPINDLE_COMMAND", "spindle-blocked", { spindle: 0, rpm: 500, enabled: true }); const blocked = send("SPINDLE_COMMAND", "spindle-blocked", { spindle: 0, rpm: 500, enabled: true });
assert.equal(blocked.kind, "ERROR"); 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 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; const secondHash = send("SNAPSHOT_REQUEST", "snapshot-b").payload.panel_snapshot.device_io.projection_hash;
assert.equal(firstHash, secondHash); assert.equal(firstHash, secondHash);

View File

@@ -15,6 +15,7 @@ const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kin
send("HELLO", "hello"); send("HELLO", "hello");
send("SESSION_ATTACH", "attach"); send("SESSION_ATTACH", "attach");
assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); 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"); const home = send("HOME", "home");
assert.equal(home.payload.motion_authority, "wasm"); assert.equal(home.payload.motion_authority, "wasm");
assert.equal(home.payload.wasm_home_limit.state.homed, true); assert.equal(home.payload.wasm_home_limit.state.homed, true);

View File

@@ -14,6 +14,7 @@ let seq = 0;
const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload }); const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id, payload });
send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); 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("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 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" }); const loaded = send("PROGRAM_LOAD", "program-load", { source, path: "workflow.ngc" });
assert.equal(loaded.kind, "COMMAND_RESULT"); assert.equal(loaded.kind, "COMMAND_RESULT");

View File

@@ -131,6 +131,7 @@ export class WasmXyzacIo {
} }
command(kind, payload = {}) { command(kind, payload = {}) {
if (kind === "RESET") return this.invoke("sim_io_reset");
if (kind === "SPINDLE_COMMAND") return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, payload.rpm ?? payload.speed ?? 0, payload.enabled === false ? 0 : 1]); if (kind === "SPINDLE_COMMAND") 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_FAULT") return this.invoke("sim_io_spindle_set_fault", [payload.spindle ?? 0, payload.fault === true ? 1 : 0]);
if (kind === "SPINDLE_INHIBIT") return this.invoke("sim_io_spindle_set_inhibit", [payload.spindle ?? 0, payload.inhibit === true ? 1 : 0]); if (kind === "SPINDLE_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; return result;
} }
if (kind === "ESTOP" || kind === "DEVICE_ESTOP") return this.invoke("sim_io_set_estop", [payload.active === true ? 1 : 0]); if (kind === "ESTOP" || kind === "DEVICE_ESTOP") return this.invoke("sim_io_set_estop", [payload.active === true ? 1 : 0]);
if (kind === "POWER") return this.invoke("sim_io_set_power", [payload.active === true ? 1 : 0]);
if (kind === "IO_RECOVER" || kind === "INTERLOCK_RESET") return this.invoke("sim_io_recover"); 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 === "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" }; if (kind === "COOLANT_COMMAND") return { status: "error", code: "wasm_device_io_required", detail: "coolant uses sim_device_io authority" };

View File

@@ -6,12 +6,16 @@ export const XYZAC_TRT_CASE_CONTRACT = Object.freeze({
"status": "indexed" "status": "indexed"
}, },
"commands": [ "commands": [
"POWER",
"RESET",
"ESTOP",
"MDI_EXECUTE", "MDI_EXECUTE",
"PROGRAM_LOAD", "PROGRAM_LOAD",
"PROGRAM_RUN", "PROGRAM_RUN",
"PROGRAM_PAUSE", "PROGRAM_PAUSE",
"PROGRAM_RESUME", "PROGRAM_RESUME",
"PROGRAM_ABORT", "PROGRAM_ABORT",
"PROGRAM_RESTART",
"JOG_START", "JOG_START",
"JOG_STOP", "JOG_STOP",
"HOME", "HOME",
@@ -20,7 +24,7 @@ export const XYZAC_TRT_CASE_CONTRACT = Object.freeze({
], ],
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"js_authority": 0, "js_authority": 0,
"report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e", "report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f",
"resources": [ "resources": [
{ {
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", "path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",

View File

@@ -1152,6 +1152,10 @@ export const XYZAC_TRT_SOURCE_SLICES = Object.freeze({
"command": "PROGRAM_ABORT", "command": "PROGRAM_ABORT",
"slice_id": "AXIS-XYZAC-PROGRAM-MDI" "slice_id": "AXIS-XYZAC-PROGRAM-MDI"
}, },
{
"command": "PROGRAM_RESTART",
"slice_id": "AXIS-XYZAC-PROGRAM-MDI"
},
{ {
"command": "MDI_EXECUTE", "command": "MDI_EXECUTE",
"slice_id": "AXIS-XYZAC-PROGRAM-MDI" "slice_id": "AXIS-XYZAC-PROGRAM-MDI"
@@ -1168,6 +1172,18 @@ export const XYZAC_TRT_SOURCE_SLICES = Object.freeze({
"command": "HOME", "command": "HOME",
"slice_id": "AXIS-XYZAC-JOG-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", "command": "M428",
"slice_id": "AXIS-XYZAC-M428-M429-M430" "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" "slice_id": "AXIS-XYZAC-ERROR-RECOVERY"
} }
], ],
"report_sha256": "817531c109c341ccda78b085d2613f35379a716a98f54c9561a118f995b6cd5e", "report_sha256": "6fdc4530f450c3305a2ab4ddc6cdb3296a6cec0f62ab87cc2cd6cfcd0d43f00f",
"schema": "xyzac-trt-source-slice-inventory.v1", "schema": "xyzac-trt-source-slice-inventory.v1",
"slices": [ "slices": [
{ {
@@ -1954,6 +1970,22 @@ export const XYZAC_TRT_SOURCE_SLICES = Object.freeze({
"slice_id": "AXIS-XYZAC-PROGRAM-MDI", "slice_id": "AXIS-XYZAC-PROGRAM-MDI",
"state": "program.current_line" "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", "slice_id": "AXIS-XYZAC-KINEMATICS",
"state": "motion.joints" "state": "motion.joints"