Display live WASM machining parameters

This commit is contained in:
2026-09-02 12:38:27 +08:00
parent cd0286a3d6
commit ee2c992328
15 changed files with 152 additions and 10 deletions

View File

@@ -14,6 +14,24 @@
"task_gate": "sim_task_command_gate", "task_gate": "sim_task_command_gate",
"frontend_role": "input_and_projection_only" "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": { "switchkins_direct_matrix": {
"command": "node --experimental-strip-types web/worker/test_xyzac_trt_case.mjs", "command": "node --experimental-strip-types web/worker/test_xyzac_trt_case.mjs",
"sequence": [0, 1, 2, 0, 1, 2, 0], "sequence": [0, 1, 2, 0, 1, 2, 0],
@@ -57,7 +75,7 @@
}, },
"published_browser_smoke": { "published_browser_smoke": {
"url": "https://49.232.250.35:58010/", "url": "https://49.232.250.35:58010/",
"release": "20260902-linuxcnc-wasm-pause-resume-r11", "release": "20260902-linuxcnc-wasm-live-parameters-r14",
"status": "verified", "status": "verified",
"startup_pause_resume_stop": "verified", "startup_pause_resume_stop": "verified",
"pause_tick": 140, "pause_tick": 140,
@@ -65,6 +83,19 @@
"stop_state": "IDLE", "stop_state": "IDLE",
"browser_errors": 0, "browser_errors": 0,
"output_sha256": "bdf705473e6cda94d721aa23329257adf4a68b705add7650da5cc47012ab70b0", "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": { "continuous_runtime": {
"sampled_running_frames": 33, "sampled_running_frames": 33,
"queue_gaps": 0, "queue_gaps": 0,
@@ -89,9 +120,9 @@
"switchkins": "920b85ad7fd6451dfb134255ffdcc4f03a7aa615ebf486c63cde8582e17af195" "switchkins": "920b85ad7fd6451dfb134255ffdcc4f03a7aa615ebf486c63cde8582e17af195"
}, },
"resource_sha256": { "resource_sha256": {
"index-EXbw4yiL.js": "6759738df285bc5e2f5c3fb84043dc09fb82292bbc1c3c1011a558c8654c96ab", "index-CDC5yZJC.js": "c3a6aa009e091d5fcb2098c7254313d987229abc1e4765052edb15b465ca0fc4",
"sim-worker-entry-DjXIMZ0D.js": "190515af943d70e93037e88a5cf63b3b8e9cda675a8338cfc1fb4bb954bfd685", "sim-worker-entry-BQKHHWpv.js": "e72d80d9ae0fd679d92d3efbad4dba1eef55f9ce7b54bcd06e4a0d644c3cfb1a",
"linuxcnc-sim-core-D3Qw0e1g.wasm": "f1b1961279cf8ff7ef2e3fcd0068b3ba79de57abe1e56d5c32b16d17568b9536" "linuxcnc-sim-core-fuIvKetC.wasm": "ad4e17ecbf73afbad0d9f069ab20ba3c66b6cc4d75ecdb2b3182673490ee916d"
}, },
"authority_dom": { "authority_dom": {
"data-axis-authority": "linuxcnc-wasm-worker", "data-axis-authority": "linuxcnc-wasm-worker",

View File

@@ -53,6 +53,8 @@ AXIS 源控件清单:菜单 80、工具栏 20、快捷键 10,共 110 条,
| Feed/Jog/Max/Spindle override | WASM TP/IO getter/setter | 受控 slider,值由快照回读 | | Feed/Jog/Max/Spindle override | WASM TP/IO getter/setter | 受控 slider,值由快照回读 |
| 视图缩放、旋转、平移、tab | React/Three 本地 UI | 仅显示交互,不参与运动计算 | | 视图缩放、旋转、平移、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 状态。 在 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 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 连续性验证: 发布站点 r10 连续性验证:
```text ```text
@@ -270,6 +303,7 @@ panel_button_test_source 80782fc1f85f88beb48ec3dfaf638a93f2edb912e694840199dbff5
switchkins_direct_test_output 24e6cb8ec1195a97f71294b49217db0cbc0d7c3ecfa4bc8aa244ba3921c816a7 switchkins_direct_test_output 24e6cb8ec1195a97f71294b49217db0cbc0d7c3ecfa4bc8aa244ba3921c816a7
panel_r10_output 9bddcd8b0219925e00598e081dc7a4ed0f4868e8822a21f0ccd08d8e9d355d45 panel_r10_output 9bddcd8b0219925e00598e081dc7a4ed0f4868e8822a21f0ccd08d8e9d355d45
pause_resume_browser_screenshot 9d1660fd043dd0628ff2989a3e139a3239acd6386d141657a5834c9d2f1bc5a7 pause_resume_browser_screenshot 9d1660fd043dd0628ff2989a3e139a3239acd6386d141657a5834c9d2f1bc5a7
live_parameters_browser_screenshot 7f5307c913d8bc4e60c2a31c29aacc9afd1b0d91fac4a4f22e8a64fc2ea37a1e
``` ```
报告对应提交前工作树保留了既有 coverage 大文件和 LinuxCNC 子模块改动;本轮提交包含 WASM TP/IO/HOME authority、无 JS JOG 运动计算、前端 pending 投影、长程序快照性能修正、聚合测试和本报告。 报告对应提交前工作树保留了既有 coverage 大文件和 LinuxCNC 子模块改动;本轮提交包含 WASM TP/IO/HOME authority、无 JS JOG 运动计算、前端 pending 投影、长程序快照性能修正、聚合测试和本报告。

View File

@@ -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_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_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_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}") set_target_properties(sim_core_module PROPERTIES LINK_FLAGS "${_sim_module_link_flags}")
endif() endif()

View File

@@ -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); 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_scale(const sim_linuxcnc_tp *planner);
double sim_linuxcnc_tp_velocity_limit(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_scale(const sim_linuxcnc_tp *planner);
double sim_linuxcnc_tp_jog_velocity(const sim_linuxcnc_tp *planner, int axis); double sim_linuxcnc_tp_jog_velocity(const sim_linuxcnc_tp *planner, int axis);
double sim_linuxcnc_tp_feed_override(const sim_linuxcnc_tp *planner); double sim_linuxcnc_tp_feed_override(const sim_linuxcnc_tp *planner);

View File

@@ -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; 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) { double sim_linuxcnc_tp_jog_scale(const sim_linuxcnc_tp *planner) {
return planner == NULL || !planner->initialized ? 0.0 : planner->jog_scale; return planner == NULL || !planner->initialized ? 0.0 : planner->jog_scale;
} }

View File

@@ -19,6 +19,7 @@ int main(void) {
assert(planner != NULL); assert(planner != NULL);
assert(sim_linuxcnc_tp_init(planner, 0.001, 35.0, 300.0) == 0); 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_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_position(planner, &origin) == 0);
assert(sim_linuxcnc_tp_set_profile(planner, 0, 0.0) == 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); assert(sim_linuxcnc_tp_add_line(planner, &first, 20.0, 300.0, 16, 1) == 0);

View File

@@ -6,7 +6,7 @@ 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 { AxisOperationController } from "../../app/axis-operation-controller.mjs"; 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"; import { XYZAC_CASE, projectXyzacPanel, type XyzacOperationTrace } from "./xyzac-panel-contract";
export const SELECTED_XYZAC_CASE = XYZAC_CASE; export const SELECTED_XYZAC_CASE = XYZAC_CASE;
@@ -472,6 +472,7 @@ export function App() {
<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 || programLoading}>Load case</TextButton><TextButton feature="AXIS-EDIT-03" onClick={loadPackagedProgram} disabled={!gates.load.enabled || programLoading}>Load 3D_Chips.ngc</TextButton><span className={`case-status${programLoading ? " loading" : ""}`} role="status" aria-live="polite" aria-busy={programLoading}>{programLoading ? "PROGRAM_LOAD pending: LinuxCNC WASM preview in progress..." : caseStatus}</span>{previewProgress && <div className={`preview-load-indicator ${previewProgress.phase}`} role="status" aria-live="polite" aria-label="Toolpath preview loading progress"><span title={previewProgress.detail}>{previewProgress.detail}</span><progress max="100" value={previewProgress.percent}>{previewProgress.percent}%</progress><output>{previewProgress.percent}%</output></div>}<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 || programLoading}>Load case</TextButton><TextButton feature="AXIS-EDIT-03" onClick={loadPackagedProgram} disabled={!gates.load.enabled || programLoading}>Load 3D_Chips.ngc</TextButton><span className={`case-status${programLoading ? " loading" : ""}`} role="status" aria-live="polite" aria-busy={programLoading}>{programLoading ? "PROGRAM_LOAD pending: LinuxCNC WASM preview in progress..." : caseStatus}</span>{previewProgress && <div className={`preview-load-indicator ${previewProgress.phase}`} role="status" aria-live="polite" aria-label="Toolpath preview loading progress"><span title={previewProgress.detail}>{previewProgress.detail}</span><progress max="100" value={previewProgress.percent}>{previewProgress.percent}%</progress><output>{previewProgress.percent}%</output></div>}<span className="command-status" role="status">{commandStatus}</span></section>
<MachineSequence wasmReady={wasmSnapshotReady} isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} programLoading={programLoading} 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} /> <MachineSequence wasmReady={wasmSnapshotReady} isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} programLoading={programLoading} 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} />
<XyzacLiveParameters snapshot={state.snapshot} />
<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} canJog={manualReady && gates.jog.enabled} canHome={gates.home.enabled} canSpindle={manualReady && Boolean(gates.spindle?.enabled)} canCoolant={wasmSnapshotReady && Boolean(gates.coolant?.enabled)} canTool={manualReady && Boolean(gates.tool?.enabled)} canProbe={manualReady && Boolean(gates.probe?.enabled)} canTouchOff={manualReady && Boolean(gates.touchOff?.enabled)} canToolTouchOff={manualReady && Boolean(gates.toolTouchOff?.enabled)} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} spindleBrake={spindleBrake} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} jogScale={Number.isFinite(sourceJogScale) ? sourceJogScale : null} onJogScale={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={manualReady && 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={programLoading ? null : 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} canJog={manualReady && gates.jog.enabled} canHome={gates.home.enabled} canSpindle={manualReady && Boolean(gates.spindle?.enabled)} canCoolant={wasmSnapshotReady && Boolean(gates.coolant?.enabled)} canTool={manualReady && Boolean(gates.tool?.enabled)} canProbe={manualReady && Boolean(gates.probe?.enabled)} canTouchOff={manualReady && Boolean(gates.touchOff?.enabled)} canToolTouchOff={manualReady && Boolean(gates.toolTouchOff?.enabled)} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} spindleBrake={spindleBrake} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} jogScale={Number.isFinite(sourceJogScale) ? sourceJogScale : null} onJogScale={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={manualReady && 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={programLoading ? null : 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>{jogScalePercent === null ? "-" : `${jogScalePercent}%`}</output><input type="range" min="6" max="100" value={jogScalePercent ?? 6} onChange={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} disabled={!gates.jog.enabled || jogScalePercent === null} aria-label="Jog speed" /></label><label>Feed Override:<output>{feedOverridePercent === null ? "-" : `${feedOverridePercent}%`}</output><input type="range" min="0" max="200" step="1" value={feedOverridePercent ?? 0} onChange={requestFeedOverride} disabled={!gates.override.enabled || programLoading || feedOverridePercent === null} aria-label="Feed override" aria-valuetext={feedOverridePercent === null ? "WASM state pending" : `${feedOverridePercent}%`} data-axis-feed-override={feedOverridePercent ?? ""} /></label><label>Spindle Override:<output>{spindleOverridePercent === null ? "-" : `${spindleOverridePercent}%`}</output><input type="range" min="0" max="200" value={spindleOverridePercent ?? 0} onChange={(event) => void dispatch("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: Number(event.target.value) / 100 })} disabled={!gates.override.enabled || spindleOverridePercent === null} aria-label="Spindle override" /></label><label>Max Velocity:<output>{maxVelocityPercent === null ? "-" : `${maxVelocityPercent}%`}</output><input type="range" min="6" max="100" value={maxVelocityPercent ?? 6} onChange={(event) => void dispatch("SET_MAX_VELOCITY", { percent: Number(event.target.value) })} disabled={!gates.override.enabled || maxVelocityPercent === null} aria-label="Max velocity" /></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>{jogScalePercent === null ? "-" : `${jogScalePercent}%`}</output><input type="range" min="6" max="100" value={jogScalePercent ?? 6} onChange={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} disabled={!gates.jog.enabled || jogScalePercent === null} aria-label="Jog speed" /></label><label>Feed Override:<output>{feedOverridePercent === null ? "-" : `${feedOverridePercent}%`}</output><input type="range" min="0" max="200" step="1" value={feedOverridePercent ?? 0} onChange={requestFeedOverride} disabled={!gates.override.enabled || programLoading || feedOverridePercent === null} aria-label="Feed override" aria-valuetext={feedOverridePercent === null ? "WASM state pending" : `${feedOverridePercent}%`} data-axis-feed-override={feedOverridePercent ?? ""} /></label><label>Spindle Override:<output>{spindleOverridePercent === null ? "-" : `${spindleOverridePercent}%`}</output><input type="range" min="0" max="200" value={spindleOverridePercent ?? 0} onChange={(event) => void dispatch("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: Number(event.target.value) / 100 })} disabled={!gates.override.enabled || spindleOverridePercent === null} aria-label="Spindle override" /></label><label>Max Velocity:<output>{maxVelocityPercent === null ? "-" : `${maxVelocityPercent}%`}</output><input type="range" min="6" max="100" value={maxVelocityPercent ?? 6} onChange={(event) => void dispatch("SET_MAX_VELOCITY", { percent: Number(event.target.value) })} disabled={!gates.override.enabled || maxVelocityPercent === null} aria-label="Max velocity" /></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>
<XyzacProgramPane status={visibleProgramStatus} currentLine={visibleCurrentLine} sourceHash={programLoading ? "" : String(state.program?.source_hash ?? "")}><ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} executionTrace={visibleExecutionTrace} programStatus={visibleProgramStatus} onLoad={loadProgram} onRun={runProgram} onStep={() => 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} /></XyzacProgramPane> <XyzacProgramPane status={visibleProgramStatus} currentLine={visibleCurrentLine} sourceHash={programLoading ? "" : String(state.program?.source_hash ?? "")}><ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} executionTrace={visibleExecutionTrace} programStatus={visibleProgramStatus} onLoad={loadProgram} onRun={runProgram} onStep={() => 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} /></XyzacProgramPane>

