From ee2c9923281ed49161735441ffe00079b6ceddbb Mon Sep 17 00:00:00 2001 From: wangdequan Date: Wed, 2 Sep 2026 12:38:27 +0800 Subject: [PATCH] Display live WASM machining parameters --- .../xyzac-panel-button-parity.report.json | 39 +++++++++++++++++-- .../xyzac-panel-button-parity-test-report.md | 34 ++++++++++++++++ wasm-core/CMakeLists.txt | 1 + wasm-core/include/sim_linuxcnc_tp.h | 1 + wasm-core/src/sim_linuxcnc_tp.c | 4 ++ wasm-core/tests/sim_linuxcnc_tp_test.c | 1 + web/react/src/App.tsx | 3 +- web/react/src/XyzacTrtPanel.tsx | 21 ++++++++++ web/react/src/protocol.ts | 7 ++++ web/react/src/store.ts | 1 + web/react/src/styles.css | 9 +++++ web/worker/sim-session.mjs | 32 ++++++++++++++- web/worker/test_xyzac_feed_override.mjs | 2 + web/worker/test_xyzac_protocol_react.mjs | 2 +- web/worker/wasm-linuxcnc-tp.mjs | 5 ++- 15 files changed, 152 insertions(+), 10 deletions(-) diff --git a/coverage/xyzac-panel-button-parity.report.json b/coverage/xyzac-panel-button-parity.report.json index ff256e6..1af893c 100644 --- a/coverage/xyzac-panel-button-parity.report.json +++ b/coverage/xyzac-panel-button-parity.report.json @@ -14,6 +14,24 @@ "task_gate": "sim_task_command_gate", "frontend_role": "input_and_projection_only" }, + "live_parameters": { + "source": "linuxcnc-wasm", + "fields": [ + "spindle_speed_rpm", + "spindle_command_rpm", + "program.speed", + "velocity_mm_min", + "program.feed", + "velocity", + "velocity_limit", + "feed_override", + "spindle_override", + "queue_depth", + "active_id", + "program.execution_line" + ], + "status": "verified" + }, "switchkins_direct_matrix": { "command": "node --experimental-strip-types web/worker/test_xyzac_trt_case.mjs", "sequence": [0, 1, 2, 0, 1, 2, 0], @@ -57,7 +75,7 @@ }, "published_browser_smoke": { "url": "https://49.232.250.35:58010/", - "release": "20260902-linuxcnc-wasm-pause-resume-r11", + "release": "20260902-linuxcnc-wasm-live-parameters-r14", "status": "verified", "startup_pause_resume_stop": "verified", "pause_tick": 140, @@ -65,6 +83,19 @@ "stop_state": "IDLE", "browser_errors": 0, "output_sha256": "bdf705473e6cda94d721aa23329257adf4a68b705add7650da5cc47012ab70b0", + "live_parameters_runtime": { + "spindle_actual_rpm": 210, + "spindle_command_rpm": 1600, + "program_speed_rpm": 1600, + "feed_actual_mm_min": 1200, + "program_feed_mm_min": 2250000, + "tp_velocity_mm_s": 20, + "queue_depth": 22, + "active_segment": 21, + "current_line": 38, + "browser_errors": 0, + "screenshot_sha256": "7f5307c913d8bc4e60c2a31c29aacc9afd1b0d91fac4a4f22e8a64fc2ea37a1e" + }, "continuous_runtime": { "sampled_running_frames": 33, "queue_gaps": 0, @@ -89,9 +120,9 @@ "switchkins": "920b85ad7fd6451dfb134255ffdcc4f03a7aa615ebf486c63cde8582e17af195" }, "resource_sha256": { - "index-EXbw4yiL.js": "6759738df285bc5e2f5c3fb84043dc09fb82292bbc1c3c1011a558c8654c96ab", - "sim-worker-entry-DjXIMZ0D.js": "190515af943d70e93037e88a5cf63b3b8e9cda675a8338cfc1fb4bb954bfd685", - "linuxcnc-sim-core-D3Qw0e1g.wasm": "f1b1961279cf8ff7ef2e3fcd0068b3ba79de57abe1e56d5c32b16d17568b9536" + "index-CDC5yZJC.js": "c3a6aa009e091d5fcb2098c7254313d987229abc1e4765052edb15b465ca0fc4", + "sim-worker-entry-BQKHHWpv.js": "e72d80d9ae0fd679d92d3efbad4dba1eef55f9ce7b54bcd06e4a0d644c3cfb1a", + "linuxcnc-sim-core-fuIvKetC.wasm": "ad4e17ecbf73afbad0d9f069ab20ba3c66b6cc4d75ecdb2b3182673490ee916d" }, "authority_dom": { "data-axis-authority": "linuxcnc-wasm-worker", diff --git a/doc-rtcp/xyzac-panel-button-parity-test-report.md b/doc-rtcp/xyzac-panel-button-parity-test-report.md index 014d6cb..11b1aae 100644 --- a/doc-rtcp/xyzac-panel-button-parity-test-report.md +++ b/doc-rtcp/xyzac-panel-button-parity-test-report.md @@ -53,6 +53,8 @@ AXIS 源控件清单:菜单 80、工具栏 20、快捷键 10,共 110 条, | Feed/Jog/Max/Spindle override | WASM TP/IO getter/setter | 受控 slider,值由快照回读 | | 视图缩放、旋转、平移、tab | React/Three 本地 UI | 仅显示交互,不参与运动计算 | +实时参数区显示:主轴实际转速 `speed_rpm`、主轴指令转速 `command_rpm`、G-code `S` 值、TP 实际进给 `velocity_mm_min`、G-code `F` 值、TP 速度/上限、F/S override、队列深度、活动段和当前执行行。所有值来自 WASM program/TP/IO 快照,参数区的 `data-xyzac-parameter-source` 为 `linuxcnc-wasm`;未取得快照时显示 `-`。 + 在 WASM 快照尚未到达前,界面显示 `WASM PENDING` 并禁用运动操作;不会使用 JS 默认轴值、任务状态或 override 值。浏览器定时器只发送 `SIM_STEP` 采样请求,不执行 G-code、轨迹、运动学或设备状态计算。React/Three 中的定时器、相机、网格、刀具网格和文本格式化均为显示层,不产生或修改任何 CNC 状态。 ## 按钮/控件矩阵 @@ -192,6 +194,37 @@ screenshot: /tmp/published-pause-resume-final-r11c.png screenshot_sha256: 9d1660fd043dd0628ff2989a3e139a3239acd6386d141657a5834c9d2f1bc5a7 ``` +发布站点 r13 实时参数验证: + +```text +url: https://49.232.250.35:58010/ +release: 20260902-linuxcnc-wasm-live-parameters-r13 +parameter_source: linuxcnc-wasm +idle: Spindle 0.0 rpm / Feed actual 0.0 mm/min / TP velocity 0.000 mm/s / Planner 0 queued +running: Feed actual 540.0 mm/min / TP velocity 9.000 mm/s / Planner 1 queued / Segment 13 / Line 65 +fields: spindle actual+command+program S, feed actual+program F, TP velocity+limit, + F/S override, queue depth, active segment, execution line +browser_errors: 0 +screenshot: /tmp/published-live-parameters-r13.png +screenshot_sha256: 4e25750136d9b5a8ed633a15091128a94fc9bbec9af857c71d9bf5dd01270fb6 +``` + +发布站点 r14(程序主轴同步 + 实时参数)验证: + +```text +url: https://49.232.250.35:58010/ +release: 20260902-linuxcnc-wasm-live-parameters-r14 +resource_sha256: index-CDC5yZJC.js c3a6aa009e091d5fcb2098c7254313d987229abc1e4765052edb15b465ca0fc4 + sim-worker-entry-BQKHHWpv.js e72d80d9ae0fd679d92d3efbad4dba1eef55f9ce7b54bcd06e4a0d644c3cfb1a + linuxcnc-sim-core-fuIvKetC.wasm ad4e17ecbf73afbad0d9f069ab20ba3c66b6cc4d75ecdb2b3182673490ee916d +3D_Chips.ngc running: spindle_actual=210 rpm, spindle_command=1600 rpm, program_S=1600 rpm, + feed_actual=1200 mm/min, program_F=2250000 mm/min, + tp_velocity=20 mm/s, queue_depth=22, active_segment=21, line=38 +browser_errors: 0 +screenshot: /tmp/published-live-parameters-r14.png +screenshot_sha256: 7f5307c913d8bc4e60c2a31c29aacc9afd1b0d91fac4a4f22e8a64fc2ea37a1e +``` + 发布站点 r10 连续性验证: ```text @@ -270,6 +303,7 @@ panel_button_test_source 80782fc1f85f88beb48ec3dfaf638a93f2edb912e694840199dbff5 switchkins_direct_test_output 24e6cb8ec1195a97f71294b49217db0cbc0d7c3ecfa4bc8aa244ba3921c816a7 panel_r10_output 9bddcd8b0219925e00598e081dc7a4ed0f4868e8822a21f0ccd08d8e9d355d45 pause_resume_browser_screenshot 9d1660fd043dd0628ff2989a3e139a3239acd6386d141657a5834c9d2f1bc5a7 +live_parameters_browser_screenshot 7f5307c913d8bc4e60c2a31c29aacc9afd1b0d91fac4a4f22e8a64fc2ea37a1e ``` 报告对应提交前工作树保留了既有 coverage 大文件和 LinuxCNC 子模块改动;本轮提交包含 WASM TP/IO/HOME authority、无 JS JOG 运动计算、前端 pending 投影、长程序快照性能修正、聚合测试和本报告。 diff --git a/wasm-core/CMakeLists.txt b/wasm-core/CMakeLists.txt index 53d555d..27dc9b4 100644 --- a/wasm-core/CMakeLists.txt +++ b/wasm-core/CMakeLists.txt @@ -309,5 +309,6 @@ if(EMSCRIPTEN) string(REPLACE "_sim_motion_step','_sim_motion_read_state" "_sim_motion_step','_sim_motion_set_profile','_sim_motion_set_lookahead','_sim_motion_set_feed_override','_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_init" "_sim_motion_init','_sim_linuxcnc_tp_size','_sim_linuxcnc_tp_queue_capacity','_sim_linuxcnc_tp_init','_sim_linuxcnc_tp_reset','_sim_linuxcnc_tp_set_profile','_sim_linuxcnc_tp_set_feed_override','_sim_linuxcnc_tp_set_velocity_scale','_sim_linuxcnc_tp_set_jog_scale','_sim_linuxcnc_tp_set_axis_limit','_sim_linuxcnc_tp_jog','_sim_linuxcnc_tp_velocity_scale','_sim_linuxcnc_tp_velocity_limit','_sim_linuxcnc_tp_jog_scale','_sim_linuxcnc_tp_jog_velocity','_sim_linuxcnc_tp_feed_override','_sim_linuxcnc_tp_set_dynamics','_sim_linuxcnc_tp_set_paused','_sim_linuxcnc_tp_abort','_sim_linuxcnc_tp_set_position','_sim_linuxcnc_tp_set_goal_position','_sim_linuxcnc_tp_add_line','_sim_linuxcnc_tp_add_circle','_sim_linuxcnc_tp_step','_sim_linuxcnc_tp_idle','_sim_linuxcnc_tp_read_state','_sim_linuxcnc_tp_queue_depth','_sim_linuxcnc_tp_queue_segment','_sim_linuxcnc_tp_restore_progress','_sim_linuxcnc_tp_state_hash','_sim_linuxcnc_tp_canonical" _sim_module_link_flags "${_sim_module_link_flags}") + string(REPLACE "_sim_linuxcnc_tp_velocity_limit" "_sim_linuxcnc_tp_velocity_limit','_sim_linuxcnc_tp_velocity_mm_min" _sim_module_link_flags "${_sim_module_link_flags}") set_target_properties(sim_core_module PROPERTIES LINK_FLAGS "${_sim_module_link_flags}") endif() diff --git a/wasm-core/include/sim_linuxcnc_tp.h b/wasm-core/include/sim_linuxcnc_tp.h index 1f9e044..f957cd7 100644 --- a/wasm-core/include/sim_linuxcnc_tp.h +++ b/wasm-core/include/sim_linuxcnc_tp.h @@ -30,6 +30,7 @@ int32_t sim_linuxcnc_tp_set_axis_limit(sim_linuxcnc_tp *planner, int axis, doubl int32_t sim_linuxcnc_tp_jog(sim_linuxcnc_tp *planner, int axis, int direction, double increment, uint32_t tag); double sim_linuxcnc_tp_velocity_scale(const sim_linuxcnc_tp *planner); double sim_linuxcnc_tp_velocity_limit(const sim_linuxcnc_tp *planner); +double sim_linuxcnc_tp_velocity_mm_min(const sim_linuxcnc_tp *planner); double sim_linuxcnc_tp_jog_scale(const sim_linuxcnc_tp *planner); double sim_linuxcnc_tp_jog_velocity(const sim_linuxcnc_tp *planner, int axis); double sim_linuxcnc_tp_feed_override(const sim_linuxcnc_tp *planner); diff --git a/wasm-core/src/sim_linuxcnc_tp.c b/wasm-core/src/sim_linuxcnc_tp.c index 0dacba9..4c0c508 100644 --- a/wasm-core/src/sim_linuxcnc_tp.c +++ b/wasm-core/src/sim_linuxcnc_tp.c @@ -231,6 +231,10 @@ double sim_linuxcnc_tp_velocity_limit(const sim_linuxcnc_tp *planner) { return planner == NULL || !planner->initialized ? 0.0 : planner->tp.vLimit; } +double sim_linuxcnc_tp_velocity_mm_min(const sim_linuxcnc_tp *planner) { + return planner == NULL || !planner->initialized ? 0.0 : planner->status.current_vel * 60.0; +} + double sim_linuxcnc_tp_jog_scale(const sim_linuxcnc_tp *planner) { return planner == NULL || !planner->initialized ? 0.0 : planner->jog_scale; } diff --git a/wasm-core/tests/sim_linuxcnc_tp_test.c b/wasm-core/tests/sim_linuxcnc_tp_test.c index 332fcbc..a40e158 100644 --- a/wasm-core/tests/sim_linuxcnc_tp_test.c +++ b/wasm-core/tests/sim_linuxcnc_tp_test.c @@ -19,6 +19,7 @@ int main(void) { assert(planner != NULL); assert(sim_linuxcnc_tp_init(planner, 0.001, 35.0, 300.0) == 0); assert(sim_linuxcnc_tp_queue_capacity(planner) > 0); + assert(sim_linuxcnc_tp_velocity_mm_min(planner) == 0.0); assert(sim_linuxcnc_tp_set_position(planner, &origin) == 0); assert(sim_linuxcnc_tp_set_profile(planner, 0, 0.0) == 0); assert(sim_linuxcnc_tp_add_line(planner, &first, 20.0, 300.0, 16, 1) == 0); diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index 5bbe496..efc575c 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -6,7 +6,7 @@ import { SIM_CASE_CATALOG } from "../../workspace/sim-case-catalog.mjs"; import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs"; import { axisIcon } from "./sourceAssets"; import { AxisOperationController } from "../../app/axis-operation-controller.mjs"; -import { XyzacDiagnostics, XyzacProgramPane, XyzacTrtPanel } from "./XyzacTrtPanel"; +import { XyzacDiagnostics, XyzacLiveParameters, XyzacProgramPane, XyzacTrtPanel } from "./XyzacTrtPanel"; import { XYZAC_CASE, projectXyzacPanel, type XyzacOperationTrace } from "./xyzac-panel-contract"; export const SELECTED_XYZAC_CASE = XYZAC_CASE; @@ -472,6 +472,7 @@ export function App() {
Load caseLoad 3D_Chips.ngc{programLoading ? "PROGRAM_LOAD pending: LinuxCNC WASM preview in progress..." : caseStatus}{previewProgress &&
{previewProgress.detail}{previewProgress.percent}%{previewProgress.percent}%
}{commandStatus}
{ void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} /> +
{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => )}
execute("PROGRAM_STEP")} canLoad={gates.load.enabled && !programLoading} canRun={gates.run.enabled && !programLoading} canStep={gates.step.enabled && !programLoading} currentLine={visibleCurrentLine} executionLine={visibleExecutionLine} executionLineText={visibleExecutionLineText} diagnostic={state.error?.code ?? null} /> diff --git a/web/react/src/XyzacTrtPanel.tsx b/web/react/src/XyzacTrtPanel.tsx index 87a08d7..009f070 100644 --- a/web/react/src/XyzacTrtPanel.tsx +++ b/web/react/src/XyzacTrtPanel.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from "react"; import { XYZAC_KINEMATICS, type XyzacOperationTrace, type XyzacPanelView } from "./xyzac-panel-contract"; +import type { SimSnapshotV1 } from "./protocol"; type Command = (kind: string, payload?: Record, action?: string) => void; @@ -34,6 +35,26 @@ export function XyzacProgramPane({ status, currentLine, sourceHash, children }: return
XYZAC Program{status}Line {currentLine ?? "-"}{sourceHash?.slice(0, 18) ?? "no source"}
{children}
; } +export function XyzacLiveParameters({ snapshot }: { snapshot: SimSnapshotV1 | null }) { + const ready = snapshot?.motion_authority === "wasm" && Boolean(snapshot?.task_authority); + const motion = (snapshot?.motion ?? {}) as Record; + const program = snapshot?.program ?? null; + const number = (value: unknown, digits = 2, unit = "") => ready && typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(digits)}${unit ? ` ${unit}` : ""}` : "-"; + const integer = (value: unknown) => ready && Number.isSafeInteger(value) ? String(value) : "-"; + const wasmState = (snapshot?.wasm_motion as { state?: Record } | undefined)?.state; + return
+
Live parameters{ready ? "LinuxCNC WASM" : "WASM PENDING"}
+
+
Spindle{number(motion.spindle_speed_rpm, 1, "rpm")}Cmd {number(motion.spindle_command_rpm, 1, "rpm")} / S {number(program?.speed, 1, "rpm")}
+
Feed actual{number(motion.velocity_mm_min, 1, "mm/min")}Program {number(motion.feed_command_mm_min, 1, "mm/min")}
+
TP velocity{number(motion.velocity, 3, "mm/s")}Limit {number(motion.max_velocity, 3, "mm/s")}
+
OverridesF {number(typeof motion.feed_override === "number" ? motion.feed_override * 100 : Number.NaN, 0, "%")}S {number(typeof motion.spindle_override === "number" ? motion.spindle_override * 100 : Number.NaN, 0, "%")}
+
Planner{integer(wasmState?.queue_depth)} queuedSegment {integer(wasmState?.active_id)}
+
ExecutionLine {integer(program?.execution_line ?? program?.current_line)}{ready ? String(program?.program_status ?? "-") : "-"}
+
+
; +} + export function XyzacDiagnostics({ view, operations, alarms, error }: { view: XyzacPanelView; operations: XyzacOperationTrace[]; alarms: Array>; error: { code: string; message?: string } | null }) { const disabled = Object.entries(view.gates).filter(([, gate]) => gate.enabled === false); return
diff --git a/web/react/src/protocol.ts b/web/react/src/protocol.ts index 5445927..a9e6c38 100644 --- a/web/react/src/protocol.ts +++ b/web/react/src/protocol.ts @@ -27,11 +27,18 @@ export type MotionSnapshot = { on_any_limit?: boolean; fault?: string | null; velocity_limit?: number; + velocity_mm_min?: number; + feed_command_mm_min?: number; velocity_scale?: number; jog_scale?: number; jog_velocity_linear?: number; jog_velocity_angular?: number; spindle_override?: number; + spindle_speed_rpm?: number; + spindle_command_rpm?: number; + spindle_enabled?: boolean; + spindle_at_speed?: boolean; + max_velocity?: number; }; export type TrajectorySnapshot = { diff --git a/web/react/src/store.ts b/web/react/src/store.ts index f1aa3de..cb3dfaf 100644 --- a/web/react/src/store.ts +++ b/web/react/src/store.ts @@ -90,6 +90,7 @@ function projection(result: Record, message: MessageEnvelope, p overrides: (result.overrides as SimSnapshotV1["overrides"]) ?? previous.snapshot.overrides, motion: { ...previous.snapshot.motion, + ...(result.motion as SimSnapshotV1["motion"] | undefined), // Keep the XYZAC axis vector intact on every motion delta. Older // projection code rebuilt only XYZ, which made A/C disappear from // consumers that read the canonical motion array. diff --git a/web/react/src/styles.css b/web/react/src/styles.css index 2d41e3b..2131427 100644 --- a/web/react/src/styles.css +++ b/web/react/src/styles.css @@ -98,6 +98,13 @@ button { color: inherit; } .xyzac-position-grid output { overflow: hidden; font-size: 13px; text-overflow: ellipsis; } .xyzac-position-grid small { overflow: hidden; color: #666; font: 9px ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; } .xyzac-offset-row { display: flex; gap: 12px; margin-top: 5px; color: #555; font: 10px ui-monospace, monospace; } +.xyzac-live-parameters { margin: 0 5px 5px; padding: 5px 8px; border: 1px solid var(--border-dark); background: #dedede; } +.xyzac-live-parameters .xyzac-section-title { margin: 0 0 4px; } +.xyzac-parameter-grid { display: grid; grid-template-columns: repeat(6, minmax(120px, 1fr)); gap: 5px; } +.xyzac-parameter-grid > div { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: 2px 6px; min-width: 0; padding: 4px 6px; border-left: 2px solid #7894aa; background: #f7f7f7; } +.xyzac-parameter-grid b { color: #36434c; font-size: 11px; } +.xyzac-parameter-grid output { grid-column: 2; color: #111; font-size: 13px; white-space: nowrap; } +.xyzac-parameter-grid small { grid-column: 1 / -1; overflow: hidden; color: #666; font: 10px ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; } .xyzac-program { display: contents; } .xyzac-program > header { display: flex; align-items: center; gap: 14px; min-height: 25px; margin: 0 5px; padding: 3px 8px; border: 1px solid #888; border-bottom: 0; background: #d4d4d4; font-size: 11px; } .xyzac-program > header code { margin-left: auto; color: #4d4d4d; } @@ -180,6 +187,8 @@ output { font: 12px ui-monospace, monospace; text-align: right; } .axis-metrics { padding: 4px 9px; border-top: 1px solid #999; background: #cecece; } .axis-slider { display: grid; grid-template-columns: repeat(3, minmax(170px, 1fr)); gap: 12px; } .axis-slider label { grid-template-columns: auto 48px; } +@media (max-width: 900px) { .xyzac-parameter-grid { grid-template-columns: repeat(3, minmax(120px, 1fr)); } } +@media (max-width: 560px) { .xyzac-parameter-grid { grid-template-columns: repeat(2, minmax(120px, 1fr)); } } .axis-program-pane { display: grid; grid-template-columns: minmax(0, 1fr) 230px; grid-template-rows: minmax(0, 1fr); flex: 0 0 430px; height: 430px; min-height: 430px; margin: 0 5px 5px; border: 2px solid #777; background: #f7f7f7; } .axis-diagnostics { display: grid; gap: 8px; padding: 10px; min-height: 190px; overflow: auto; background: #f7f7f7; font-size: 12px; } diff --git a/web/worker/sim-session.mjs b/web/worker/sim-session.mjs index fede802..dd6198a 100644 --- a/web/worker/sim-session.mjs +++ b/web/worker/sim-session.mjs @@ -196,6 +196,13 @@ export class SimSession { source_hash: program.source_hash, state_hash: program.state_hash, active_gcodes: program.active_gcodes, + feed: program.feed, + speed: program.speed, + units: program.units, + distance_mode: program.distance_mode, + motion_control: program.motion_control, + motion_control_mode: program.motion_control_mode, + path_tolerance: program.path_tolerance, } : null; const trace = Array.isArray(statePayload.motion_trace) ? statePayload.motion_trace : []; const motionSample = motionDelta ? (trace.at(-1) ?? { @@ -239,12 +246,14 @@ export class SimSession { }; } - wasmMotionProjection() { + wasmMotionProjection(program = null) { const wasmMotion = this.wasmMotion.read(); const position = wasmMotion.status === "verified" ? wasmMotion.state.position : { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 }; const home = this.wasmHomeLimit.read(); const io = this.xyzacIo?.canonical?.(); const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null; + const sourceProgram = program ?? this.xyzacProgram?.snapshot(); + const spindle = ioState?.spindles?.[0] ?? null; const kinematics = this.xyzacKinematics?.snapshot?.(); const plannerJoints = wasmMotion.status === "verified" && Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : null; const kinematicsJoints = plannerJoints ?? (kinematics?.joints?.length === 5 ? [...kinematics.joints] : null); @@ -255,8 +264,14 @@ export class SimSession { axes: { x: position.x, y: position.y, z: position.z }, joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]), velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0, + velocity_mm_min: Number(wasmMotion.state?.velocity_mm_min ?? 0), + feed_command_mm_min: Number(sourceProgram?.feed ?? 0), feed_override: Number(wasmMotion.state?.feed_override ?? 1), spindle_override: Number(ioState?.spindles?.[0]?.override ?? 1), + spindle_speed_rpm: Number(spindle?.speed_rpm ?? 0), + spindle_command_rpm: Number(spindle?.command_rpm ?? 0), + spindle_enabled: Boolean(spindle?.enabled), + spindle_at_speed: Boolean(spindle?.at_speed), velocity_scale: Number(wasmMotion.state?.velocity_scale ?? 1), jog_scale: Number(wasmMotion.state?.jog_scale ?? 1), jog_velocity_linear: Number(wasmMotion.state?.jog_velocity_linear ?? 0), @@ -290,6 +305,17 @@ export class SimSession { return { ...unsigned, state_hash: fnv1a64(unsigned) }; } + syncXyzacProgramSpindle(program) { + if (!this.xyzacIo || !program || typeof program !== "object") return { status: "skipped" }; + const mode = Number(program.spindle_mode ?? 5); + const rpm = Number(program.speed ?? 0); + const enabled = (mode === 3 || mode === 4) && Number.isFinite(rpm) && rpm > 0; + const signedRpm = mode === 4 ? -rpm : rpm; + const current = this.xyzacIo.canonical()?.spindles?.[0]; + if (current && Boolean(current.enabled) === enabled && Math.abs(Number(current.command_rpm ?? 0) - (enabled ? signedRpm * Number(current.override ?? 1) : 0)) < 1e-9) return { status: "skipped" }; + return this.xyzacIo.command("SPINDLE_COMMAND", { spindle: 0, rpm: signedRpm, enabled }); + } + loadXyzacSubroutines(programPath) { if (!this.xyzacProgram) return { status: "verified", count: 0 }; const requirement = this.xyzacProgram.requiresSubroutines(); @@ -323,7 +349,7 @@ export class SimSession { } panelSnapshot({ task = this.task.export(), program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : null, input = {}, tick = this.tick, motionTrace = null, includeToolpath = input.kind !== "SIM_STEP" || !this.xyzacProgram, includeConfiguration = input.kind !== "SIM_STEP" || !this.xyzacProgram, includeStableRuntime = input.kind !== "SIM_STEP" || !this.xyzacProgram } = {}) { - const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(); + const motion = this.xyzacProgram ? this.wasmMotionProjection(program) : this.motion.export(); const wasmMotion = this.wasmMotion.read(); const homeLimit = this.wasmHomeLimit.read(); const ioState = this.xyzacIo?.canonical?.() ?? {}; @@ -1681,6 +1707,8 @@ export class SimSession { } const step = this.xyzacProgram.step(); if (step.status !== "verified") throw new Error(step.code); + const spindleSync = this.syncXyzacProgramSpindle(step.program); + if (spindleSync.status === "error") throw new Error(spindleSync.code ?? "wasm_program_spindle_sync_failed"); // Path control is produced by the WASM interpreter and // applied to the WASM trajectory queue before enqueue. const motionControlMode = Number(step.program?.motion_control_mode ?? 0); diff --git a/web/worker/test_xyzac_feed_override.mjs b/web/worker/test_xyzac_feed_override.mjs index 7a71939..e0ff0e5 100644 --- a/web/worker/test_xyzac_feed_override.mjs +++ b/web/worker/test_xyzac_feed_override.mjs @@ -45,6 +45,8 @@ assert.equal(halfStep.payload.status, "verified"); assert.equal(session.wasmMotion.controls.feed_override, 0.5); assert.equal(halfStep.payload.panel_snapshot.overrides.feed, 0.5); assert.ok(halfStep.payload.wasm_motion.state.velocity > 0, JSON.stringify(halfStep.payload.wasm_motion.state)); +assert.equal(halfStep.payload.motion.velocity_mm_min, halfStep.payload.wasm_motion.state.velocity_mm_min); +assert.ok(halfStep.payload.motion.velocity_mm_min > 0); const fullOverride = send("SET_OVERRIDE", { feed: 2 }); assert.equal(fullOverride.payload.status, "verified"); diff --git a/web/worker/test_xyzac_protocol_react.mjs b/web/worker/test_xyzac_protocol_react.mjs index 2ceff00..43a7803 100644 --- a/web/worker/test_xyzac_protocol_react.mjs +++ b/web/worker/test_xyzac_protocol_react.mjs @@ -72,7 +72,7 @@ const contractSource = await readFile(new URL("../react/src/xyzac-panel-contract const sessionSource = await readFile(new URL("./sim-session.mjs", import.meta.url), "utf8"); const tpSource = await readFile(new URL("./wasm-linuxcnc-tp.mjs", import.meta.url), "utf8"); const combined = `${appSource}\n${panelSource}\n${contractSource}`; -for (const symbol of ["XyzacTrtPanel", "XyzacProgramPane", "XyzacKinematicsControl", "XyzacManualPanel", "XyzacDiagnostics", "KINEMATICS_SELECT", "VISUAL_CLEAR", "POWER", "switchkinsXyzac", "Operation trace", "Alarms / recovery"]) assert.ok(combined.includes(symbol), symbol); +for (const symbol of ["XyzacTrtPanel", "XyzacProgramPane", "XyzacKinematicsControl", "XyzacManualPanel", "XyzacLiveParameters", "XyzacDiagnostics", "Spindle", "Feed actual", "TP velocity", "Live parameters", "KINEMATICS_SELECT", "VISUAL_CLEAR", "POWER", "switchkinsXyzac", "Operation trace", "Alarms / recovery"]) assert.ok(combined.includes(symbol), symbol); for (const symbol of ["wasmSnapshotReady", "manualReady", "sourceJogScale", "sourceVelocityScale", "spindleOverrideSource", "SET_JOG_SCALE", "SET_MAX_VELOCITY", "SET_SPINDLE_OVERRIDE", "WASM PENDING", "LinuxCNC WASM override-limits command unavailable"]) assert.ok(appSource.includes(symbol), symbol); for (const forbidden of [/defaultValue=\"50\"/, /defaultValue=\"100\"/, /setBlockDelete/, /setOptionalStop/, /state\.status === \"running\" \? \"READING\"/]) assert.equal(forbidden.test(appSource), false, String(forbidden)); for (const forbidden of [/_sim_/, /_malloc/, /\bHEAP(?:U|F|I)?\d*\b/, /navigator\.storage/, /\bOPFS\b/i, /postMessage\s*\(/]) assert.equal(forbidden.test(combined), false, String(forbidden)); diff --git a/web/worker/wasm-linuxcnc-tp.mjs b/web/worker/wasm-linuxcnc-tp.mjs index a9a15e7..8013e97 100644 --- a/web/worker/wasm-linuxcnc-tp.mjs +++ b/web/worker/wasm-linuxcnc-tp.mjs @@ -17,7 +17,7 @@ function toPose(values) { return Object.fromEntries(["x", "y", "z", "a", "b", "c export class WasmLinuxcncTp { constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300, limits = {} } = {}) { this.core = core; - this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_queue_capacity", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_velocity_scale", "sim_linuxcnc_tp_set_jog_scale", "sim_linuxcnc_tp_set_axis_limit", "sim_linuxcnc_tp_jog", "sim_linuxcnc_tp_velocity_scale", "sim_linuxcnc_tp_velocity_limit", "sim_linuxcnc_tp_jog_scale", "sim_linuxcnc_tp_jog_velocity", "sim_linuxcnc_tp_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function"); + this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_queue_capacity", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_velocity_scale", "sim_linuxcnc_tp_set_jog_scale", "sim_linuxcnc_tp_set_axis_limit", "sim_linuxcnc_tp_jog", "sim_linuxcnc_tp_velocity_scale", "sim_linuxcnc_tp_velocity_limit", "sim_linuxcnc_tp_velocity_mm_min", "sim_linuxcnc_tp_jog_scale", "sim_linuxcnc_tp_jog_velocity", "sim_linuxcnc_tp_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function"); this.cycleTime = cycleTime; this.velocityLimit = velocityLimit; this.accelerationLimit = accelerationLimit; @@ -192,7 +192,8 @@ export class WasmLinuxcncTp { this.controls.feed_override = feedOverride; const position = toPose(doubles); const joints = this.commandedJoints(position, this.activeTag); - const state = { position, velocity: doubles[9], acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: joints, joint_actual: joints.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, velocity_limit: velocityLimit, velocity_scale: velocityScale, jog_velocity_linear: this.jogVelocity("x"), jog_velocity_angular: this.jogVelocity("a"), feed_override: feedOverride, jog_scale: jogScale, source: "linuxcnc-source-tp" }; + const velocityMmMin = Number(call(this.core, "sim_linuxcnc_tp_velocity_mm_min", "number", ["number"], [this.planner])); + const state = { position, velocity: doubles[9], velocity_mm_min: velocityMmMin, acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: joints, joint_actual: joints.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, velocity_limit: velocityLimit, velocity_scale: velocityScale, jog_velocity_linear: this.jogVelocity("x"), jog_velocity_angular: this.jogVelocity("a"), feed_override: feedOverride, jog_scale: jogScale, source: "linuxcnc-source-tp" }; this.lastState = state; return { status: "verified", state }; }