Complete XYZAC switchkins transitions

This commit is contained in:
2026-09-02 09:31:55 +08:00
parent 6b689fd69e
commit 0a22c7bf1b
4 changed files with 70 additions and 6 deletions

View File

@@ -14,6 +14,15 @@
"task_gate": "sim_task_command_gate",
"frontend_role": "input_and_projection_only"
},
"switchkins_direct_matrix": {
"command": "node --experimental-strip-types web/worker/test_xyzac_trt_case.mjs",
"sequence": [0, 1, 2, 0, 1, 2, 0],
"pose_source": "linuxcnc-source-tp",
"kinematics_source": "linuxcnc-wasm-sim_kinematics_switch",
"pose_match_tolerance": 1e-9,
"status": "verified",
"output_sha256": "24e6cb8ec1195a97f71294b49217db0cbc0d7c3ecfa4bc8aa244ba3921c816a7"
},
"source_control_inventory": {
"menus": 80,
"toolbar": 20,
@@ -47,7 +56,7 @@
},
"published_browser_smoke": {
"url": "https://49.232.250.35:58010/",
"release": "20260902-linuxcnc-wasm-authority-r6",
"release": "20260902-linuxcnc-wasm-switchkins-r7",
"status": "verified",
"startup_pause_resume_stop": "verified",
"pause_tick": 140,
@@ -57,7 +66,8 @@
"output_sha256": "bdf705473e6cda94d721aa23329257adf4a68b705add7650da5cc47012ab70b0",
"screenshot_sha256": {
"authority": "e58205228a04b386c7b7514bf03c71df7b53f66f2d9ebe2292931b36ef0edb55",
"running": "7fc474d0cb31a0fdd4afb198061e9812c753684e3d2963cc004ec4483c5c4990"
"running": "7fc474d0cb31a0fdd4afb198061e9812c753684e3d2963cc004ec4483c5c4990",
"switchkins": "920b85ad7fd6451dfb134255ffdcc4f03a7aa615ebf486c63cde8582e17af195"
},
"resource_sha256": {
"index-DEinWp01.js": "79f5e933909fc631186db9b5091b6239f30c1ade612bb908a4ab9fcf79965343",

View File

@@ -87,6 +87,7 @@ AXIS 源控件清单:菜单 80、工具栏 20、快捷键 10,共 110 条,
| Tool | Prepare、Prepared、Change、Changed | iocontrol/tool handler | WASM IO tool state | 通过 |
| Probe | Arm、Disarm、Trip | probe canonical/HAL | WASM IO + HAL | 通过 |
| Switchkins | Identity/M429、TCP: XYZAC/M428、User K/M430 | `switchkins_postgui.hal` + remap files | WASM kinematics `active_type` | 通过 |
| Switchkins | IDENTITY <-> TCP: XYZAC <-> USERK 任意切换 | `KINEMATICS_SELECT` / `M429` / `M428` / `M430` | 当前 WASM TP pose + WASM inverse/forward | 通过 |
| Preview | Preview、DRO、路径/实时/限位/速度/偏置/范围 | `glcanon.py` draw layers | WASM pose/joints/toolpath | 通过投影 |
| Preview | 缩放、平移、旋转、原点坐标系 | AXIS view handlers | React/Three display-only state | 通过显示 |
| Diagnostics | 状态、门控、错误、恢复 | AXIS status/error channel | Worker snapshot/ledger | 通过 |
@@ -117,6 +118,7 @@ AXIS 源控件清单:菜单 80、工具栏 20、快捷键 10,共 110 条,
- 主轴:CW、signed increase/decrease、Stop、Brake engage/release;
- Coolant、Tool prepare/change ack、Probe arm/disarm、ESTOP recovery;
- M428/M429/M430、VISUAL_CLEAR 和诊断快照。
- Switchkins 直接按钮在非零姿态下 `0 -> 1 -> 2 -> 0 -> 1 -> 2 -> 0` 循环切换;每一步的 kinematics pose 与 TP pose 逐轴一致。
8 条错误路径是刻意验证的拒绝:OFF 状态 Run/HOME/JOG/MDI/M428、Feed 201%、非法 MDI、运行中 MDI。它们均返回 LinuxCNC/WASM 可读原因,没有把拒绝当成成功。
@@ -149,6 +151,7 @@ ledger_audit_window: 32
scripts/build-wasm.sh
npm run build --prefix web/react
node --experimental-strip-types web/worker/test_xyzac_panel_buttons.mjs
node --experimental-strip-types web/worker/test_xyzac_trt_case.mjs
node --experimental-strip-types web/worker/test_xyzac_button_constraints.mjs
node --experimental-strip-types web/worker/test_xyzac_workflow.mjs
node --experimental-strip-types web/worker/test_xyzac_manual_control.mjs
@@ -191,6 +194,19 @@ screenshots: /tmp/published-r6-authority.png (e58205228a04b386c7b7514bf03c71df7b
/tmp/published-r6-running.png (7fc474d0cb31a0fdd4afb198061e9812c753684e3d2963cc004ec4483c5c4990)
```
发布站点 r7(IDENTITY/TCP: XYZAC/USERK 切换版本)浏览器验收:
```text
url: https://49.232.250.35:58010/
release: 20260902-linuxcnc-wasm-switchkins-r7
startup: WASM authority ready, initial machine OFF
power -> HOME ALL -> IDENTITY -> TCP: XYZAC -> USERK -> IDENTITY -> TCP: XYZAC -> USERK: verified
active modes: 0 -> 1 -> 2 -> 0 -> 1 -> 2
browser_errors: 0
screenshot: /tmp/published-switchkins-r7.png
screenshot_sha256: 920b85ad7fd6451dfb134255ffdcc4f03a7aa615ebf486c63cde8582e17af195
```
## 证据哈希
```text
@@ -201,6 +217,7 @@ feed_authority_output deab4a51ee0e56d4f8134efba4b7a97a2b88f37d9165cf730207f71886
wasm_io_source 1cb066d185dd2c5bbf435e022c2518483bd44962e5a0303248757b2da51740f7
wasm_io_c_source 09bce1430d6f71702ca0b3bf29ac50db377a6f232ce0d1a9b2c1eaac7ea2162e
panel_button_test_source 80782fc1f85f88beb48ec3dfaf638a93f2edb912e694840199dbff504477fe74
switchkins_direct_test_output 24e6cb8ec1195a97f71294b49217db0cbc0d7c3ecfa4bc8aa244ba3921c816a7
```
报告对应提交前工作树保留了既有 coverage 大文件和 LinuxCNC 子模块改动;本轮提交包含 WASM TP/IO/HOME authority、无 JS JOG 运动计算、前端 pending 投影、长程序快照性能修正、聚合测试和本报告。

View File

@@ -476,6 +476,9 @@ export class SimSession {
const program = this.xyzacProgram.snapshot();
const io = this.xyzacIo?.canonical?.() ?? {};
const wasmMotion = this.wasmMotion.read();
if (input.kind === "KINEMATICS_SELECT" && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) > 0) {
return { action: input.payload?.type === 0 ? "switchkinsIdentity" : input.payload?.type === 1 ? "switchkinsXyzac" : "switchkinsUserk", enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } };
}
const homeLimit = this.wasmHomeLimit.read();
const estop = Boolean(io.estop);
const ready = Boolean(io.ready) && !estop;
@@ -528,6 +531,22 @@ export class SimSession {
return { action, enabled: result === 0, reason, taskState: task.state, interpState: task.state === "PAUSED" || program.status === 3 ? "PAUSED" : task.state === "RUNNING" || program.status === 2 ? "READING" : "IDLE", wasm: { io_ready: ready, io_estop: estop, program_status: program.program_status, queue_depth: wasmMotion.state.queue_depth, gate_result: result } };
}
switchXyzacKinematics(mode) {
if (!this.xyzacKinematics) return { status: "error", code: "xyzac_kinematics_unavailable" };
const current = this.wasmMotion.read();
if (current.status !== "verified") return { status: "error", code: "wasm_motion_unavailable" };
if (Number(current.state.queue_depth ?? 0) > 0) return { status: "error", code: "xyzac_motion_active" };
const joints = Array.isArray(current.state.joint_commanded) && current.state.joint_commanded.length === 5
? [...current.state.joint_commanded]
: [...this.xyzacKinematics.snapshot().joints];
const selected = this.xyzacKinematics.select(mode, joints);
if (selected.status !== "verified") return selected;
this.xyzacProgramKinematicsMode = mode;
if (typeof this.wasmMotion.setActiveKinematicsTag === "function") this.wasmMotion.setActiveKinematicsTag(mode);
if (selected.pose && typeof this.wasmMotion.setPosition === "function") this.wasmMotion.setPosition(selected.pose);
return { ...selected, kinematics: this.xyzacKinematics.snapshot(), wasm_motion: this.wasmMotion.read() };
}
stateForCheckpoint() {
const state = {
context_id: this.contextId,
@@ -2095,9 +2114,9 @@ export class SimSession {
const controlCode = Number(result.program?.control_code ?? 0);
if ([428, 429, 430].includes(controlCode) && this.xyzacKinematics) {
const mode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[controlCode];
const selected = this.xyzacKinematics.select(mode, this.xyzacKinematics.snapshot().joints);
const selected = this.switchXyzacKinematics(mode);
if (selected.status !== "verified") return finish(this.error(input, "kinematics", selected.code ?? "xyzac_switchkins_failed"), "failed");
result.kinematics = this.xyzacKinematics.snapshot();
result.kinematics = selected.kinematics;
result.remap = { code: `M${controlCode}`, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" };
}
this.task.setMode("MDI");
@@ -2123,9 +2142,9 @@ export class SimSession {
const mode = Number(input.payload?.type);
if (![XYZAC_MODES.identity, XYZAC_MODES.xyzac, XYZAC_MODES.userk].includes(mode)) return finish(this.error(input, "kinematics", "xyzac_kinematics_type_invalid"), "failed");
this.task.setMode("MDI");
const result = this.xyzacKinematics.select(mode, this.xyzacKinematics.snapshot().joints);
const result = this.switchXyzacKinematics(mode);
const code = mode === XYZAC_MODES.xyzac ? "M428" : mode === XYZAC_MODES.identity ? "M429" : "M430";
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, remap: { code, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" }, kinematics: this.xyzacKinematics.snapshot(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, remap: { code, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" }, kinematics: result.kinematics ?? this.xyzacKinematics.snapshot(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "VISUAL_CLEAR":
return visualClearResponse();

View File

@@ -48,6 +48,23 @@ for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2]]) {
switchRows.push({ code, tick: result.simulation_tick, active_type: expected, state_hash: result.payload.panel_snapshot.state_hash });
}
// Direct panel selection must preserve the live WASM TP pose at a non-zero
// position for every pair of IDENTITY/TCP:XYZAC/USERK transitions.
const move = send("MDI_EXECUTE", "seed-position", { block: "G0 X10 Y5 Z20 A30 C45" });
assert.equal(move.kind, "COMMAND_RESULT");
for (let cycle = 0; cycle < 1000 && session.wasmMotion.read().state.queue_depth > 0; cycle += 1) send("SIM_STEP", `seed-step-${cycle}`, { cycles: 1 });
assert.equal(session.wasmMotion.read().state.queue_depth, 0);
const directSwitches = [];
for (const expected of [0, 1, 2, 0, 1, 2, 0]) {
const result = send("KINEMATICS_SELECT", `direct-${expected}-${seq}`, { type: expected });
assert.equal(result.kind, "COMMAND_RESULT");
assert.equal(result.payload.kinematics.active_type, expected);
const pose = result.payload.kinematics.pose;
const tpPose = result.payload.wasm_motion.state.position;
for (const axis of ["x", "y", "z", "a", "b", "c", "u", "v", "w"]) assert.ok(Math.abs(Number(pose[axis]) - Number(tpPose[axis])) <= 1e-9, `${axis}: ${pose[axis]} != ${tpPose[axis]}`);
directSwitches.push({ active_type: expected, state_hash: result.payload.kinematics.state_hash });
}
const pose = session.xyzacKinematics.forward([100, 50, 100, 30, 45]);
assert.equal(pose.status, "verified");
assert.equal(pose.joint_count, 5);
@@ -70,6 +87,7 @@ console.log(JSON.stringify({
motion_authority: loaded.payload.motion_authority,
control_count: controls.length,
switchkins: switchRows,
direct_switchkins: directSwitches,
pose: pose.pose,
wasm_state_hash: pose.wasm_state_hash,
state_hash: pose.state_hash,