Enforce AXIS source parity across panel controls
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
- 配置批量 runtime 已生成 264 个配置注册入口、233 个 sim entrypoint 和 16,093 条资源边;缺失、未解析和未索引资源均输出可定位诊断并继续作为仿真输入闭包处理。
|
||||
- 路线对齐的 React 前端位于 `web/react`:React + TypeScript + Vite + Tailwind + Radix UI + Three.js,React 只通过 protocol.v1/WorkerClient 访问 Dedicated Worker;FE-M1 构建证据见 `coverage/react-frontend-m1.report.json`。
|
||||
- AXIS 主界面完全对标的源程序功能清单、菜单/工具栏/状态门控、严格差异流程、任务分解和 M0-M7 验收门槛见 [`LinuxCNC AXIS 完全对标规范与执行计划`](docs/linuxcnc-axis-complete-parity-spec.md)。该文档以锁定的 `linuxcnc/share/axis/tcl/axis.tcl`、Gremlin/GLCanon、原始 bitmap 和 `configs/sim/axis` 为唯一 source oracle。
|
||||
- Web 控制面板操作必须遵守 [`LinuxCNC AXIS Web 铁律`](docs/linuxcnc-axis-web-iron-law.md):AXIS 源码和 LinuxCNC WASM 是唯一操作权威,React/JavaScript/Three.js 不得成为第二套运动或状态实现。
|
||||
|
||||
## 当前发布范围
|
||||
|
||||
|
||||
@@ -141,6 +141,25 @@
|
||||
},
|
||||
"browser_errors": 0
|
||||
},
|
||||
"published_panel_audit": {
|
||||
"command": "PLAYWRIGHT_MODULE=... node scripts/test_xyzac_panel_buttons_browser.mjs",
|
||||
"url": "https://49.232.250.35:58010/",
|
||||
"release": "20260902-linuxcnc-wasm-panel-audit-r27",
|
||||
"status": "verified",
|
||||
"verified_groups": 20,
|
||||
"button_count": 83,
|
||||
"disabled_button_count": 37,
|
||||
"browser_errors": 0,
|
||||
"authority": "linuxcnc-wasm-worker",
|
||||
"motion_authority": "wasm",
|
||||
"task_authority": "wasm_task_canonical",
|
||||
"final_machine_state": "Machine OFF",
|
||||
"final_operation_state": "Operation IDLE",
|
||||
"final_program_status": "loaded",
|
||||
"screenshot": "/tmp/xyzac-panel-buttons-browser.png",
|
||||
"screenshot_sha256": "dbf6d196cec3fe765a64d5a1d194b7ebb5681a30555f238e084c9ad6deac6032",
|
||||
"status_detail": "WASM startup, file/package load, POWER, HOME, RESET, switchkins, views, clear plot, MANUAL/MDI, XYZAC JOG, spindle/brake, coolant, tool/probe, overrides, MDI, RUN/PAUSE/RESUME/ABORT/STEP, startup sequence, editor Load/Reopen and ABI-disabled gates all verified"
|
||||
},
|
||||
"release_status": "STOP_RELEASE",
|
||||
"scope_gaps": [
|
||||
"override_limits_command",
|
||||
|
||||
@@ -326,3 +326,25 @@ program_replace_browser_screenshot c3e0a5b992381e7b6cb26f98a30338935df5239c87575
|
||||
```
|
||||
|
||||
报告对应提交前工作树保留了既有 coverage 大文件和 LinuxCNC 子模块改动;本轮提交包含 WASM TP/IO/HOME authority、无 JS JOG 运动计算、前端 pending 投影、长程序快照性能修正、聚合测试和本报告。
|
||||
|
||||
## AXIS 源码逐项映射与发布站点审计
|
||||
|
||||
本轮按 `linuxcnc/src/emc/usr_intf/axis/scripts/axis.py` 对照了 `manual_ok()`、`ensure_mode()`、`open_file_guts()`、`task_run/task_pause/task_resume/task_stop/task_step`、`jog_on/jog_off_actual`、`mist_toggle/flood_toggle`、`spindle_*`、`change_tool` 和 MDI 入口。映射约束与禁止事项见 [`LinuxCNC AXIS Web 铁律`](../docs/linuxcnc-axis-web-iron-law.md)。
|
||||
|
||||
发现并修复的实际偏差:
|
||||
|
||||
- 手动/MDI 页签现在通过 WASM `SET_MODE` 同步 LinuxCNC task mode;之前仅切 React 页签会使 JOG、主轴和 MDI 被错误地显示为不可用。
|
||||
- React 现在保留 `panel_snapshot.io` 的 WASM canonical envelope,正确读取 `state.ready/estop` 和设备状态。
|
||||
- `JOG_STOP` 对 LinuxCNC source TP 只停止当前 jog 并保持 TP runnable;不再伪造 `AUTO_PAUSE`,避免后续 MDI/主轴操作得到 `linuxcnc_tp_paused` 调度错误。
|
||||
- MDI 在前一次 JOG_STOP 后明确恢复 WASM TP runnable,再由 WASM 解释器/队列执行 MDI block。
|
||||
- 工具栏 Reset 和 Reopen defaults 均走 WASM gate;Reopen 执行 `LOAD_WORKSPACE + PROGRAM_LOAD`,不再只修改本地编辑器字符串。
|
||||
|
||||
发布地址 `https://49.232.250.35:58010/` 的 Chromium 审计命令:
|
||||
|
||||
```text
|
||||
PLAYWRIGHT_MODULE=... node scripts/test_xyzac_panel_buttons_browser.mjs
|
||||
```
|
||||
|
||||
审计覆盖:WASM startup、Open G-code file、`3D_Chips.ngc`、Load case、POWER ON、HOME ALL、RESET、IDENTITY/TCP:XYZAC/USERK、视图和 Clear plot、MANUAL/MDI、XYZAC 五轴 JOG、主轴正反转/停止/刹车、Mist/Flood、Tool/Probe、Jog/Feed/Spindle/Max velocity overrides、MDI、Run/Pause/Resume/Abort/Step、Run Startup Sequence、编辑器 Load/Reopen 和 ABI-disabled 控件。发布版本 `20260902-linuxcnc-wasm-panel-audit-r27` 最终结果为 20 组 verified、83 个按钮、37 个按 LinuxCNC gate 禁用、`browser_errors=0`;WASM worker authority 标记为 `linuxcnc-wasm-worker` / `wasm_task_canonical`,截图 `/tmp/xyzac-panel-buttons-browser.png`,SHA-256 `dbf6d196cec3fe765a64d5a1d194b7ebb5681a30555f238e084c9ad6deac6032`。
|
||||
|
||||
源代码对标结论:AXIS 的可执行操作均通过对应 WASM command/result 和后续 snapshot;`Override Limits`、Block delete/Optional pause、完整 Tool Touch Off/G10 交互以及桌面 tool editor/Quit 仍明确记录为 `blocked_by_abi` 或浏览器宿主边界,不能用 JavaScript 模拟。整体 `release_status` 继续保持 `STOP_RELEASE`,因为本报告不替代完整 TP 数值 oracle。
|
||||
|
||||
43
docs/linuxcnc-axis-web-iron-law.md
Normal file
43
docs/linuxcnc-axis-web-iron-law.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# LinuxCNC AXIS Web 铁律
|
||||
|
||||
本仓库的 Web 控制面板必须对标 `linuxcnc/src/emc/usr_intf/axis/scripts/axis.py` 的 AXIS 行为。LinuxCNC AXIS 源码和 LinuxCNC WASM 是操作语义、门控、状态、错误和运动结果的唯一权威;React、浏览器事件和 Three.js 只能负责输入、异步传输、状态投影和显示。
|
||||
|
||||
## 强制映射
|
||||
|
||||
| Web 操作 | AXIS 源入口 | WASM 命令/状态 | Web 规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| 急停/解除急停 | `commands.estop`、`General_Halt` | `ESTOP`、IO `estop` | 立即停机;解除后仍须按 LinuxCNC 的恢复/上电顺序 |
|
||||
| 上电/断电/重置 | `commands.power`、`commands.reset` | `POWER`、`RESET`、IO ready/task state | 仅按 WASM gate 启用;断电保留程序但中止运动 |
|
||||
| HOME ALL/单轴回零 | `home_all_joints`、`home_joint` | `HOME`、`wasm_home_limit` | 只能在 `manual_ok()` 等价条件满足时执行 |
|
||||
| 手动/JOG | `ensure_mode(MODE_MANUAL)`、`jog_on/jog_off` | `SET_MODE`、`JOG_START/JOG_STOP`、WASM TP tick | 按住开始、释放停止;五个轴都必须使用同一 WASM 运动源 |
|
||||
| MDI | `ensure_mode(MODE_MDI)`、`send_mdi_command` | `SET_MODE`、`MDI_EXECUTE` | 解释器运行时拒绝;队列和错误由 WASM 返回 |
|
||||
| 运行/暂停/恢复/停止 | `task_run`、`task_pause`、`task_resume`、`task_stop` | `PROGRAM_RUN/PAUSE/RESUME/ABORT` | 运行期间不切手动/MDI;暂停保留 TP 队列,恢复继续同一队列 |
|
||||
| 单步/重启/重新加载 | `task_step`、`reload_file`、`open_file_guts` | `PROGRAM_STEP/RESTART/LOAD_WORKSPACE/PROGRAM_LOAD` | 清理旧预览和执行轨迹,再显示新 WASM 结果 |
|
||||
| 主轴/冷却/刀具/探针 | `spindle_*`、`mist_toggle`、`flood_toggle`、`change_tool`、`probe` | `SPINDLE_*`、`COOLANT_COMMAND`、`TOOL_*`、`PROBE_CONFIG` | HAL/IO/设备状态由 WASM canonical snapshot 回显 |
|
||||
| 进给/主轴/最大速度覆盖 | `set_feedrate`、`set_spindlerate`、`tpSetVlimit` | `SET_OVERRIDE`、`SET_SPINDLE_OVERRIDE`、`SET_MAX_VELOCITY` | 只发送目标值;显示值必须来自 WASM snapshot |
|
||||
| 预览/清图/视图 | `glcanon`/`clear_live_plot` | `PROGRAM_LOAD` 生成的 canonical toolpath、`VISUAL_CLEAR` | 视图变换是显示操作,不能重算 G-code 或运动 |
|
||||
|
||||
## 门控铁律
|
||||
|
||||
网页必须实现 AXIS `manual_ok()` 的等价行为:task 必须 `STATE_ON`,不能处于 AUTO/MDI 的非空闲解释器状态;MDI 还必须受 queued-MDI 上限限制。按钮的 `disabled` 属性只能投影 WASM snapshot 的 gate,不能在 React 中复制另一套 task/motion/kinematics 判断。WASM 拒绝的命令必须保留错误码、`gate_reason` 和 operation trace。
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 禁止 React/JavaScript 解析 G-code、规划轨迹、计算 RTCP/FK/IK、推进运动或伪造轴值。
|
||||
- 禁止用本地按钮状态覆盖 WASM 的 task、IO、HAL、tool、probe、home/limit 或 planner 状态。
|
||||
- 禁止把按钮“可点击”、静态截图或单次页面加载当作实现证据;必须有 worker/WASM command result 和后续 snapshot。
|
||||
- 不具备对应 LinuxCNC/WASM ABI 的功能必须保持禁用,并明确记录 `blocked_by_abi`,不得用 JS 模拟补齐。
|
||||
|
||||
## 验收证据
|
||||
|
||||
每次面板变更必须运行:
|
||||
|
||||
```text
|
||||
node --experimental-strip-types web/worker/test_xyzac_button_constraints.mjs
|
||||
node --experimental-strip-types web/worker/test_xyzac_panel_buttons.mjs
|
||||
npm run typecheck --prefix web/react
|
||||
npm run build --prefix web/react
|
||||
PLAYWRIGHT_MODULE=... node scripts/test_xyzac_panel_buttons_browser.mjs
|
||||
```
|
||||
|
||||
浏览器审计必须覆盖真实发布站点的启动、打开文件、加载程序、上电、回零、重置、三种运动学、视图、清图、XYZAC JOG、主轴、冷却、刀具/探针、覆盖率、MDI、运行、暂停、恢复、停止、启动序列、编辑器加载和重新打开。结果应记录在 `doc-rtcp/xyzac-panel-button-parity-test-report.md`,并保持 `STOP_RELEASE`,直到更高层的完整 TP 数值 oracle 也通过。
|
||||
196
scripts/test_xyzac_panel_buttons_browser.mjs
Normal file
196
scripts/test_xyzac_panel_buttons_browser.mjs
Normal file
@@ -0,0 +1,196 @@
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const modulePath = process.env.PLAYWRIGHT_MODULE;
|
||||
const url = process.env.XYZAC_PANEL_URL ?? "https://49.232.250.35:58010/";
|
||||
if (!modulePath) {
|
||||
console.log(JSON.stringify({ schema: "xyzac-panel-buttons-browser.v1", status: "blocked", code: "playwright_unavailable" }));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const imported = await import(pathToFileURL(modulePath).href);
|
||||
const playwright = imported.default ?? imported;
|
||||
const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--ignore-certificate-errors", "--disable-gpu"] });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, ignoreHTTPSErrors: true });
|
||||
const failures = [];
|
||||
const browserErrors = [];
|
||||
page.on("pageerror", (error) => browserErrors.push(`pageerror:${error.message}`));
|
||||
page.on("console", (message) => { if (message.type() === "error") browserErrors.push(`console:${message.text()}`); });
|
||||
const main = page.locator("main.axis-app");
|
||||
const state = () => main.evaluate((element) => ({
|
||||
authority: element.dataset.axisAuthority,
|
||||
source: element.dataset.axisStateSource,
|
||||
motion: element.dataset.axisMotionAuthority,
|
||||
task: element.dataset.axisTaskAuthority,
|
||||
machine: element.querySelector(".machine-state")?.textContent?.trim(),
|
||||
operation: element.querySelector(".operation-state")?.textContent?.trim(),
|
||||
case: element.querySelector(".case-status")?.textContent?.trim(),
|
||||
mode: element.dataset.axisTaskMode,
|
||||
program: element.dataset.axisProgramStatus,
|
||||
runEnabled: element.dataset.axisRunEnabled,
|
||||
programLoading: element.querySelector(".case-status")?.getAttribute("aria-busy") === "true",
|
||||
homed: element.dataset.axisHomed,
|
||||
line: element.dataset.axisCurrentLine,
|
||||
queue: element.dataset.axisTpQueueDepth,
|
||||
velocity: element.dataset.axisTpVelocity,
|
||||
command: element.querySelector(".command-status")?.textContent?.trim(),
|
||||
error: element.querySelector(".diagnostic-error")?.textContent?.trim() ?? null,
|
||||
}));
|
||||
const wait = async (predicate, label, timeout = 45000) => {
|
||||
const started = Date.now(); let last;
|
||||
while (Date.now() - started < timeout) { last = await state(); if (predicate(last)) return last; await page.waitForTimeout(250); }
|
||||
throw new Error(`${label}:${JSON.stringify(last)}`);
|
||||
};
|
||||
const click = async (locator, label, { required = true, timeout = required ? 30000 : 3000 } = {}) => {
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < timeout) {
|
||||
if (await locator.count() && !await locator.first().isDisabled()) { await locator.first().click(); return true; }
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
if (required) throw new Error(`${label}:disabled_or_missing`);
|
||||
return false;
|
||||
};
|
||||
const record = async (name, action) => {
|
||||
process.stderr.write(`[panel-audit] ${name} ...\n`);
|
||||
try { await action(); process.stderr.write(`[panel-audit] ${name}: verified\n`); return { name, result: "verified" }; }
|
||||
catch (error) { const row = { name, result: "failed", reason: error?.message ?? String(error) }; failures.push(row); process.stderr.write(`[panel-audit] ${name}: failed ${row.reason}\n`); return row; }
|
||||
};
|
||||
const results = [];
|
||||
try {
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
results.push(await record("WASM startup", async () => { const value = await wait((s) => s.source === "linuxcnc-wasm" && s.motion === "wasm", "wasm_startup"); if (value.authority !== "linuxcnc-wasm-worker") throw new Error("authority_marker_missing"); }));
|
||||
results.push(await record("OPEN G-CODE FILE", async () => {
|
||||
const fileInput = main.locator('input[type="file"][aria-label="Open G-code file"]');
|
||||
if (!await fileInput.count()) throw new Error("file_input_missing");
|
||||
await fileInput.setInputFiles({ name: "browser-panel-audit.ngc", mimeType: "text/plain", buffer: Buffer.from("G21\nG0 X1 Y1 Z1\nM2\n") });
|
||||
await wait((s) => s.program === "loaded" && s.command?.includes("PROGRAM_LOAD acknowledged") && s.error === null, "open_file_ack");
|
||||
await wait((s) => s.command?.includes("PROGRAM_LOAD acknowledged") && s.error === null, "open_file_status");
|
||||
await click(main.locator('button[aria-label="Load exact XYZAC TRT case"]'), "restore_case_after_open");
|
||||
await wait((s) => s.program === "loaded" && s.case?.includes("xyzac_switchkins") && s.error === null, "restore_case_after_open_ack");
|
||||
}));
|
||||
results.push(await record("PACKAGED PROGRAM LOAD", async () => {
|
||||
await click(main.getByRole("button", { name: "Load 3D_Chips.ngc", exact: true }), "load_3d_chips");
|
||||
await wait((s) => s.program === "loaded" && s.command?.includes("PROGRAM_LOAD acknowledged") && s.error === null, "load_3d_chips_ack");
|
||||
await click(main.locator('button[aria-label="Load exact XYZAC TRT case"]'), "restore_case_after_packaged");
|
||||
await wait((s) => s.program === "loaded" && s.case?.includes("xyzac_switchkins") && s.error === null, "restore_case_after_packaged_ack");
|
||||
}));
|
||||
results.push(await record("POWER ON", async () => { await click(main.locator('button[aria-label="Power on [F2]"]'), "power_on"); await wait((s) => s.machine === "Machine ON", "power_state"); }));
|
||||
results.push(await record("HOME ALL", async () => { await click(main.locator('button[aria-label="Home all axes"]'), "home_all"); await wait((s) => s.homed === "[true,true,true,true,true]", "home_state"); }));
|
||||
results.push(await record("RESET", async () => { await click(main.locator('button[aria-label="Reset machine"]'), "reset"); await wait((s) => s.command?.includes("RESET acknowledged") || s.error === null, "reset_state"); }));
|
||||
results.push(await record("KINEMATICS IDENTITY/TCP/USERK", async () => {
|
||||
for (const name of ["Identity", "TCP: XYZAC", "User K", "Identity", "TCP: XYZAC", "User K"]) {
|
||||
await click(main.getByRole("button", { name: new RegExp(`^${name}`) }), `kinematics_${name}`);
|
||||
await wait((s) => s.error === null, `kinematics_${name}_ack`);
|
||||
}
|
||||
// LinuxCNC only exposes the manual XYZAC controls for the identity
|
||||
// kinematics in this selected WASM configuration; leave the panel in
|
||||
// that authoritative mode for the following manual-control checks.
|
||||
await click(main.getByRole("button", { name: /^Identity/ }), "kinematics_restore_identity");
|
||||
await wait((s) => s.error === null, "kinematics_restore_identity_ack");
|
||||
}));
|
||||
results.push(await record("VIEW buttons", async () => {
|
||||
for (const name of ["Zoom in", "Zoom out", "Top view", "Rotated top view", "Side view", "Front view", "Inverted front view", "Perspective view", "Drag / rotate mode"]) await click(main.locator(`button[aria-label="${name}"]`), `view_${name}`);
|
||||
}));
|
||||
results.push(await record("VISUAL CLEAR", async () => { await click(main.locator('button[aria-label="Clear live plot [Ctrl-K]"]'), "visual_clear"); await wait((s) => s.error === null, "visual_clear_ack"); }));
|
||||
results.push(await record("MANUAL MODE", async () => { await main.getByRole("tab", { name: "Manual Control [F3]" }).click(); await wait((s) => s.mode === "MANUAL" || s.command?.includes("SET_MODE acknowledged"), "manual_mode_ack"); }));
|
||||
results.push(await record("JOG XYZAC", async () => {
|
||||
for (const axis of ["X", "Y", "Z", "A", "C"]) {
|
||||
const axisInput = main.locator(`input[name="axis"][value="${axis.toLowerCase()}"]`);
|
||||
if (!await axisInput.count()) throw new Error(`axis_${axis}:missing`);
|
||||
await axisInput.check();
|
||||
await click(main.locator(`button[aria-label="Jog ${axis} positive"]`), `jog_${axis}_positive`);
|
||||
await wait((s) => s.error === null, `jog_${axis}_ack`);
|
||||
await click(main.getByRole("button", { name: "Stop", exact: true }).first(), `jog_${axis}_stop`);
|
||||
}
|
||||
}));
|
||||
results.push(await record("SPINDLE", async () => {
|
||||
await click(main.locator('button[aria-label="Spindle clockwise"]'), "spindle_cw");
|
||||
await click(main.locator('button[aria-label="Spindle counter-clockwise"]'), "spindle_ccw");
|
||||
await click(main.getByRole("button", { name: "Stop", exact: true }).nth(1), "spindle_stop");
|
||||
const brake = main.getByLabel("Brake", { exact: true });
|
||||
if (await brake.count() && !await brake.isDisabled()) {
|
||||
await brake.click();
|
||||
await wait((s) => s.error === null && s.command?.includes("SPINDLE_BRAKE acknowledged"), "spindle_brake_toggle_ack");
|
||||
await brake.click();
|
||||
await wait((s) => s.error === null && s.command?.includes("SPINDLE_BRAKE acknowledged"), "spindle_brake_restore_ack");
|
||||
}
|
||||
}));
|
||||
results.push(await record("COOLANT", async () => {
|
||||
for (const name of ["Mist", "Flood"]) {
|
||||
const box = main.getByLabel(name, { exact: true });
|
||||
if (await box.count()) {
|
||||
if (await box.isDisabled()) throw new Error(`${name}:disabled`);
|
||||
await box.click({ force: true });
|
||||
await wait((s) => s.error === null && s.command?.includes("COOLANT_COMMAND acknowledged"), `${name}_toggle_ack`);
|
||||
await box.click({ force: true });
|
||||
await wait((s) => s.error === null && s.command?.includes("COOLANT_COMMAND acknowledged"), `${name}_restore_ack`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
results.push(await record("TOOL/PROBE", async () => {
|
||||
for (const name of ["Prepare T1", "Change", "Arm Probe", "Disarm"]) {
|
||||
const clicked = await click(main.getByRole("button", { name, exact: true }), `device_${name}`, { required: false });
|
||||
if (clicked) await wait((s) => s.error === null, `device_${name}_ack`);
|
||||
}
|
||||
}));
|
||||
results.push(await record("OVERRIDES", async () => {
|
||||
for (const [name, value] of [["Jog speed", "50"], ["Feed override", "50"], ["Spindle override", "50"], ["Max velocity", "50"]]) {
|
||||
const input = main.locator(`input[aria-label="${name}"]`);
|
||||
if (await input.count() && !await input.isDisabled()) {
|
||||
await input.evaluate((element, next) => { element.value = String(next); element.dispatchEvent(new Event("input", { bubbles: true })); element.dispatchEvent(new Event("change", { bubbles: true })); }, value);
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
}));
|
||||
results.push(await record("MDI", async () => { await main.getByRole("tab", { name: "MDI [F5]" }).click(); await wait((s) => s.mode === "MDI" || s.command?.includes("SET_MODE acknowledged"), "mdi_mode_ack"); const input = main.locator('.mdi-control input'); await input.fill("G0 X1"); await click(main.getByRole("button", { name: "Go", exact: true }), "mdi_go"); await wait((s) => s.error === null && s.command?.includes("MDI_EXECUTE acknowledged") && s.queue === "0", "mdi_ack"); await main.getByRole("tab", { name: "Manual Control [F3]" }).click(); await wait((s) => s.mode === "MANUAL" || s.command?.includes("SET_MODE acknowledged"), "manual_mode_ack"); }));
|
||||
results.push(await record("PROGRAM LOAD/RUN/PAUSE/RESUME/ABORT/RESTART/STEP", async () => {
|
||||
await main.getByRole("tab", { name: "Manual Control [F3]" }).click();
|
||||
await wait((s) => s.queue === "0" && s.error === null, "program_preflight_idle");
|
||||
await page.waitForTimeout(1000);
|
||||
await click(main.locator(".program-actions button").first(), "program_load");
|
||||
await wait((s) => s.program === "loaded" && s.queue === "0" && s.runEnabled === "true" && !s.programLoading && s.error === null, "program_loaded");
|
||||
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "program_run");
|
||||
await wait((s) => s.program === "running", "program_running");
|
||||
await click(main.locator('button[aria-label="Pause execution [P]"]'), "program_pause");
|
||||
await wait((s) => s.program === "paused", "program_paused");
|
||||
await click(main.locator('button[aria-label="Resume execution [S]"]'), "program_resume");
|
||||
await wait((s) => s.program === "running", "program_resumed");
|
||||
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "program_abort");
|
||||
await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "program_aborted");
|
||||
await click(main.locator(".program-actions button").first(), "program_reload_before_step");
|
||||
await wait((s) => s.program === "loaded" && s.error === null, "program_reload_before_step_ack");
|
||||
await click(main.locator(".program-actions button").nth(2), "program_step");
|
||||
await wait((s) => s.program === "paused", "program_stepped");
|
||||
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "program_step_abort");
|
||||
await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "program_step_abort_ack");
|
||||
await click(main.locator(".program-actions button").first(), "program_reload_before_rerun");
|
||||
await wait((s) => s.program === "loaded" && s.error === null, "program_reload_before_rerun_ack");
|
||||
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "program_run_again");
|
||||
await wait((s) => s.program === "running", "program_running_again");
|
||||
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "program_abort_again");
|
||||
await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program) && s.error === null, "program_abort_again_ack");
|
||||
}));
|
||||
results.push(await record("RUN STARTUP SEQUENCE", async () => {
|
||||
await click(main.locator(".program-actions button").first(), "startup_sequence_reload");
|
||||
await wait((s) => s.program === "loaded" && s.queue === "0" && s.runEnabled === "true" && !s.programLoading && s.error === null, "startup_sequence_reload_ack");
|
||||
await click(main.getByRole("button", { name: "Run startup sequence", exact: true }), "startup_sequence");
|
||||
await wait((s) => s.program === "running", "startup_sequence_running");
|
||||
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "startup_sequence_abort");
|
||||
await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program) && s.error === null, "startup_sequence_abort_ack");
|
||||
}));
|
||||
results.push(await record("EDITOR PROGRAM LOAD + REOPEN", async () => {
|
||||
const editor = main.locator("#axis-program");
|
||||
await editor.fill("G21\nG1 X2 Y2 Z2 F100\nM2\n");
|
||||
await click(main.locator(".program-actions button").first(), "editor_program_load");
|
||||
await wait((s) => s.program === "loaded" && s.command?.includes("PROGRAM_LOAD acknowledged") && s.error === null, "editor_program_load_ack");
|
||||
await click(main.locator('button[aria-label="Reopen editor defaults"]'), "reopen_defaults");
|
||||
await wait((s) => s.program === "loaded" && s.case?.includes("xyzac_switchkins") && s.error === null, "reopen_defaults_ack");
|
||||
}));
|
||||
results.push(await record("DISABLED unsupported buttons", async () => {
|
||||
for (const name of ["Toggle skip lines with slash", "Toggle optional pause"]) if (!await main.locator(`button[aria-label="${name}"]`).isDisabled()) throw new Error(`${name}:must_be_disabled`);
|
||||
}));
|
||||
} finally {
|
||||
const buttons = await main.locator("button").evaluateAll((items) => items.map((button) => ({ text: button.textContent?.trim() ?? "", aria: button.getAttribute("aria-label"), disabled: button.disabled })));
|
||||
await page.screenshot({ path: "/tmp/xyzac-panel-buttons-browser.png", fullPage: true });
|
||||
console.log(JSON.stringify({ schema: "xyzac-panel-buttons-browser.v1", status: failures.length === 0 && browserErrors.length === 0 ? "verified" : "failed", url, results, button_count: buttons.length, disabled_button_count: buttons.filter((button) => button.disabled).length, failures, browser_errors: browserErrors, final_state: await state(), screenshot: "/tmp/xyzac-panel-buttons-browser.png" }));
|
||||
await browser.close();
|
||||
}
|
||||
@@ -49,7 +49,7 @@ function MachineSequence({ wasmReady, isEstop, machineOn, allHomed, programLoade
|
||||
<TextButton disabled={!canPower} onClick={onPower} ariaLabel={machineOn ? "Power off" : "Power on"}>{machineOn ? "Power Off" : "Power On"}</TextButton>
|
||||
<TextButton disabled={!canHome} onClick={onHomeAll} ariaLabel="Home all axes">HOME ALL</TextButton>
|
||||
<TextButton disabled={!canRun || programLoading} onClick={onRun} ariaLabel="Run program">Run Program</TextButton>
|
||||
<TextButton feature="AXIS-MACHINE-SEQUENCE" disabled={!programLoaded || programRunning || programLoading} onClick={onSequence} ariaLabel="Run startup sequence">Run Startup Sequence</TextButton>
|
||||
<TextButton feature="AXIS-MACHINE-SEQUENCE" disabled={!programLoaded || !canRun || programRunning || programLoading} onClick={onSequence} ariaLabel="Run startup sequence">Run Startup Sequence</TextButton>
|
||||
</div>
|
||||
</section>;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export function App() {
|
||||
const [casePath, setCasePath] = useState<string>(DEFAULT_CASE);
|
||||
const [caseStatus, setCaseStatus] = useState("ready to load");
|
||||
const [commandStatus, setCommandStatus] = useState("Idle");
|
||||
const [programText, setProgramText] = useState(DEFAULT_PROGRAM);
|
||||
const [programText, setProgramTextState] = useState(DEFAULT_PROGRAM);
|
||||
const [programPath, setProgramPath] = useState(PROGRAM_PATH);
|
||||
const [programLoading, setProgramLoading] = useState(false);
|
||||
const [selectedAxis, setSelectedAxis] = useState("x");
|
||||
@@ -213,7 +213,7 @@ export function App() {
|
||||
const stopJogTimer = () => { if (jogTimer.current) { clearInterval(jogTimer.current); jogTimer.current = null; } };
|
||||
const startJogTimer = () => { if (jogTimer.current) return; jogTimer.current = setInterval(() => { if (jogStepPending.current) return; jogStepPending.current = true; operationController.execute("SIM_STEP", { cycles: 1, response: "motion_delta" }, { state: axisState, gate: false }).catch(() => undefined).finally(() => { jogStepPending.current = false; }); }, 50); };
|
||||
const startRunTimer = () => { if (runTimer.current) return; runTimer.current = setInterval(() => { if (runStepPending.current) return; runStepPending.current = true; operationController.execute("SIM_STEP", { cycles: 5, response: "motion_delta" }, { state: axisState, gate: false }).catch(() => undefined).finally(() => { runStepPending.current = false; }); }, 50); };
|
||||
const dispatch = (kind: string, payload: Record<string, unknown> = {}, gate = true, action: string | null = null): Promise<void> => {
|
||||
const dispatch = async (kind: string, payload: Record<string, unknown> = {}, gate = true, action: string | null = null): Promise<void> => {
|
||||
setCommandStatus(`${kind} pending`);
|
||||
// The AXIS task stops issuing servo/task polls before changing the
|
||||
// interpreter state. A SIM_STEP already in flight is allowed to finish,
|
||||
@@ -221,6 +221,13 @@ export function App() {
|
||||
// protocol sequence reserved for RESUME.
|
||||
if (kind === "JOG_STOP") stopJogTimer();
|
||||
if (["PROGRAM_PAUSE", "PROGRAM_ABORT"].includes(kind)) stopRunTimer();
|
||||
// AXIS stops its periodic stat/LivePlotter update before AUTO_PAUSE,
|
||||
// AUTO_RESUME and abort. Wait for the single already-issued WASM tick to
|
||||
// settle so the strict protocol sequence cannot interleave a control edge
|
||||
// with a servo/task sample.
|
||||
if (["PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT"].includes(kind)) {
|
||||
while (runStepPending.current) await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
const pending = operationController.execute(kind, payload, { state: axisState, gate, ...(action ? { action } : {}) });
|
||||
setOperationTrace(operationController.exportTrace());
|
||||
return pending.then(() => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN" || kind === "PROGRAM_RESUME") startRunTimer(); if (["PROGRAM_PAUSE", "PROGRAM_ABORT", "PROGRAM_RESTART"].includes(kind)) stopRunTimer(); if (kind === "JOG_START") startJogTimer(); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => {
|
||||
@@ -237,7 +244,7 @@ export function App() {
|
||||
const releaseEstop = () => dispatch("ESTOP", { active: false }, false);
|
||||
const toggleEstop = () => { if (!isEstop) { emergencyStop(); return; } void releaseEstop().catch(() => undefined); };
|
||||
const powerOn = () => { if (!gates.power.enabled) return; void dispatch("POWER", { active: isEstop ? false : !machineOn }, false).catch(() => undefined); };
|
||||
const resetMachine = () => { stopRunTimer(); stopJogTimer(); if (isEstop || programRunning || !machineOn) return; void dispatch("RESET").catch(() => undefined); };
|
||||
const resetMachine = () => { stopRunTimer(); stopJogTimer(); if (!gates.reset.enabled) return; void dispatch("RESET").catch(() => undefined); };
|
||||
const homeAll = () => { void dispatch("HOME", { all: true }).catch(() => undefined); };
|
||||
const runStartupSequence = () => {
|
||||
if (programLoading) return;
|
||||
@@ -314,6 +321,17 @@ export function App() {
|
||||
setCaseStatus(`blocked: ${error instanceof Error ? error.message : "case_load_failed"}`);
|
||||
} finally { setProgramLoading(false); }
|
||||
};
|
||||
const reopenDefaults = () => {
|
||||
if (programLoading || programRunning || !gates.load.enabled) return;
|
||||
void loadCase();
|
||||
};
|
||||
// The legacy menu/toolbar handlers call setProgramText(DEFAULT_PROGRAM).
|
||||
// Route that action through the same WASM-backed reopen transaction instead
|
||||
// of changing only a local editor string.
|
||||
const setProgramText = (value: string) => {
|
||||
if (value === DEFAULT_PROGRAM) { reopenDefaults(); return; }
|
||||
setProgramTextState(value);
|
||||
};
|
||||
const loadPackagedProgram = async () => {
|
||||
if (programLoading || programRunning) return;
|
||||
const path = "linuxcnc/nc_files/3D_Chips.ngc";
|
||||
@@ -397,6 +415,13 @@ export function App() {
|
||||
}
|
||||
};
|
||||
const enterManualMode = () => { setLeftTab("manual"); if (String(axisState.task.mode).toUpperCase() !== "MANUAL") void dispatch("SET_MODE", { mode: "MANUAL" }, false).catch(() => undefined); };
|
||||
const enterMdiMode = () => { setLeftTab("mdi"); if (String(axisState.task.mode).toUpperCase() !== "MDI") void dispatch("SET_MODE", { mode: "MDI" }, false).catch(() => undefined); };
|
||||
useEffect(() => {
|
||||
if (!wasmSnapshotReady || programRunning || programPaused || programLoading || leftTab === "diagnostics") return;
|
||||
const desiredMode = leftTab === "mdi" ? "MDI" : "MANUAL";
|
||||
if (String(axisState.task.mode).toUpperCase() === desiredMode) return;
|
||||
void dispatch("SET_MODE", { mode: desiredMode }, false).catch(() => undefined);
|
||||
}, [leftTab, axisState.task.mode, wasmSnapshotReady, programRunning, programPaused, programLoading]);
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
@@ -419,15 +444,18 @@ export function App() {
|
||||
useEffect(() => {
|
||||
const wasmQueueDepth = Number((state.snapshot?.wasm_motion as { state?: { queue_depth?: number } } | undefined)?.state?.queue_depth ?? 0);
|
||||
const programComplete = String(state.program?.program_status ?? "") === "completed" && wasmQueueDepth === 0;
|
||||
if (programRunning && !programComplete) startRunTimer(); else stopRunTimer();
|
||||
const mdiMotionActive = !programRunning && String(state.task?.mode ?? "").toUpperCase() === "MDI" && wasmQueueDepth > 0;
|
||||
if ((programRunning && !programComplete) || mdiMotionActive) startRunTimer(); else stopRunTimer();
|
||||
if (!ready) { stopRunTimer(); stopJogTimer(); }
|
||||
}, [programRunning, ready, state.program?.program_status, state.snapshot?.wasm_motion]);
|
||||
}, [programRunning, ready, state.program?.program_status, state.task?.mode, state.snapshot?.wasm_motion]);
|
||||
const spindleState = (state.devices as Record<string, any> | null)?.spindles?.[0] as Record<string, any> | undefined;
|
||||
const spindleOn = spindleState ? Boolean(spindleState.enabled) : false;
|
||||
const spindleBrake = spindleState ? Boolean(spindleState.brake) : false;
|
||||
const ioState = (state.snapshot?.io as Record<string, any> | undefined)?.state as Record<string, any> | undefined;
|
||||
const manualReady = wasmSnapshotReady && Boolean(ioState?.ready) && !Boolean(ioState?.estop);
|
||||
const coolant = { mist: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.mist_requested), flood: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.flood_requested) };
|
||||
const deviceIoEnvelope = state.deviceIo as Record<string, any> | null;
|
||||
const deviceIoCanonical = (deviceIoEnvelope?.state as Record<string, any> | undefined) ?? deviceIoEnvelope;
|
||||
const coolant = { mist: Boolean(deviceIoCanonical?.coolant?.mist ?? deviceIoCanonical?.coolant?.mist_requested), flood: Boolean(deviceIoCanonical?.coolant?.flood ?? deviceIoCanonical?.coolant?.flood_requested) };
|
||||
const onSpindle = (direction: number) => { execute("SPINDLE_COMMAND", { spindle: 0, direction, rpm: direction === 0 ? 0 : 600, enabled: direction !== 0 }); };
|
||||
const onCoolant = (key: "mist" | "flood") => { const next = { ...coolant, [key]: !coolant[key] }; execute("COOLANT_COMMAND", { mist: next.mist, flood: next.flood, lube: false }); };
|
||||
const wasmMotion = (state.snapshot?.wasm_motion as { state?: Record<string, unknown> } | undefined)?.state ?? {};
|
||||
@@ -465,15 +493,15 @@ export function App() {
|
||||
const allHomed = homedAxes.length >= 5 && homedAxes.slice(0, 5).every(Boolean);
|
||||
const xyzacCommand = (kind: string, payload: Record<string, unknown> = {}, action?: string) => { void dispatch(kind, payload, true, action ?? null).then(() => { if (kind === "VISUAL_CLEAR") setPlotCleared(true); }).catch(() => undefined); };
|
||||
const position = xyzacView.pose ? `X${Number(xyzacView.pose.x).toFixed(3)} Y${Number(xyzacView.pose.y).toFixed(3)} Z${Number(xyzacView.pose.z).toFixed(3)} A${Number(xyzacView.pose.a).toFixed(3)} C${Number(xyzacView.pose.c).toFixed(3)}` : "WASM state pending";
|
||||
return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-authority="linuxcnc-wasm-worker" data-axis-state-source={wasmSnapshotReady ? "linuxcnc-wasm" : "pending"} data-axis-motion-authority={String(state.snapshot?.motion_authority ?? "pending")} data-axis-task-authority={String(state.snapshot?.task_authority ?? "pending")} data-axis-tp-queue-depth={String((wasmMotion as { queue_depth?: number }).queue_depth ?? "")} data-axis-tp-velocity={String((wasmMotion as { velocity?: number }).velocity ?? "")} data-axis-tp-active-id={String((wasmMotion as { active_id?: number }).active_id ?? "")} data-axis-current-line={String(visibleCurrentLine ?? "")} data-axis-run-enabled={String(gates.run.enabled)} data-axis-homed={JSON.stringify(state.snapshot?.motion?.homed_axes ?? state.snapshot?.motion?.homed ?? null)} data-axis-program-status={String(state.program?.program_status ?? "idle")}>
|
||||
return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-authority="linuxcnc-wasm-worker" data-axis-state-source={wasmSnapshotReady ? "linuxcnc-wasm" : "pending"} data-axis-motion-authority={String(state.snapshot?.motion_authority ?? "pending")} data-axis-task-authority={String(state.snapshot?.task_authority ?? "pending")} data-axis-task-mode={String(taskRecord.mode ?? "pending")} data-axis-tp-queue-depth={String((wasmMotion as { queue_depth?: number }).queue_depth ?? "")} data-axis-tp-velocity={String((wasmMotion as { velocity?: number }).velocity ?? "")} data-axis-tp-active-id={String((wasmMotion as { active_id?: number }).active_id ?? "")} data-axis-current-line={String(visibleCurrentLine ?? "")} data-axis-run-enabled={String(gates.run.enabled)} data-axis-homed={JSON.stringify(state.snapshot?.motion?.homed_axes ?? state.snapshot?.motion?.homed ?? null)} data-axis-program-status={String(state.program?.program_status ?? "idle")}>
|
||||
<header className="axis-window-header" data-axis-feature="AXIS-SHELL-01"><div className="axis-brand"><img src={axisIcon("axis-24x24.png")} alt="AXIS" /> <strong>AXIS</strong><span>LinuxCNC Web/WASM</span></div><span className={`connection-state ${state.status}`} data-axis-feature="AXIS-SHELL-01" role="status">{state.status.toUpperCase()}</span><span className="axis-state-badge" data-axis-state={taskState} aria-label={`Machine ${machineState}, operation ${operationState}`}>{taskState}</span></header>
|
||||
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); }} disabled={programRunning || programLoading}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn} disabled={!gates.power.enabled}>{machineOn ? "Power off" : "Power on"}</MenuAction><MenuAction onClick={resetMachine} disabled={!gates.reset.enabled}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram} disabled={!gates.run.enabled || programLoading}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled || programLoading}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled || programLoading}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled || programLoading}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram} disabled={!gates.abort.enabled}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode} disabled={programRunning || programLoading}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")} disabled={programRunning || programLoading}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
|
||||
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); }} disabled={programRunning || programLoading}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn} disabled={!gates.power.enabled}>{machineOn ? "Power off" : "Power on"}</MenuAction><MenuAction onClick={resetMachine} disabled={!gates.reset.enabled}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram} disabled={!gates.run.enabled || programLoading}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled || programLoading}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled || programLoading}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled || programLoading}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram} disabled={!gates.abort.enabled}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode} disabled={programRunning || programLoading}>Manual Control</MenuAction><MenuAction onClick={enterMdiMode} disabled={programRunning || programLoading}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
|
||||
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label={machineOn ? "Power off [F2]" : "Power on [F2]"} asset="tool_power.gif" disabled={!gates.power.enabled} onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" disabled={!gates.reset.enabled} onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" disabled={programRunning || programLoading} onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" disabled /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" disabled /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /></section>
|
||||
<section className="axis-case-strip" data-axis-feature="AXIS-EDIT-03"><label>Simulation case<select data-axis-feature="AXIS-EDIT-03" value={casePath} onChange={(event) => setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path === SELECTED_XYZAC_CASE).map((entry) => <option key={entry.path} value={entry.path}>{entry.label} | XYZAC TRT</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase} disabled={!gates.load.enabled || 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} />
|
||||
<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={enterMdiMode}>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-panel-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>
|
||||
<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>
|
||||
<input ref={programFileInput} className="visually-hidden-file-input" type="file" accept=".ngc,.nc,.gcode,.tap,text/plain" onChange={onProgramFile} aria-label="Open G-code file" />
|
||||
|
||||
@@ -61,7 +61,10 @@ export type SimSnapshotV1 = {
|
||||
authoritative?: { state_hash?: string; planner?: TrajectorySnapshot; kinematics?: Record<string, unknown> };
|
||||
coordinates?: { machine?: number[]; relative?: number[]; g5x?: number; g92?: number[]; tool_offset?: number[] };
|
||||
spindle?: Array<{ enabled?: boolean; direction?: number; commanded?: number; actual?: number; at_speed?: boolean }>;
|
||||
io?: { coolant?: number; lube?: boolean; digital?: number[]; analog?: number[]; probe?: boolean };
|
||||
// XYZAC panel snapshots carry the canonical LinuxCNC WASM IO envelope
|
||||
// (`{ schema, source, state, ... }`). Generic legacy cases may still add
|
||||
// coolant/probe fields, so keep this boundary open to both shapes.
|
||||
io?: Record<string, unknown>;
|
||||
tool?: { current?: number; prepared?: number; pocket?: number; change_state?: string };
|
||||
overrides?: { feed?: number; rapid?: number; spindle?: number[]; adaptive?: number; feed_hold?: boolean };
|
||||
hal?: { graph_revision?: number; changed_values?: ArrayBuffer };
|
||||
|
||||
@@ -104,6 +104,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
||||
joints,
|
||||
},
|
||||
wasm_motion: (result.wasm_motion as Record<string, unknown>) ?? previous.snapshot.wasm_motion,
|
||||
io: (result.io as Record<string, unknown>) ?? previous.snapshot.io,
|
||||
...(visual ? { visual } : {}),
|
||||
} satisfies SimSnapshotV1;
|
||||
return {
|
||||
@@ -158,6 +159,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
||||
visual: { ...panel.visual, toolpath: visualSegments },
|
||||
kinematics: panel.kinematics,
|
||||
xyzac: panel.xyzac,
|
||||
io: panel.io ?? previous.snapshot?.io,
|
||||
gates: panel.gates,
|
||||
motion_authority: panel.motion_authority,
|
||||
task_authority: panel.task_authority,
|
||||
|
||||
@@ -549,6 +549,9 @@ export class SimSession {
|
||||
});
|
||||
if (result === null) return { action, enabled: false, reason: "linuxcnc_wasm_gate_unavailable", taskState: task.state, interpState: "UNKNOWN" };
|
||||
let reason = result === 0 ? "linuxcnc_wasm_authority" : WASM_GATE_REASONS[result] ?? "linuxcnc_wasm_gate_unknown_command";
|
||||
if (input.kind === "PROGRAM_RUN" && Number(program.status) === 5) {
|
||||
return { action, enabled: false, reason: "xyzac_program_aborted_requires_restart", taskState: task.state, interpState: "IDLE", wasm: { io_ready: ready, io_estop: estop, program_status: program.program_status, queue_depth: wasmMotion.state.queue_depth, gate_result: result } };
|
||||
}
|
||||
if (result === 2 && input.kind === "POWER" && input.payload?.active !== true) reason = "xyzac_power_already_off";
|
||||
if (result === 3 && input.kind === "POWER") reason = "xyzac_power_already_on";
|
||||
if (result === 4 && input.kind === "MDI_EXECUTE") reason = "mdi_while_program_active";
|
||||
@@ -1953,7 +1956,14 @@ export class SimSession {
|
||||
if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
|
||||
if (result.status === "verified") { const subroutines = this.loadXyzacSubroutines(input.payload?.path); if (subroutines.status !== "verified") return finish(this.error(input, "program_subroutine", subroutines.code), "failed"); }
|
||||
const preview = result.status === "verified" ? this.xyzacProgram.preview() : result;
|
||||
if (result.status === "verified" && preview.status === "verified") { this.previewState = preview; this.task.setProgram("loaded", 0); }
|
||||
if (result.status === "verified" && preview.status === "verified") {
|
||||
this.previewState = preview;
|
||||
// AXIS open_file_guts()/reload_file() leaves the task usable for
|
||||
// AUTO single-step after an earlier abort. Recover the task edge
|
||||
// before publishing the newly loaded interpreter state.
|
||||
if (this.task.state.state === "STOPPED") this.task.recover();
|
||||
this.task.setProgram("loaded", 0);
|
||||
}
|
||||
const finalStatus = result.status === "verified" && preview.status === "verified" ? "verified" : "error";
|
||||
return finish(this.envelope(finalStatus === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, preview, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), finalStatus);
|
||||
}
|
||||
@@ -2116,6 +2126,13 @@ export class SimSession {
|
||||
if (current.status === 1) this.xyzacProgram.start();
|
||||
const result = this.xyzacProgram.step();
|
||||
if (result.status === "verified") {
|
||||
// AXIS AUTO_STEP returns to an interpreter-paused state after the
|
||||
// single canonical block. Keep the WASM interpreter status and
|
||||
// task status aligned so the Resume gate is authoritative.
|
||||
if (result.program_status !== 4) {
|
||||
const pauseResult = this.xyzacProgram.pause();
|
||||
if (pauseResult.status !== "verified") return finish(this.error(input, "program_pause", pauseResult.code ?? "program_pause_failed"), "failed");
|
||||
}
|
||||
if (this.task.state.state === "READY") this.task.transition("RUNNING");
|
||||
if (result.motion) this.wasmMotion.enqueue(result.motion);
|
||||
this.wasmMotion.setPaused(true);
|
||||
@@ -2175,6 +2192,11 @@ export class SimSession {
|
||||
const block = input.payload?.block ?? input.payload?.command;
|
||||
if (this.xyzacProgram) {
|
||||
this.task.setMode("MDI");
|
||||
// AXIS send_mdi_command() leaves the interpreter/TP runnable after
|
||||
// a previous manual jog stop. JOG_STOP intentionally pauses the
|
||||
// WASM TP, so clear that transport pause before queuing MDI motion;
|
||||
// the queue itself remains authoritative for execution.
|
||||
this.wasmMotion.setPaused(false);
|
||||
const result = this.xyzacProgram.executeBlock(block);
|
||||
if (result.status === "verified") {
|
||||
const controlCode = Number(result.program?.control_code ?? 0);
|
||||
@@ -2233,7 +2255,11 @@ export class SimSession {
|
||||
if (this.motionAuthority === "wasm") {
|
||||
if (input.kind === "JOG_STOP") {
|
||||
this.wasmMotion.abort();
|
||||
this.wasmMotion.setPaused(true);
|
||||
// AXIS jog_off() stops the active jog, but it does not leave the
|
||||
// task/TP in AUTO_PAUSE. Keep the LinuxCNC TP runnable so an
|
||||
// already queued servo sample, MDI block, or spindle/IO command
|
||||
// cannot surface a synthetic linuxcnc_tp_paused scheduler error.
|
||||
this.wasmMotion.setPaused(false);
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
if (input.kind === "SET_OVERRIDE") {
|
||||
|
||||
@@ -67,13 +67,16 @@ assert.equal(sent.length, 2);
|
||||
assert.deepEqual(controller.exportTrace().map((item) => item.phase), ["pending", "result", "pending", "result"]);
|
||||
|
||||
const appSource = await readFile(new URL("../react/src/App.tsx", import.meta.url), "utf8");
|
||||
const ironLaw = await readFile(new URL("../../docs/linuxcnc-axis-web-iron-law.md", import.meta.url), "utf8");
|
||||
for (const source of [ironLaw, appSource]) assert.ok(source.includes("linuxcnc"), "axis_source_authority");
|
||||
for (const symbol of ["manual_ok()", "PROGRAM_RUN/PAUSE/RESUME/ABORT", "JOG_START/JOG_STOP", "MDI_EXECUTE", "COOLANT_COMMAND", "blocked_by_abi"]) assert.ok(ironLaw.includes(symbol), `iron_law_${symbol}`);
|
||||
const panelSource = await readFile(new URL("../react/src/XyzacTrtPanel.tsx", import.meta.url), "utf8");
|
||||
const contractSource = await readFile(new URL("../react/src/xyzac-panel-contract.ts", 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 combined = `${appSource}\n${panelSource}\n${contractSource}`;
|
||||
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", "reopenDefaults", "gates.reset.enabled", "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));
|
||||
assert.ok(appSource.includes("SELECTED_XYZAC_CASE"));
|
||||
|
||||
Reference in New Issue
Block a user