diff --git a/coverage/xyzac-panel-button-parity.report.json b/coverage/xyzac-panel-button-parity.report.json index 9bd5284..0088769 100644 --- a/coverage/xyzac-panel-button-parity.report.json +++ b/coverage/xyzac-panel-button-parity.report.json @@ -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", diff --git a/doc-rtcp/xyzac-panel-button-parity-test-report.md b/doc-rtcp/xyzac-panel-button-parity-test-report.md index b62f824..05acbd4 100644 --- a/doc-rtcp/xyzac-panel-button-parity-test-report.md +++ b/doc-rtcp/xyzac-panel-button-parity-test-report.md @@ -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 投影、长程序快照性能修正、聚合测试和本报告。 diff --git a/web/worker/sim-session.mjs b/web/worker/sim-session.mjs index 08c459b..4d7a83a 100644 --- a/web/worker/sim-session.mjs +++ b/web/worker/sim-session.mjs @@ -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(); diff --git a/web/worker/test_xyzac_trt_case.mjs b/web/worker/test_xyzac_trt_case.mjs index 59d8cf7..c553fb3 100644 --- a/web/worker/test_xyzac_trt_case.mjs +++ b/web/worker/test_xyzac_trt_case.mjs @@ -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,