View File

@@ -1,5 +1,6 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { XYZAC_KINEMATICS, type XyzacOperationTrace, type XyzacPanelView } from "./xyzac-panel-contract"; import { XYZAC_KINEMATICS, type XyzacOperationTrace, type XyzacPanelView } from "./xyzac-panel-contract";
import type { SimSnapshotV1 } from "./protocol";
type Command = (kind: string, payload?: Record<string, unknown>, action?: string) => void; type Command = (kind: string, payload?: Record<string, unknown>, action?: string) => void;
@@ -34,6 +35,26 @@ export function XyzacProgramPane({ status, currentLine, sourceHash, children }:
return <section className="xyzac-program" aria-label="XYZAC program"><header><strong>XYZAC Program</strong><span>{status}</span><span>Line {currentLine ?? "-"}</span><code>{sourceHash?.slice(0, 18) ?? "no source"}</code></header>{children}</section>; return <section className="xyzac-program" aria-label="XYZAC program"><header><strong>XYZAC Program</strong><span>{status}</span><span>Line {currentLine ?? "-"}</span><code>{sourceHash?.slice(0, 18) ?? "no source"}</code></header>{children}</section>;
} }
export function XyzacLiveParameters({ snapshot }: { snapshot: SimSnapshotV1 | null }) {
const ready = snapshot?.motion_authority === "wasm" && Boolean(snapshot?.task_authority);
const motion = (snapshot?.motion ?? {}) as Record<string, unknown>;
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<string, unknown> } | undefined)?.state;
return <section className="xyzac-live-parameters" data-axis-feature="AXIS-LIVE-PARAMETERS" data-xyzac-parameter-source={ready ? "linuxcnc-wasm" : "pending"} aria-label="Live machining parameters">
<div className="xyzac-section-title"><strong>Live parameters</strong><span>{ready ? "LinuxCNC WASM" : "WASM PENDING"}</span></div>
<div className="xyzac-parameter-grid">
<div><b>Spindle</b><output>{number(motion.spindle_speed_rpm, 1, "rpm")}</output><small>Cmd {number(motion.spindle_command_rpm, 1, "rpm")} / S {number(program?.speed, 1, "rpm")}</small></div>
<div><b>Feed actual</b><output>{number(motion.velocity_mm_min, 1, "mm/min")}</output><small>Program {number(motion.feed_command_mm_min, 1, "mm/min")}</small></div>
<div><b>TP velocity</b><output>{number(motion.velocity, 3, "mm/s")}</output><small>Limit {number(motion.max_velocity, 3, "mm/s")}</small></div>
<div><b>Overrides</b><output>F {number(typeof motion.feed_override === "number" ? motion.feed_override * 100 : Number.NaN, 0, "%")}</output><small>S {number(typeof motion.spindle_override === "number" ? motion.spindle_override * 100 : Number.NaN, 0, "%")}</small></div>
<div><b>Planner</b><output>{integer(wasmState?.queue_depth)} queued</output><small>Segment {integer(wasmState?.active_id)}</small></div>
<div><b>Execution</b><output>Line {integer(program?.execution_line ?? program?.current_line)}</output><small>{ready ? String(program?.program_status ?? "-") : "-"}</small></div>
</div>
</section>;
}
export function XyzacDiagnostics({ view, operations, alarms, error }: { view: XyzacPanelView; operations: XyzacOperationTrace[]; alarms: Array<Record<string, unknown>>; error: { code: string; message?: string } | null }) { export function XyzacDiagnostics({ view, operations, alarms, error }: { view: XyzacPanelView; operations: XyzacOperationTrace[]; alarms: Array<Record<string, unknown>>; error: { code: string; message?: string } | null }) {
const disabled = Object.entries(view.gates).filter(([, gate]) => gate.enabled === false); const disabled = Object.entries(view.gates).filter(([, gate]) => gate.enabled === false);
return <section className="xyzac-diagnostics" aria-label="XYZAC diagnostics"> return <section className="xyzac-diagnostics" aria-label="XYZAC diagnostics">

View File

@@ -27,11 +27,18 @@ export type MotionSnapshot = {
on_any_limit?: boolean; on_any_limit?: boolean;
fault?: string | null; fault?: string | null;
velocity_limit?: number; velocity_limit?: number;
velocity_mm_min?: number;
feed_command_mm_min?: number;
velocity_scale?: number; velocity_scale?: number;
jog_scale?: number; jog_scale?: number;
jog_velocity_linear?: number; jog_velocity_linear?: number;
jog_velocity_angular?: number; jog_velocity_angular?: number;
spindle_override?: 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 = { export type TrajectorySnapshot = {

View File

@@ -90,6 +90,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
overrides: (result.overrides as SimSnapshotV1["overrides"]) ?? previous.snapshot.overrides, overrides: (result.overrides as SimSnapshotV1["overrides"]) ?? previous.snapshot.overrides,
motion: { motion: {
...previous.snapshot.motion, ...previous.snapshot.motion,
...(result.motion as SimSnapshotV1["motion"] | undefined),
// Keep the XYZAC axis vector intact on every motion delta. Older // Keep the XYZAC axis vector intact on every motion delta. Older
// projection code rebuilt only XYZ, which made A/C disappear from // projection code rebuilt only XYZ, which made A/C disappear from
// consumers that read the canonical motion array. // consumers that read the canonical motion array.

View File

@@ -98,6 +98,13 @@ button { color: inherit; }
.xyzac-position-grid output { overflow: hidden; font-size: 13px; text-overflow: ellipsis; } .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-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-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 { 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 { 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; } .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-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 { display: grid; grid-template-columns: repeat(3, minmax(170px, 1fr)); gap: 12px; }
.axis-slider label { grid-template-columns: auto 48px; } .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-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; } .axis-diagnostics { display: grid; gap: 8px; padding: 10px; min-height: 190px; overflow: auto; background: #f7f7f7; font-size: 12px; }

View File

@@ -196,6 +196,13 @@ export class SimSession {
source_hash: program.source_hash, source_hash: program.source_hash,
state_hash: program.state_hash, state_hash: program.state_hash,
active_gcodes: program.active_gcodes, 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; } : null;
const trace = Array.isArray(statePayload.motion_trace) ? statePayload.motion_trace : []; const trace = Array.isArray(statePayload.motion_trace) ? statePayload.motion_trace : [];
const motionSample = motionDelta ? (trace.at(-1) ?? { const motionSample = motionDelta ? (trace.at(-1) ?? {
@@ -239,12 +246,14 @@ export class SimSession {
}; };
} }
wasmMotionProjection() { wasmMotionProjection(program = null) {
const wasmMotion = this.wasmMotion.read(); 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 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 home = this.wasmHomeLimit.read();
const io = this.xyzacIo?.canonical?.(); const io = this.xyzacIo?.canonical?.();
const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null; 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 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 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); 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 }, 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]), 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: 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), feed_override: Number(wasmMotion.state?.feed_override ?? 1),
spindle_override: Number(ioState?.spindles?.[0]?.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), velocity_scale: Number(wasmMotion.state?.velocity_scale ?? 1),
jog_scale: Number(wasmMotion.state?.jog_scale ?? 1), jog_scale: Number(wasmMotion.state?.jog_scale ?? 1),
jog_velocity_linear: Number(wasmMotion.state?.jog_velocity_linear ?? 0), jog_velocity_linear: Number(wasmMotion.state?.jog_velocity_linear ?? 0),
@@ -290,6 +305,17 @@ export class SimSession {
return { ...unsigned, state_hash: fnv1a64(unsigned) }; 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) { loadXyzacSubroutines(programPath) {
if (!this.xyzacProgram) return { status: "verified", count: 0 }; if (!this.xyzacProgram) return { status: "verified", count: 0 };
const requirement = this.xyzacProgram.requiresSubroutines(); 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 } = {}) { 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 wasmMotion = this.wasmMotion.read();
const homeLimit = this.wasmHomeLimit.read(); const homeLimit = this.wasmHomeLimit.read();
const ioState = this.xyzacIo?.canonical?.() ?? {}; const ioState = this.xyzacIo?.canonical?.() ?? {};
@@ -1681,6 +1707,8 @@ export class SimSession {
} }
const step = this.xyzacProgram.step(); const step = this.xyzacProgram.step();
if (step.status !== "verified") throw new Error(step.code); 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 // Path control is produced by the WASM interpreter and
// applied to the WASM trajectory queue before enqueue. // applied to the WASM trajectory queue before enqueue.
const motionControlMode = Number(step.program?.motion_control_mode ?? 0); const motionControlMode = Number(step.program?.motion_control_mode ?? 0);

View File

@@ -45,6 +45,8 @@ assert.equal(halfStep.payload.status, "verified");
assert.equal(session.wasmMotion.controls.feed_override, 0.5); assert.equal(session.wasmMotion.controls.feed_override, 0.5);
assert.equal(halfStep.payload.panel_snapshot.overrides.feed, 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.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 }); const fullOverride = send("SET_OVERRIDE", { feed: 2 });
assert.equal(fullOverride.payload.status, "verified"); assert.equal(fullOverride.payload.status, "verified");

View File

@@ -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 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 tpSource = await readFile(new URL("./wasm-linuxcnc-tp.mjs", import.meta.url), "utf8");
const combined = `${appSource}\n${panelSource}\n${contractSource}`; 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 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 [/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)); 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));

View File

@@ -17,7 +17,7 @@ function toPose(values) { return Object.fromEntries(["x", "y", "z", "a", "b", "c
export class WasmLinuxcncTp { export class WasmLinuxcncTp {
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300, limits = {} } = {}) { constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300, limits = {} } = {}) {
this.core = core; 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.cycleTime = cycleTime;
this.velocityLimit = velocityLimit; this.velocityLimit = velocityLimit;
this.accelerationLimit = accelerationLimit; this.accelerationLimit = accelerationLimit;
@@ -192,7 +192,8 @@ export class WasmLinuxcncTp {
this.controls.feed_override = feedOverride; this.controls.feed_override = feedOverride;
const position = toPose(doubles); const position = toPose(doubles);
const joints = this.commandedJoints(position, this.activeTag); 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; this.lastState = state;
return { status: "verified", state }; return { status: "verified", state };
} }