按关节槽位驱动 13 个五轴案例的三维模型
关节契约扩为槽位制:以 LinuxCNC 联合号标识关节,轴字母降为槽位属性, 重复轴(bridge-mill 的两个 Y)、并联杆长(hexapod/pentapod)和被动槽位 (max5 第 6 关节)都能表达。v1 规则保持不变并合成 jointOrder,已保存 GLB 与既有测试不受影响。 13 个结构 GLB 由 scripts/generate_physical_case_glbs.mjs 统一重建, 可动关节均为按槽位命名的 Group 并写 userData.linuxcncJoint,共 71 个 关节,槽位数与运行目录的 joint_count 逐条一致。 新增 machine-visual-bindings.json 与纯函数模块,以 machine id 为键: 坐标字母不能作键,因为三个案例共用 XYZAB、三个共用 XYZABC。主界面切换 案例时自动装载对应模型,失败时保留内置模型并显式提示。 消除前端五关节截断:visual 适配器的 joints 改为数组并新增 jointsAuthoritative;allHomed 按 joint_count 判断;面板按 worker 断言的 machine id 查找机型。 管理器解锁:坐标与案例下拉以"存在完整视觉绑定"为启用条件,13 案例全部 可选;machine-urdf.ts 新增 generic-wasm/1 profile,AC/BC 严格校验路径 不变;新增 machine-kinematics-specs.json 保存逐关节限位,INI 未声明的 限位保持 null 而不编造。 修复三个真实缺陷:构建漏发 woodpecker/gmoccapy/qtdragon 配置目录( DEFECT-02 根因);案例切换沿用"装载需要已上电"的门控,导致关机状态下 第二次切换被静默拒绝;重复轴机型上 React 列表键冲突。 验收:13 案例逐个切换装载成功、槽位键与绑定定义逐一一致、浏览器错误 0; typecheck 与生产构建通过;视觉绑定、GLB 命名、关节契约、投影、store、 双画布、运动学规格漂移、11 案例 Worker 闭环、管理器浏览器回归全部通过。 证据在 coverage/glb-machine-manager/physical-drive/。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
558
docs/user-manual/LinuxCNC-Web-使用说明书.html
Normal file
558
docs/user-manual/LinuxCNC-Web-使用说明书.html
Normal file
File diff suppressed because one or more lines are too long
45
docs/user-manual/README.md
Normal file
45
docs/user-manual/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 使用说明书(HTML)与发布功能巡检
|
||||
|
||||
本目录存放线上发布站点 `https://49.232.250.35:58010/` 的用户使用说明书和配套的功能测试。
|
||||
|
||||
## 交付物
|
||||
|
||||
- [`LinuxCNC-Web-使用说明书.html`](LinuxCNC-Web-使用说明书.html):单文件中文使用说明书,22 张实测截图以 data URL 内嵌,可离线打开、可打印/导出 PDF,桌面与窄屏均无横向溢出。
|
||||
已随前端产物发布:线上地址 <https://49.232.250.35:58010/manual/>,主页标题栏的"使用说明书"按钮与 Help 菜单指向同一入口。
|
||||
- `manual-content.html`:说明书正文源稿(图片以相对路径引用),修改内容后重新构建即可。
|
||||
- `build_manual.mjs`:把正文中的截图内联,生成最终单文件 HTML。
|
||||
- `capture_deployed_manual.mjs`:线上站点功能巡检脚本,同时采集说明书截图。
|
||||
- `local-walkthrough.mjs`:按说明书顺序在本地发布站点上完整走一遍标准使用流程的演练脚本(`WALKTHROUGH_HEADED=1` 可在桌面开可见窗口逐步演示),输出到 `local-walkthrough/`。
|
||||
- `deployment-test.report.json`:最近一次巡检报告(步骤结果、缺陷、案例矩阵、HTTP 失败记录)。
|
||||
- `local-walkthrough/walkthrough.report.json`:本地发布演练的分步结果与发现。
|
||||
- `screenshots/`:说明书插图原始 PNG。
|
||||
|
||||
## 重新生成
|
||||
|
||||
```bash
|
||||
# 1. 重新巡检线上站点并刷新截图与报告(约 8 分钟)
|
||||
PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs \
|
||||
node docs/user-manual/capture_deployed_manual.mjs
|
||||
|
||||
# 2. 重新构建单文件说明书
|
||||
node docs/user-manual/build_manual.mjs
|
||||
```
|
||||
|
||||
脚本可用 `MANUAL_URL` 指向其它地址(例如本地 `http://127.0.0.1:5174/`),用 `CHROME_PATH` 指定浏览器。
|
||||
|
||||
## 最近一次巡检结论(2026-10-08)
|
||||
|
||||
- 26 个功能步骤全部通过:启动与状态、菜单、RTCP 程序库、机床模型对话框、案例切换、急停/上电/回零、连续与增量 Jog、主轴与冷却液、MDI、DRO、倍率与图层、工具栏"载入精确案例"重新载入、程序运行/暂停/继续/停止、视图、诊断、快捷键、390 px 窄屏布局。
|
||||
- 案例可用性:2026-10-29 复测 13 个案例全部可切换并装载各自结构模型(`coverage/glb-machine-manager/physical-drive/browser.report.json`,13/13,浏览器错误 0)。
|
||||
- 待完成项 0 项。
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 2026-10-08 `DEFECT-01` 已修复:工具栏"载入精确案例"按钮此前把点击事件对象当成案例路径(`simulation_case_unknown`)。现已在 `loadCase` 入口统一把非字符串入参回退为当前案例,线上版本 `20261008c-linuxcnc-wasm-af65b69` 验证点击后正常出现 `Loading` 并回到 `PROGRAM_LOAD acknowledged`。
|
||||
|
||||
注意:重新载入案例会按设计先给机床下电并复位回零状态,重新运行程序前需要重新上电并 HOME ALL。
|
||||
|
||||
## 本地发布演练发现
|
||||
|
||||
- `FINDING-01`(中):MDI 执行完成后(`Planner queued=0`、`Task mode=MDI`),"Load XYZAC boat program" 仍被门控拒绝并提示 `blocked:xyzac_motion_active`,需要切回 Manual Control 页签(发出一条模式命令刷新门控)或刷新页面才能装载程序。属于门控状态刷新滞后,不是 MDI 运动本身的问题。
|
||||
- 2026-10-29 `DEFECT-02` 已修复:`woodpecker`、`gmoccapy`、`qtdragon` 三个案例配置目录进入构建复制清单,案例切换不再要求机床已上电。13 案例逐个切换验收通过,13/13 装载成功、槽位键与案例定义一致、浏览器错误 0。
|
||||
35
docs/user-manual/build_manual.mjs
Normal file
35
docs/user-manual/build_manual.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
// 把 manual-content.html 中的截图内联为 data URL,输出可离线单文件阅读的使用说明书。
|
||||
//
|
||||
// 运行:node docs/user-manual/build_manual.mjs
|
||||
// 输出:docs/user-manual/LinuxCNC-Web-使用说明书.html
|
||||
|
||||
import { readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const contentPath = resolve(here, "manual-content.html");
|
||||
const outputPath = resolve(here, "LinuxCNC-Web-使用说明书.html");
|
||||
|
||||
const source = await readFile(contentPath, "utf8");
|
||||
const references = [...source.matchAll(/src="(screenshots\/[^"]+\.png)"/g)].map((match) => match[1]);
|
||||
const unique = [...new Set(references)];
|
||||
|
||||
let html = source;
|
||||
let totalBytes = 0;
|
||||
for (const reference of unique) {
|
||||
const file = resolve(here, reference);
|
||||
const info = await stat(file).catch(() => null);
|
||||
if (!info) throw new Error(`缺少截图:${reference}`);
|
||||
const base64 = (await readFile(file)).toString("base64");
|
||||
totalBytes += info.size;
|
||||
html = html.replaceAll(`src="${reference}"`, `src="data:image/png;base64,${base64}"`);
|
||||
}
|
||||
|
||||
const remaining = [...html.matchAll(/src="(screenshots\/[^"]+)"/g)];
|
||||
if (remaining.length) throw new Error(`仍有未内联的图片:${remaining.map((match) => match[1]).join(", ")}`);
|
||||
|
||||
await writeFile(outputPath, html);
|
||||
const outputSize = (await stat(outputPath)).size;
|
||||
console.log(`内联 ${unique.length} 张截图(原始 ${(totalBytes / 1024 / 1024).toFixed(2)} MiB)`);
|
||||
console.log(`已生成 ${outputPath}(${(outputSize / 1024 / 1024).toFixed(2)} MiB)`);
|
||||
554
docs/user-manual/capture_deployed_manual.mjs
Normal file
554
docs/user-manual/capture_deployed_manual.mjs
Normal file
@@ -0,0 +1,554 @@
|
||||
// 对线上发布站点做功能巡检并采集使用说明书插图。
|
||||
//
|
||||
// 运行:
|
||||
// PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs \
|
||||
// node docs/user-manual/capture_deployed_manual.mjs
|
||||
//
|
||||
// 可选环境变量:MANUAL_URL(默认线上 58010 站点)、CHROME_PATH。
|
||||
// 输出:docs/user-manual/screenshots/*.png 与 docs/user-manual/deployment-test.report.json
|
||||
//
|
||||
// 结构:
|
||||
// 阶段一 在 XYZAC 上跑完整操作链(上电、回零、Jog、主轴、MDI、程序运行等)。
|
||||
// 阶段二 逐案例探测仿真案例在浏览器中的可用性(每案例独立页面)。
|
||||
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const shotDir = resolve(here, "screenshots");
|
||||
const reportPath = resolve(here, "deployment-test.report.json");
|
||||
const baseUrl = process.env.MANUAL_URL ?? "https://49.232.250.35:58010/";
|
||||
const chromePath = process.env.CHROME_PATH ?? "/usr/bin/google-chrome";
|
||||
const playwrightModule = process.env.PLAYWRIGHT_MODULE;
|
||||
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
const failedRequests = [];
|
||||
const artifacts = [];
|
||||
const defects = [];
|
||||
const caseMatrix = [];
|
||||
|
||||
await mkdir(shotDir, { recursive: true });
|
||||
|
||||
const { chromium } = await import(playwrightModule);
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
args: ["--no-sandbox", "--ignore-certificate-errors", "--enable-unsafe-swiftshader"],
|
||||
});
|
||||
|
||||
const newPage = async () => {
|
||||
const page = await browser.newPage({ ignoreHTTPSErrors: true, viewport: { width: 1440, height: 900 } });
|
||||
page.on("pageerror", (error) => consoleErrors.push({ kind: "pageerror", text: String(error) }));
|
||||
page.on("console", (message) => { if (message.type() === "error") consoleErrors.push({ kind: "console", text: message.text() }); });
|
||||
page.on("requestfailed", (request) => failedRequests.push(`${request.url()} ${request.failure()?.errorText ?? ""}`));
|
||||
page.on("response", (response) => { if (response.status() >= 400) failedRequests.push(`HTTP ${response.status()} ${response.url()}`); });
|
||||
return page;
|
||||
};
|
||||
|
||||
const page = await newPage();
|
||||
|
||||
const step = async (name, body) => {
|
||||
const started = Date.now();
|
||||
try {
|
||||
const detail = await body();
|
||||
results.push({ name, status: "pass", ms: Date.now() - started, detail: detail ?? null });
|
||||
console.log(`PASS ${name}${detail ? ` — ${typeof detail === "string" ? detail : JSON.stringify(detail)}` : ""}`);
|
||||
} catch (error) {
|
||||
results.push({ name, status: "fail", ms: Date.now() - started, detail: String(error?.message ?? error) });
|
||||
console.log(`FAIL ${name} — ${error?.message ?? error}`);
|
||||
}
|
||||
};
|
||||
|
||||
const shot = async (name, { full = false } = {}) => {
|
||||
await page.screenshot({ path: resolve(shotDir, `${name}.png`), fullPage: full });
|
||||
artifacts.push(`${name}.png`);
|
||||
};
|
||||
|
||||
const text = async (selector) => (await page.locator(selector).first().innerText()).replace(/\s+/g, " ").trim();
|
||||
const statusText = () => text(".machine-status");
|
||||
const positions = async () => page.locator(".xyzac-position-grid output").allInnerTexts();
|
||||
const settle = async (ms = 600) => { await page.waitForTimeout(ms); };
|
||||
|
||||
const closeDialog = async () => {
|
||||
const dialog = page.locator("dialog[open]").first();
|
||||
if (!(await dialog.count())) return;
|
||||
const button = dialog.locator('header button[aria-label^="关闭"]').first();
|
||||
if (await button.count()) await button.click();
|
||||
else await page.keyboard.press("Escape");
|
||||
await dialog.waitFor({ state: "hidden", timeout: 5000 }).catch(() => {});
|
||||
};
|
||||
|
||||
const waitFor = async (predicate, timeout, label) => {
|
||||
const deadline = Date.now() + timeout;
|
||||
let last = "";
|
||||
while (Date.now() < deadline) {
|
||||
last = await predicate();
|
||||
if (last) return last;
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
throw new Error(`${label} 在 ${timeout}ms 内未满足,最后观察:${String(last).slice(0, 160)}`);
|
||||
};
|
||||
|
||||
const waitState = async (pattern, timeout = 60000) => {
|
||||
const deadline = Date.now() + timeout;
|
||||
let last = "";
|
||||
while (Date.now() < deadline) {
|
||||
last = await statusText();
|
||||
if (pattern.test(last)) return last;
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
throw new Error(`状态 ${pattern} 在 ${timeout}ms 内未满足,最后状态:${last.slice(0, 200)}`);
|
||||
};
|
||||
|
||||
const waitCase = (timeout = 120000) => waitFor(async () => {
|
||||
const value = (await page.locator(".case-status").innerText()).trim();
|
||||
return value.startsWith("Program:") ? value : null;
|
||||
}, timeout, "案例程序加载完成");
|
||||
|
||||
// 重新载入案例会按设计先给机床下电并复位回零状态,运行程序前需要重新上电并回零。
|
||||
const ensureReady = async () => {
|
||||
const estop = page.locator('[data-axis-control="toolbar-estop"]');
|
||||
if ((await estop.getAttribute("aria-label"))?.includes("Cancel")) { await estop.click(); await settle(1500); }
|
||||
const power = page.locator('[data-axis-control="toolbar-power"]');
|
||||
if ((await power.getAttribute("aria-label"))?.includes("Power on") && !(await power.isDisabled())) {
|
||||
await power.click();
|
||||
await waitState(/STATE_ON|STATE_IDLE/, 40000);
|
||||
}
|
||||
if (!/Machine sequence HOMED/.test(await statusText())) {
|
||||
await page.locator('[data-axis-control="tab-manual"]').click();
|
||||
await page.locator('[data-axis-control="manual-home-all"]').click();
|
||||
await waitState(/Machine sequence HOMED/, 60000);
|
||||
}
|
||||
return statusText();
|
||||
};
|
||||
|
||||
// canLoad 为假时下拉框会拒绝切换,这里按值重试直到状态真正生效。
|
||||
const selectCase = async (value, timeout = 45000) => {
|
||||
const select = page.locator('[data-axis-control="workspace-case-select"]');
|
||||
const deadline = Date.now() + timeout;
|
||||
while (Date.now() < deadline) {
|
||||
await select.selectOption(value);
|
||||
await settle(2500);
|
||||
if ((await select.inputValue()) === value) { await waitCase(); await settle(2500); return value; }
|
||||
}
|
||||
throw new Error(`案例切换未生效:${value}`);
|
||||
};
|
||||
|
||||
// 冷却液等开关是受 WASM 状态回传驱动的受控复选框,点击后要等状态回传再判断,避免误判失败。
|
||||
const setToggle = async (selector, target, attempts = 6) => {
|
||||
const box = page.locator(selector);
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
if ((await box.isChecked()) === target) return true;
|
||||
await box.click();
|
||||
await settle(1200);
|
||||
}
|
||||
return (await box.isChecked()) === target;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------- 阶段一:TRT 主线
|
||||
await step("打开线上站点并等待 WASM 会话就绪", async () => {
|
||||
await page.goto(baseUrl, { waitUntil: "load", timeout: 90000 });
|
||||
await page.waitForSelector('[data-axis-control="workspace-case-select"]', { timeout: 60000 });
|
||||
await waitState(/STATE_/, 60000);
|
||||
await waitCase(90000);
|
||||
await settle(2500);
|
||||
const cases = await page.locator('[data-axis-control="workspace-case-select"] option').allInnerTexts();
|
||||
await shot("01-main-overview");
|
||||
return { title: await page.title(), canvases: await page.locator("canvas").count(), cases: cases.length };
|
||||
});
|
||||
|
||||
await step("状态栏、轴位与实时参数由 LinuxCNC WASM 驱动", async () => {
|
||||
const status = await statusText();
|
||||
const live = await page.locator("[data-xyzac-parameter-source]").first().getAttribute("data-xyzac-parameter-source");
|
||||
const axes = await positions();
|
||||
if (!/Task mode/.test(status)) throw new Error(`状态栏异常:${status}`);
|
||||
if (live !== "linuxcnc-wasm") throw new Error(`实时参数来源不是 WASM:${live}`);
|
||||
if (axes.length < 5) throw new Error(`轴位显示数量不足:${axes.length}`);
|
||||
return { live, axes, status: status.slice(0, 120) };
|
||||
});
|
||||
|
||||
await step("Help 菜单可打开 About 对话框", async () => {
|
||||
await page.locator('[data-axis-control="menu-help"]').click();
|
||||
await page.locator('[data-axis-control="menu-help-about"]').click();
|
||||
const dialog = page.locator(".axis-about-dialog");
|
||||
await dialog.waitFor({ state: "visible", timeout: 5000 });
|
||||
await shot("02-about-dialog");
|
||||
const body = (await dialog.innerText()).replace(/\s+/g, " ");
|
||||
await page.locator('[data-axis-control="about-close"]').click();
|
||||
await dialog.waitFor({ state: "hidden", timeout: 5000 });
|
||||
return body.slice(0, 120);
|
||||
});
|
||||
|
||||
await step("File 菜单把宿主边界项正确置灰", async () => {
|
||||
await page.locator('[data-axis-control="menu-file"]').click();
|
||||
const states = [
|
||||
await page.locator('[data-axis-control="menu-file-edit-tool-data"]').isDisabled(),
|
||||
await page.locator('[data-axis-control="menu-file-quit"]').isDisabled(),
|
||||
];
|
||||
await shot("03-file-menu");
|
||||
await page.keyboard.press("Escape");
|
||||
if (states.some((disabled) => !disabled)) throw new Error(`宿主边界项未置灰:${states}`);
|
||||
return states;
|
||||
});
|
||||
|
||||
await step("RTCP 程序库按机型过滤并列出可加载程序", async () => {
|
||||
await page.locator('[data-axis-control="workspace-rtcp-programs"]').click();
|
||||
const panel = page.locator("dialog.rtcp-program-dialog");
|
||||
await panel.waitFor({ state: "visible", timeout: 8000 });
|
||||
await settle(600);
|
||||
await shot("04-rtcp-program-library");
|
||||
const summary = (await panel.locator(".rtcp-program-summary").innerText()).replace(/\s+/g, " ");
|
||||
const options = await panel.locator('[data-axis-control="rtcp-program-select"] option').count();
|
||||
const scopes = await panel.locator('[data-axis-control="rtcp-program-scope"] option').allInnerTexts();
|
||||
await closeDialog();
|
||||
if (options < 3) throw new Error(`RTCP 程序条目不足:${options}`);
|
||||
return { options, summary, scopes };
|
||||
});
|
||||
|
||||
await step("导入机床模型对话框列出内置示例与下载入口", async () => {
|
||||
await page.locator('[data-axis-control="machine-glb-settings"]').click();
|
||||
const dialog = page.locator("dialog[open]");
|
||||
await dialog.waitFor({ state: "visible", timeout: 8000 });
|
||||
await settle(800);
|
||||
await shot("05-machine-import-dialog");
|
||||
const downloads = await dialog.locator("a[download]").count();
|
||||
const summary = (await dialog.innerText()).replace(/\s+/g, " ");
|
||||
await closeDialog();
|
||||
return { downloads, summary: summary.slice(0, 160) };
|
||||
});
|
||||
|
||||
await step("三维机床管理四个步骤页签可切换", async () => {
|
||||
await page.locator('[data-axis-control="machine-manager-settings"]').click();
|
||||
const dialog = page.locator("dialog[open]");
|
||||
await dialog.waitFor({ state: "visible", timeout: 8000 });
|
||||
await settle(1200);
|
||||
const seen = [];
|
||||
for (const [id, name] of [["structure", "06-machine-manager-structure"], ["motion", "07-machine-manager-motion"], ["calibration", "08-machine-manager-calibration"], ["files", "09-machine-manager-files"]]) {
|
||||
await page.locator(`[data-axis-control="machine-manager-tab-${id}"]`).click();
|
||||
await settle(900);
|
||||
await shot(name);
|
||||
seen.push((await text(`#machine-panel-${id}`)).slice(0, 70));
|
||||
}
|
||||
await closeDialog();
|
||||
return seen;
|
||||
});
|
||||
|
||||
await step("机床参数对话框显示旋转中心与轴间偏置字段", async () => {
|
||||
await page.locator('[data-axis-control="geometry-settings"]').click();
|
||||
const dialog = page.locator("dialog[open]");
|
||||
await dialog.waitFor({ state: "visible", timeout: 8000 });
|
||||
await settle(600);
|
||||
await shot("10-geometry-parameters");
|
||||
const fieldCount = await dialog.locator('input[type="number"]').count();
|
||||
await closeDialog();
|
||||
if (fieldCount < 4) throw new Error(`几何参数字段不足:${fieldCount}`);
|
||||
return fieldCount;
|
||||
});
|
||||
|
||||
await step("案例切换到 XYZBC 后机型与轴组同步改变", async () => {
|
||||
await selectCase("linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini");
|
||||
await shot("11-case-xyzbc");
|
||||
const axes = await page.locator(".axis-radio span").allInnerTexts();
|
||||
if (axes.join("") !== "XYZBC") throw new Error(`切换机型后轴组异常:${axes}`);
|
||||
await selectCase("linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini");
|
||||
const back = await page.locator(".axis-radio span").allInnerTexts();
|
||||
if (back.join("") !== "XYZAC") throw new Error(`切回 XYZAC 失败:${back}`);
|
||||
return { axes, back };
|
||||
});
|
||||
|
||||
await step("急停复位并可上电", async () => {
|
||||
const estop = page.locator('[data-axis-control="toolbar-estop"]');
|
||||
if ((await estop.getAttribute("aria-label"))?.includes("Cancel")) { await estop.click(); await settle(1500); }
|
||||
await page.locator('[data-axis-control="toolbar-power"]').click();
|
||||
return (await waitState(/STATE_ON|STATE_IDLE/, 40000)).slice(0, 120);
|
||||
});
|
||||
|
||||
await step("HOME ALL 完成全部关节回零", async () => {
|
||||
await page.locator('[data-axis-control="manual-home-all"]').click();
|
||||
const status = await waitState(/Machine sequence HOMED/, 60000);
|
||||
await shot("12-manual-homed");
|
||||
return status.slice(0, 120);
|
||||
});
|
||||
|
||||
await step("连续 Jog:按住 X 正向位置变化,松手停止", async () => {
|
||||
await page.locator('[data-axis-control="manual-axis-x"]').check();
|
||||
const before = (await positions())[0];
|
||||
await page.locator('[data-axis-control="manual-jog-positive"]').hover();
|
||||
await page.mouse.down();
|
||||
await settle(1800);
|
||||
await shot("13-manual-jog");
|
||||
await page.mouse.up();
|
||||
await settle(1500);
|
||||
const after = (await positions())[0];
|
||||
if (before === after) throw new Error(`连续 Jog 后 X 未变化:${before}`);
|
||||
return { before, after };
|
||||
});
|
||||
|
||||
await step("增量 Jog:0.1000 步长按一次步进", async () => {
|
||||
await page.locator('[data-axis-control="manual-jog-increment"]').selectOption("0.1000");
|
||||
const before = Number((await positions())[0]);
|
||||
await page.locator('[data-axis-control="manual-jog-negative"]').hover();
|
||||
await page.mouse.down();
|
||||
await settle(1200);
|
||||
await page.mouse.up();
|
||||
await settle(2000);
|
||||
const after = Number((await positions())[0]);
|
||||
await page.locator('[data-axis-control="manual-jog-increment"]').selectOption("Continuous");
|
||||
if (!(Math.abs(Math.abs(before - after) - 0.1) < 0.04)) throw new Error(`增量步长不符:${before} -> ${after}`);
|
||||
return { before, after };
|
||||
});
|
||||
|
||||
await step("主轴启停与冷却液开关可用", async () => {
|
||||
await page.locator('[data-axis-control="tab-manual"]').click();
|
||||
await page.locator('[data-axis-control="manual-spindle-cw"]').click();
|
||||
await settle(2500);
|
||||
const live = await text('[data-axis-feature="AXIS-LIVE-PARAMETERS"]');
|
||||
const mistOn = await setToggle('[data-axis-control="manual-coolant-mist"]', true);
|
||||
await shot("14-spindle-coolant");
|
||||
await page.locator('[data-axis-control="manual-spindle-stop"]').click();
|
||||
const mistOff = await setToggle('[data-axis-control="manual-coolant-mist"]', false);
|
||||
if (!mistOn || !mistOff) throw new Error(`冷却液开关未按预期切换:开=${mistOn} 关=${mistOff}`);
|
||||
return { mistOn, mistOff, live: live.slice(0, 110) };
|
||||
});
|
||||
|
||||
await step("MDI 指令执行并写入历史", async () => {
|
||||
await page.locator('[data-axis-control="tab-mdi"]').click();
|
||||
await page.locator('[data-axis-control="mdi-command"]').fill("G0 X12.5 Y-4 Z0");
|
||||
await page.locator('[data-axis-control="mdi-submit"]').click();
|
||||
await settle(3000);
|
||||
await shot("15-mdi");
|
||||
const history = await page.locator('[data-axis-control="mdi-history-select"]').first().innerText();
|
||||
if (!history.includes("G0")) throw new Error(`MDI 历史缺记录:${history}`);
|
||||
return history;
|
||||
});
|
||||
|
||||
await step("DRO 视图可切换坐标、显示与单位", async () => {
|
||||
await page.locator('[data-axis-control="tab-dro"]').click();
|
||||
await page.locator('[data-axis-control="dro-coordinate"]').selectOption("relative");
|
||||
await page.locator('[data-axis-control="dro-display"]').selectOption("commanded");
|
||||
await page.locator('[data-axis-control="dro-units"]').selectOption("inch");
|
||||
await settle(1500);
|
||||
await shot("16-dro");
|
||||
const rows = await page.locator(".dro-row").count();
|
||||
const footer = await text(".dro-footer");
|
||||
await page.locator('[data-axis-control="dro-units"]').selectOption("mm");
|
||||
await page.locator('[data-axis-control="dro-coordinate"]').selectOption("machine");
|
||||
await page.locator('[data-axis-control="dro-display"]').selectOption("actual");
|
||||
await settle(800);
|
||||
return { rows, footer: footer.slice(0, 110) };
|
||||
});
|
||||
|
||||
await step("倍率滑块与预览图层开关可用", async () => {
|
||||
await page.locator('[data-axis-control="override-feed"]').fill("80");
|
||||
await settle(1500);
|
||||
const label = (await page.locator("section.axis-metrics").innerText()).replace(/\s+/g, " ");
|
||||
const layer = page.locator('[data-axis-control="preview-layer-velocity"]');
|
||||
await layer.uncheck();
|
||||
await settle(500);
|
||||
const unchecked = !(await layer.isChecked());
|
||||
await layer.check();
|
||||
await shot("17-overrides-layers");
|
||||
if (!unchecked) throw new Error("预览图层开关无效");
|
||||
return label.slice(0, 150);
|
||||
});
|
||||
|
||||
await step("工具栏『载入精确案例』按钮重新载入当前案例", async () => {
|
||||
await page.locator('[data-axis-control="tab-preview"]').click();
|
||||
await page.locator('[data-axis-control="toolbar-load-case"]').click();
|
||||
// 修复前这里会停在 blocked: simulation_case_unknown,永远不会回到 Program: 状态。
|
||||
const settled = await waitCase(120000);
|
||||
const status = (await page.locator(".case-status").innerText()).trim();
|
||||
if (/^blocked:/.test(status)) throw new Error(`载入精确案例失败:${status}`);
|
||||
return status.slice(0, 80);
|
||||
});
|
||||
|
||||
await step("载入案例程序并运行、暂停、继续、停止", async () => {
|
||||
await ensureReady();
|
||||
await page.locator('[data-axis-control="workspace-load-boat"]').click();
|
||||
await waitCase();
|
||||
await settle(3000);
|
||||
await page.locator('[data-axis-control="toolbar-run"]').click();
|
||||
const running = await waitState(/STATE_RUNNING|INTERP_READING|INTERP_WAITING/, 60000);
|
||||
await settle(2500);
|
||||
await shot("18-program-running");
|
||||
let paused = "";
|
||||
if (await page.locator('[data-axis-control="toolbar-pause-resume"]').isEnabled()) {
|
||||
await page.locator('[data-axis-control="toolbar-pause-resume"]').click();
|
||||
paused = await waitState(/PAUSED|INTERP_PAUSED/, 30000);
|
||||
await settle(1500);
|
||||
await shot("19-program-paused");
|
||||
await page.locator('[data-axis-control="toolbar-pause-resume"]').click();
|
||||
await settle(2500);
|
||||
}
|
||||
await page.locator('[data-axis-control="toolbar-stop"]').click();
|
||||
await settle(3000);
|
||||
return { running: running.slice(0, 100), paused: paused.slice(0, 100), stopped: (await statusText()).slice(0, 100) };
|
||||
});
|
||||
|
||||
await step("RTCP 程序库『加载到控制器』可用", async () => {
|
||||
await page.locator('[data-axis-control="workspace-rtcp-programs"]').click();
|
||||
const panel = page.locator("dialog.rtcp-program-dialog");
|
||||
await panel.waitFor({ state: "visible", timeout: 8000 });
|
||||
await settle(800);
|
||||
try {
|
||||
const load = panel.locator('[data-axis-control="rtcp-program-load"]');
|
||||
if (!(await load.isEnabled())) throw new Error("默认主程序当前不可加载");
|
||||
await load.click();
|
||||
await panel.locator('output[role="status"]').filter({ hasText: "程序已加载" }).waitFor({ state: "visible", timeout: 60000 });
|
||||
await shot("22-rtcp-load");
|
||||
return (await panel.locator('output[role="status"]').innerText()).trim();
|
||||
} finally {
|
||||
await closeDialog();
|
||||
await settle(2500);
|
||||
}
|
||||
});
|
||||
|
||||
await step("工具栏『打开 G 代码文件』可装载本地 .ngc", async () => {
|
||||
await page.setInputFiles('[data-axis-control="program-file-input"]', "web/react/public/machine-cases/xyzac-trt.ngc");
|
||||
await waitCase(90000);
|
||||
await settle(2500);
|
||||
return (await page.locator(".case-status").innerText()).trim();
|
||||
});
|
||||
|
||||
await step("工具栏『重载当前程序』可用", async () => {
|
||||
await page.locator('[data-axis-control="toolbar-reload"]').click();
|
||||
await settle(4000);
|
||||
return (await statusText()).slice(0, 110);
|
||||
});
|
||||
|
||||
await step("工具栏视图按钮与清除轨迹可用", async () => {
|
||||
for (const id of ["toolbar-view-z", "toolbar-view-x", "toolbar-view-y", "toolbar-view-p"]) {
|
||||
await page.locator(`[data-axis-control="${id}"]`).click();
|
||||
await settle(500);
|
||||
}
|
||||
const pressed = await page.locator('[data-axis-control="toolbar-view-p"]').getAttribute("aria-pressed");
|
||||
await page.locator('[data-axis-control="toolbar-rotate"]').click();
|
||||
await page.locator('[data-axis-control="toolbar-clear-live-plot"]').click();
|
||||
await settle(1200);
|
||||
await shot("23-views-cleared");
|
||||
if (pressed !== "true") throw new Error(`视图按钮未置为按下:${pressed}`);
|
||||
return { pressed };
|
||||
});
|
||||
|
||||
await step("诊断页显示启动资源与门控", async () => {
|
||||
await page.locator('[data-axis-control="tab-diagnostics"]').click();
|
||||
await settle(1200);
|
||||
await shot("20-diagnostics");
|
||||
const body = await text("#axis-panel-diagnostics");
|
||||
if (!/Disabled operations|Startup resources/.test(body)) throw new Error(`诊断页内容异常:${body.slice(0, 100)}`);
|
||||
return body.slice(0, 150);
|
||||
});
|
||||
|
||||
await step("快捷键 F1 / F3 / F5 生效", async () => {
|
||||
await page.locator("body").click({ position: { x: 5, y: 5 } });
|
||||
await page.keyboard.press("F1");
|
||||
const estop = await waitState(/ESTOP/, 15000);
|
||||
await page.keyboard.press("F1");
|
||||
await settle(2000);
|
||||
await page.keyboard.press("F3");
|
||||
await settle(800);
|
||||
const manualSelected = await page.locator('[data-axis-control="tab-manual"]').getAttribute("aria-selected");
|
||||
await page.keyboard.press("F5");
|
||||
await settle(800);
|
||||
const mdiSelected = await page.locator('[data-axis-control="tab-mdi"]').getAttribute("aria-selected");
|
||||
if (manualSelected !== "true" || mdiSelected !== "true") throw new Error(`F3/F5 未切换页签:${manualSelected}/${mdiSelected}`);
|
||||
return { estopTriggered: /ESTOP/.test(estop), manualSelected, mdiSelected };
|
||||
});
|
||||
|
||||
await step("窄视口 390x844 无横向溢出", async () => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await settle(2500);
|
||||
await shot("21-mobile-390", { full: true });
|
||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
if (overflow > 2) throw new Error(`出现横向溢出 ${overflow}px`);
|
||||
return { overflow };
|
||||
});
|
||||
|
||||
await page.close();
|
||||
|
||||
// ---------------------------------------------------------------- 阶段二:案例矩阵
|
||||
const MATRIX = [
|
||||
["xyzac-trt", "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"],
|
||||
["xyzbc-trt", "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"],
|
||||
["xyzab-tdr", "linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini"],
|
||||
["max5-maxkins", "linuxcnc/configs/sim/axis/vismach/5axis/max5/max5kins.ini"],
|
||||
["trivkins-xyzya", "linuxcnc/configs/sim/woodpecker/woodpecker_xyzya.ini"],
|
||||
["hexapod-genhexkins", "linuxcnc/configs/sim/axis/vismach/hexapod-sim/hexapod.ini"],
|
||||
["scorbot-kins", "linuxcnc/configs/sim/axis/vismach/scorbot/scorbot.ini"],
|
||||
];
|
||||
|
||||
console.log("\n阶段二:案例可用性矩阵(每个案例独立页面)");
|
||||
for (const [id, entrypoint] of MATRIX) {
|
||||
const probe = await newPage();
|
||||
const fourXx = [];
|
||||
probe.on("response", (response) => { if (response.status() >= 400) fourXx.push(`${response.status()} ${response.url().replace(baseUrl.replace(/\/$/, ""), "")}`); });
|
||||
try {
|
||||
await probe.goto(baseUrl, { waitUntil: "load", timeout: 90000 });
|
||||
await probe.waitForSelector('[data-axis-control="workspace-case-select"]', { timeout: 60000 });
|
||||
await probe.waitForFunction(() => document.querySelector(".case-status")?.textContent?.startsWith("Program:"), null, { timeout: 90000 });
|
||||
await probe.waitForTimeout(4000);
|
||||
const select = probe.locator('[data-axis-control="workspace-case-select"]');
|
||||
let applied = false;
|
||||
for (let attempt = 0; attempt < 5 && !applied; attempt += 1) {
|
||||
await select.selectOption(entrypoint);
|
||||
await probe.waitForTimeout(2500);
|
||||
applied = (await select.inputValue()) === entrypoint;
|
||||
}
|
||||
await probe.waitForTimeout(20000);
|
||||
const power = probe.locator('[data-axis-control="toolbar-power"]');
|
||||
const usable = applied && !(await power.isDisabled());
|
||||
const status = (await probe.locator(".machine-status").innerText()).replace(/\s+/g, " ");
|
||||
caseMatrix.push({
|
||||
id,
|
||||
applied,
|
||||
power: usable ? "enabled" : `disabled:${await power.getAttribute("title")}`,
|
||||
state: status.slice(0, 90),
|
||||
http_4xx: fourXx.slice(0, 2),
|
||||
verdict: usable ? "usable" : applied ? "loads_but_controller_pending" : "switch_rejected",
|
||||
});
|
||||
console.log(` ${usable ? "可用 " : "不可用"} ${id.padEnd(20)} ${usable ? "可上电" : status.slice(0, 46)}`);
|
||||
} catch (error) {
|
||||
caseMatrix.push({ id, applied: false, power: "unknown", state: String(error?.message ?? error).slice(0, 120), http_4xx: fourXx.slice(0, 2), verdict: "probe_error" });
|
||||
console.log(` 探测失败 ${id} — ${String(error?.message ?? error).slice(0, 80)}`);
|
||||
} finally {
|
||||
await probe.close();
|
||||
}
|
||||
}
|
||||
|
||||
const pendingCases = caseMatrix.filter((row) => row.verdict !== "usable");
|
||||
if (pendingCases.length) {
|
||||
defects.push({
|
||||
id: "DEFECT-02",
|
||||
severity: "high",
|
||||
control: "Simulation case 下拉框中的通用运动学案例",
|
||||
observed: `${pendingCases.map((row) => row.id).join("、")} 载入后控制器停在 PENDING,电源按钮保持禁用(linuxcnc_wasm_snapshot_pending),此时下拉框无法切回其它案例,只能刷新页面恢复`,
|
||||
expected: "选中案例后可上电、回零并运行其干运行程序",
|
||||
root_cause: "通用案例运行链在浏览器侧尚未完成:案例的运动学源文件(如 linuxcnc/src/emc/kinematics/trivkins.c、linuxcnc/src/hal/components/xyzab_tdr_kins.comp)没有随前端产物发布,请求返回 404;且会话启动失败后前端不回滚,卡在 pending",
|
||||
workaround: "当前只使用 XYZAC TRT / XYZBC TRT 两个案例;误选其它案例后刷新浏览器页面",
|
||||
});
|
||||
}
|
||||
|
||||
const report = {
|
||||
report_id: "LINUXCNC-WEB-DEPLOYED-MANUAL-TEST",
|
||||
url: baseUrl,
|
||||
checked_at: new Date().toISOString(),
|
||||
// 只有步骤失败才判定 failed;已知缺陷(DEFECT-xx)单独记录,不掩盖功能测试结论。
|
||||
status: results.every((row) => row.status === "pass") ? "verified" : "failed",
|
||||
passed: results.filter((row) => row.status === "pass").length,
|
||||
failed: results.filter((row) => row.status === "fail").length,
|
||||
results,
|
||||
defects,
|
||||
case_matrix: caseMatrix,
|
||||
console_errors: consoleErrors,
|
||||
http_and_request_failures: failedRequests,
|
||||
screenshots: artifacts,
|
||||
};
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(`\n${report.status.toUpperCase()} ${report.passed} passed / ${report.failed} failed → ${reportPath}`);
|
||||
if (defects.length) console.log(`缺陷/限制 ${defects.length} 项:`, defects.map((item) => item.id).join("、"));
|
||||
if (consoleErrors.length) console.log("console/page errors:", JSON.stringify(consoleErrors.slice(0, 3)));
|
||||
|
||||
await browser.close();
|
||||
process.exit(report.status === "verified" ? 0 : 1);
|
||||
364
docs/user-manual/deployment-test.report.json
Normal file
364
docs/user-manual/deployment-test.report.json
Normal file
@@ -0,0 +1,364 @@
|
||||
{
|
||||
"report_id": "LINUXCNC-WEB-DEPLOYED-MANUAL-TEST",
|
||||
"url": "https://49.232.250.35:58010/",
|
||||
"checked_at": "2026-10-08T10:52:07.892Z",
|
||||
"status": "verified",
|
||||
"passed": 26,
|
||||
"failed": 0,
|
||||
"results": [
|
||||
{
|
||||
"name": "打开线上站点并等待 WASM 会话就绪",
|
||||
"status": "pass",
|
||||
"ms": 20508,
|
||||
"detail": {
|
||||
"title": "LinuxCNC Web/WASM",
|
||||
"canvases": 2,
|
||||
"cases": 13
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "状态栏、轴位与实时参数由 LinuxCNC WASM 驱动",
|
||||
"status": "pass",
|
||||
"ms": 542,
|
||||
"detail": {
|
||||
"live": "linuxcnc-wasm",
|
||||
"axes": [
|
||||
"0.0000",
|
||||
"0.0000",
|
||||
"0.0000",
|
||||
"0.000",
|
||||
"0.000"
|
||||
],
|
||||
"status": "Task mode MANUAL Task state STATE_ESTOP_RESET Interpreter INTERP_IDLE Motion mode FREE Motion type 0 Homing IDLE Machine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Help 菜单可打开 About 对话框",
|
||||
"status": "pass",
|
||||
"ms": 5995,
|
||||
"detail": "LinuxCNC AXIS Web/WASM XYZAC TRT OK"
|
||||
},
|
||||
{
|
||||
"name": "File 菜单把宿主边界项正确置灰",
|
||||
"status": "pass",
|
||||
"ms": 2899,
|
||||
"detail": [
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RTCP 程序库按机型过滤并列出可加载程序",
|
||||
"status": "pass",
|
||||
"ms": 5690,
|
||||
"detail": {
|
||||
"options": 16,
|
||||
"summary": "AC(XYZAC) 程序 当前分类:16 个 可直接加载:9 LinuxCNC 源程序:32 个 · 含案例干运行程序共 45 个",
|
||||
"scopes": [
|
||||
"当前 AC(XYZAC) 机床",
|
||||
"全部 45 个程序"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "导入机床模型对话框列出内置示例与下载入口",
|
||||
"status": "pass",
|
||||
"ms": 5585,
|
||||
"detail": {
|
||||
"downloads": 4,
|
||||
"summary": "三维机床模型 × 三维机床 内置模型 导入 GLB AC 示例 下载 AC BC 示例 下载 BC AC 示例桁架型 下载 AC桁架型 BC 示例桁架型 下载 BC桁架型 单个 GLB,包含层级模型及 URDF,最大 64 MiB"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "三维机床管理四个步骤页签可切换",
|
||||
"status": "pass",
|
||||
"ms": 19399,
|
||||
"detail": [
|
||||
"部件与层级 0 节点 展开 折叠 导入机床或部件后显示 GLB 内部层级。 选择层级节点,配置其唯一自由度。",
|
||||
"跟随数控系统实时关节位置 手动验证(相对物理零点) 层级运动测试 复位 X mm Y mm Z mm A deg C deg 这些输入仅验证",
|
||||
"机床标定 C 轴原点 (mm) X Y Z A 到 C 轴偏置 (mm) X Y Z 机床物理零点(按当前坐标,直线 mm / 旋转 deg",
|
||||
"机床文档 模型名称 设计说明 LinuxCNC 五轴案例库 13 个结构/运动学案例 案例来源于仓库内 LinuxCNC 配置、示例 G 代"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "机床参数对话框显示旋转中心与轴间偏置字段",
|
||||
"status": "pass",
|
||||
"ms": 5161,
|
||||
"detail": 6
|
||||
},
|
||||
{
|
||||
"name": "案例切换到 XYZBC 后机型与轴组同步改变",
|
||||
"status": "pass",
|
||||
"ms": 40918,
|
||||
"detail": {
|
||||
"axes": [
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"B",
|
||||
"C"
|
||||
],
|
||||
"back": [
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"A",
|
||||
"C"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "急停复位并可上电",
|
||||
"status": "pass",
|
||||
"ms": 1791,
|
||||
"detail": "Task mode MANUAL Task state STATE_ON Interpreter INTERP_IDLE Motion mode FREE Motion type 0 Homing IDLE Machine sequence"
|
||||
},
|
||||
{
|
||||
"name": "HOME ALL 完成全部关节回零",
|
||||
"status": "pass",
|
||||
"ms": 2910,
|
||||
"detail": "Task mode MANUAL Task state STATE_ON Interpreter INTERP_IDLE Motion mode TELEOP Motion type 0 Homing IDLE Machine sequen"
|
||||
},
|
||||
{
|
||||
"name": "连续 Jog:按住 X 正向位置变化,松手停止",
|
||||
"status": "pass",
|
||||
"ms": 7696,
|
||||
"detail": {
|
||||
"before": "0.0000",
|
||||
"after": "0.9075"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "增量 Jog:0.1000 步长按一次步进",
|
||||
"status": "pass",
|
||||
"ms": 6327,
|
||||
"detail": {
|
||||
"before": 0.9075,
|
||||
"after": 0.8075
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "主轴启停与冷却液开关可用",
|
||||
"status": "pass",
|
||||
"ms": 13600,
|
||||
"detail": {
|
||||
"mistOn": true,
|
||||
"mistOff": true,
|
||||
"live": "Live parameters LinuxCNC WASM Spindle 1.0 rpm Cmd 1.0 rpm / S 0.0 rpm Feed actual 0.0 mm/min Program 0.0 mm/mi"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "MDI 指令执行并写入历史",
|
||||
"status": "pass",
|
||||
"ms": 7596,
|
||||
"detail": "G0 X12.5 Y-4 Z0"
|
||||
},
|
||||
{
|
||||
"name": "DRO 视图可切换坐标、显示与单位",
|
||||
"status": "pass",
|
||||
"ms": 6126,
|
||||
"detail": {
|
||||
"rows": 5,
|
||||
"footer": "Units: inch / deg relative / commanded Tick: 144 Hash: fnv1a64:0780"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "倍率滑块与预览图层开关可用",
|
||||
"status": "pass",
|
||||
"ms": 4898,
|
||||
"detail": "Jog Speed: 100% Feed Override: 80% Spindle Override: 100% Max Velocity: 100% Rapid Override Unavailable path live limits velocity offsets extents"
|
||||
},
|
||||
{
|
||||
"name": "工具栏『载入精确案例』按钮重新载入当前案例",
|
||||
"status": "pass",
|
||||
"ms": 19213,
|
||||
"detail": "Program: boat-xyzac.ngc"
|
||||
},
|
||||
{
|
||||
"name": "载入案例程序并运行、暂停、继续、停止",
|
||||
"status": "pass",
|
||||
"ms": 38054,
|
||||
"detail": {
|
||||
"running": "Task mode AUTO Task state STATE_ON Interpreter INTERP_READING Motion mode COORD Motion type 0 Homing",
|
||||
"paused": "Task mode AUTO Task state STATE_ON Interpreter INTERP_PAUSED Motion mode COORD Motion type 1 Homing ",
|
||||
"stopped": "Task mode MANUAL Task state STATE_ON Interpreter INTERP_IDLE Motion mode TELEOP Motion type 0 Homing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "RTCP 程序库『加载到控制器』可用",
|
||||
"status": "pass",
|
||||
"ms": 11529,
|
||||
"detail": "程序已加载到 LinuxCNC WASM 控制器"
|
||||
},
|
||||
{
|
||||
"name": "工具栏『打开 G 代码文件』可装载本地 .ngc",
|
||||
"status": "pass",
|
||||
"ms": 3960,
|
||||
"detail": "Program: xyzac-trt.ngc"
|
||||
},
|
||||
{
|
||||
"name": "工具栏『重载当前程序』可用",
|
||||
"status": "pass",
|
||||
"ms": 5257,
|
||||
"detail": "Task mode MANUAL Task state STATE_ON Interpreter INTERP_IDLE Motion mode TELEOP Motion type 0 Homing IDLE Mach"
|
||||
},
|
||||
{
|
||||
"name": "工具栏视图按钮与清除轨迹可用",
|
||||
"status": "pass",
|
||||
"ms": 10442,
|
||||
"detail": {
|
||||
"pressed": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "诊断页显示启动资源与门控",
|
||||
"status": "pass",
|
||||
"ms": 3151,
|
||||
"detail": "TASK_READY Tick 324 Hash fnv1a64:8cadf3b1b75faa04 Startup resources Entrypoint: linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt"
|
||||
},
|
||||
{
|
||||
"name": "快捷键 F1 / F3 / F5 生效",
|
||||
"status": "pass",
|
||||
"ms": 5683,
|
||||
"detail": {
|
||||
"estopTriggered": true,
|
||||
"manualSelected": "true",
|
||||
"mdiSelected": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "窄视口 390x844 无横向溢出",
|
||||
"status": "pass",
|
||||
"ms": 3884,
|
||||
"detail": {
|
||||
"overflow": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"defects": [
|
||||
{
|
||||
"id": "DEFECT-02",
|
||||
"severity": "high",
|
||||
"control": "Simulation case 下拉框中的通用运动学案例",
|
||||
"observed": "xyzab-tdr、max5-maxkins、trivkins-xyzya、hexapod-genhexkins、scorbot-kins 载入后控制器停在 PENDING,电源按钮保持禁用(linuxcnc_wasm_snapshot_pending),此时下拉框无法切回其它案例,只能刷新页面恢复",
|
||||
"expected": "选中案例后可上电、回零并运行其干运行程序",
|
||||
"root_cause": "通用案例运行链在浏览器侧尚未完成:案例的运动学源文件(如 linuxcnc/src/emc/kinematics/trivkins.c、linuxcnc/src/hal/components/xyzab_tdr_kins.comp)没有随前端产物发布,请求返回 404;且会话启动失败后前端不回滚,卡在 pending",
|
||||
"workaround": "当前只使用 XYZAC TRT / XYZBC TRT 两个案例;误选其它案例后刷新浏览器页面"
|
||||
}
|
||||
],
|
||||
"case_matrix": [
|
||||
{
|
||||
"id": "xyzac-trt",
|
||||
"applied": true,
|
||||
"power": "enabled",
|
||||
"state": "Task mode MANUAL Task state STATE_ESTOP_RESET Interpreter INTERP_IDLE Motion mode FREE Mot",
|
||||
"http_4xx": [],
|
||||
"verdict": "usable"
|
||||
},
|
||||
{
|
||||
"id": "xyzbc-trt",
|
||||
"applied": true,
|
||||
"power": "enabled",
|
||||
"state": "Task mode MANUAL Task state STATE_ESTOP_RESET Interpreter INTERP_IDLE Motion mode FREE Mot",
|
||||
"http_4xx": [],
|
||||
"verdict": "usable"
|
||||
},
|
||||
{
|
||||
"id": "xyzab-tdr",
|
||||
"applied": true,
|
||||
"power": "disabled:linuxcnc_wasm_snapshot_pending",
|
||||
"state": "Task mode PENDING Task state PENDING Interpreter PENDING Motion mode PENDING Motion type -",
|
||||
"http_4xx": [
|
||||
"404 /linuxcnc/src/hal/components/xyzab_tdr_kins.comp"
|
||||
],
|
||||
"verdict": "loads_but_controller_pending"
|
||||
},
|
||||
{
|
||||
"id": "max5-maxkins",
|
||||
"applied": true,
|
||||
"power": "disabled:linuxcnc_wasm_snapshot_pending",
|
||||
"state": "Task mode PENDING Task state PENDING Interpreter PENDING Motion mode PENDING Motion type -",
|
||||
"http_4xx": [],
|
||||
"verdict": "loads_but_controller_pending"
|
||||
},
|
||||
{
|
||||
"id": "trivkins-xyzya",
|
||||
"applied": true,
|
||||
"power": "disabled:linuxcnc_wasm_snapshot_pending",
|
||||
"state": "Task mode PENDING Task state PENDING Interpreter PENDING Motion mode PENDING Motion type -",
|
||||
"http_4xx": [
|
||||
"404 /linuxcnc/src/emc/kinematics/trivkins.c"
|
||||
],
|
||||
"verdict": "loads_but_controller_pending"
|
||||
},
|
||||
{
|
||||
"id": "hexapod-genhexkins",
|
||||
"applied": true,
|
||||
"power": "disabled:linuxcnc_wasm_snapshot_pending",
|
||||
"state": "Task mode PENDING Task state PENDING Interpreter PENDING Motion mode PENDING Motion type -",
|
||||
"http_4xx": [
|
||||
"404 /linuxcnc/src/emc/kinematics/genhexkins.c"
|
||||
],
|
||||
"verdict": "loads_but_controller_pending"
|
||||
},
|
||||
{
|
||||
"id": "scorbot-kins",
|
||||
"applied": true,
|
||||
"power": "disabled:linuxcnc_wasm_snapshot_pending",
|
||||
"state": "Task mode PENDING Task state PENDING Interpreter PENDING Motion mode PENDING Motion type -",
|
||||
"http_4xx": [
|
||||
"404 /linuxcnc/src/emc/kinematics/scorbot-kins.c"
|
||||
],
|
||||
"verdict": "loads_but_controller_pending"
|
||||
}
|
||||
],
|
||||
"console_errors": [
|
||||
{
|
||||
"kind": "console",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
},
|
||||
{
|
||||
"kind": "console",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
},
|
||||
{
|
||||
"kind": "console",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
},
|
||||
{
|
||||
"kind": "console",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
}
|
||||
],
|
||||
"http_and_request_failures": [
|
||||
"HTTP 404 https://49.232.250.35:58010/linuxcnc/src/hal/components/xyzab_tdr_kins.comp",
|
||||
"HTTP 404 https://49.232.250.35:58010/linuxcnc/src/emc/kinematics/trivkins.c",
|
||||
"HTTP 404 https://49.232.250.35:58010/linuxcnc/src/emc/kinematics/genhexkins.c",
|
||||
"HTTP 404 https://49.232.250.35:58010/linuxcnc/src/emc/kinematics/scorbot-kins.c"
|
||||
],
|
||||
"screenshots": [
|
||||
"01-main-overview.png",
|
||||
"02-about-dialog.png",
|
||||
"03-file-menu.png",
|
||||
"04-rtcp-program-library.png",
|
||||
"05-machine-import-dialog.png",
|
||||
"06-machine-manager-structure.png",
|
||||
"07-machine-manager-motion.png",
|
||||
"08-machine-manager-calibration.png",
|
||||
"09-machine-manager-files.png",
|
||||
"10-geometry-parameters.png",
|
||||
"11-case-xyzbc.png",
|
||||
"12-manual-homed.png",
|
||||
"13-manual-jog.png",
|
||||
"14-spindle-coolant.png",
|
||||
"15-mdi.png",
|
||||
"16-dro.png",
|
||||
"17-overrides-layers.png",
|
||||
"18-program-running.png",
|
||||
"19-program-paused.png",
|
||||
"22-rtcp-load.png",
|
||||
"23-views-cleared.png",
|
||||
"20-diagnostics.png",
|
||||
"21-mobile-390.png"
|
||||
]
|
||||
}
|
||||
182
docs/user-manual/local-machine-manager-demo.mjs
Normal file
182
docs/user-manual/local-machine-manager-demo.mjs
Normal file
@@ -0,0 +1,182 @@
|
||||
// 可见演示:打开"三维机床管理"对话框,导入机床模型并测试。
|
||||
// 流程:打开网站 → 打开三维机床管理 → 查看案例库 → 导入 AC 示例机床 → 查看层级与轴绑定
|
||||
// → 运动验证(手动输入各轴)→ 查看标定参数 → 应用到 Web LinuxCNC → 回到主页上电/回零/Jog 验证模型跟随。
|
||||
//
|
||||
// 运行:
|
||||
// DISPLAY=:1 PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs \
|
||||
// node docs/user-manual/local-machine-manager-demo.mjs
|
||||
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(here, "../..");
|
||||
const baseUrl = process.env.DEMO_URL ?? "http://127.0.0.1:5174/";
|
||||
const chromePath = process.env.CHROME_PATH ?? "/usr/bin/google-chrome";
|
||||
const pace = Number(process.env.DEMO_PACE_MS ?? 2500);
|
||||
// 默认用带 JT_X/JT_Y/JT_Z/JT_A/JT_C 语义节点名的真实机床模型,分析器才能自动识别五轴。
|
||||
const glbPath = process.env.DEMO_GLB ?? resolve(repoRoot, "glb/m1.glb");
|
||||
const shotDir = resolve(here, "local-walkthrough");
|
||||
|
||||
const { chromium } = await import(process.env.PLAYWRIGHT_MODULE);
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
headless: false,
|
||||
args: ["--no-sandbox", "--start-maximized", "--disable-infobars", "--enable-unsafe-swiftshader"],
|
||||
});
|
||||
const page = await browser.newPage({ viewport: null });
|
||||
await page.bringToFront().catch(() => {});
|
||||
|
||||
const wait = (ms) => page.waitForTimeout(ms);
|
||||
const text = async (selector) => (await page.locator(selector).first().innerText()).replace(/\s+/g, " ").trim();
|
||||
const status = async () => text(".machine-status");
|
||||
const axes = async () => page.locator(".xyzac-position-grid output").allInnerTexts();
|
||||
const shot = (name) => page.screenshot({ path: resolve(shotDir, name) });
|
||||
const say = (step, message) => console.log(`\n[${step}] ${message}`);
|
||||
const waitFor = async (check, timeout, label) => {
|
||||
const deadline = Date.now() + timeout;
|
||||
let last = "";
|
||||
while (Date.now() < deadline) {
|
||||
last = await check();
|
||||
if (last) return last;
|
||||
await wait(400);
|
||||
}
|
||||
throw new Error(`${label} 超时(最后:${last || "无"})`);
|
||||
};
|
||||
const waitStatus = (pattern, timeout = 60000) => waitFor(async () => {
|
||||
const value = await status();
|
||||
return pattern.test(value) ? value : null;
|
||||
}, timeout, `等待 ${pattern}`);
|
||||
const dialog = () => page.locator("dialog[open]").first();
|
||||
|
||||
say(1, `打开网站 ${baseUrl}`);
|
||||
await page.goto(baseUrl, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForSelector('[data-axis-control="workspace-case-select"]', { timeout: 60000 });
|
||||
await waitFor(async () => ((await text(".case-status")).startsWith("Program:") ? "ok" : null), 90000, "案例就绪");
|
||||
await wait(2000);
|
||||
console.log(` 连接:${await text(".connection-state")}|机型:${await text(".xyzac-section-title")}|状态:${await status()}`);
|
||||
await shot("mgr-1-home.png");
|
||||
|
||||
say(2, "打开“三维机床管理”对话框(此时还没有任何机床模型)");
|
||||
await page.locator('[data-axis-control="machine-manager-settings"]').click();
|
||||
await dialog().waitFor({ state: "visible", timeout: 10000 });
|
||||
await wait(2500);
|
||||
console.log(` 草稿状态:${await text('[data-axis-control="machine-manager-draft-state"]')}|${await text('[data-axis-control="machine-manager-validation"]')}`);
|
||||
await shot("mgr-2-dialog-empty.png");
|
||||
await wait(pace);
|
||||
|
||||
say(3, "查看“文件与恢复”里的 LinuxCNC 五轴案例库");
|
||||
await page.locator('[data-axis-control="machine-manager-tab-files"]').click();
|
||||
await wait(2000);
|
||||
const caseCount = await page.locator("[data-machine-case]").count();
|
||||
const cases = await page.locator("[data-machine-case] strong").allInnerTexts();
|
||||
console.log(` 案例库 ${caseCount} 项:${cases.slice(0, 4).join("、")} …`);
|
||||
console.log(` 案例卡片示例:${(await text("[data-machine-case]")).slice(0, 120)}`);
|
||||
await shot("mgr-3-case-library.png");
|
||||
await wait(pace);
|
||||
|
||||
say(4, `回到“部件与层级”,导入机床模型 ${glbPath.split("/").pop()}`);
|
||||
await page.locator('[data-axis-control="machine-manager-tab-structure"]').click();
|
||||
await wait(1200);
|
||||
const [chooser] = await Promise.all([
|
||||
page.waitForEvent("filechooser"),
|
||||
page.locator('[data-axis-control="machine-manager-import"]').click(),
|
||||
]);
|
||||
await chooser.setFiles(glbPath);
|
||||
await waitFor(async () => ((await page.locator("[data-axis-control=machine-model-list]").count()) ? "ok" : null), 60000, "模型导入");
|
||||
await waitFor(async () => (/[1-9]\d*\/\d+ 轴已绑定/.test(await text(".machine-manager-toolbar")) ? "ok" : null), 120000, "关节自动绑定").catch(() => {});
|
||||
await wait(4000);
|
||||
console.log(` 模型列表:${await text("[data-axis-control=machine-model-list]")}`);
|
||||
console.log(` 工具栏:${await text(".machine-manager-toolbar")}`);
|
||||
await shot("mgr-4-imported-structure.png");
|
||||
await wait(pace);
|
||||
|
||||
say(5, "查看 GLB 五轴分析结果与层级树");
|
||||
const analysis = await page.locator('[data-axis-control="glb-five-axis-analysis"]').innerText().catch(() => "(无分析卡片)");
|
||||
console.log(` 分析:${analysis.replace(/\s+/g, " ").slice(0, 260)}`);
|
||||
const treeNodes = await page.locator('[data-axis-control="machine-tree-node"]').count();
|
||||
console.log(` 层级树节点数:${treeNodes}`);
|
||||
await shot("mgr-5-analysis-tree.png");
|
||||
await wait(pace);
|
||||
|
||||
say(6, "运动验证:手动驱动模型各轴(不发运动指令给控制器)");
|
||||
await page.locator('[data-axis-control="machine-manager-tab-motion"]').click();
|
||||
await wait(1500);
|
||||
const poseJson = async () => {
|
||||
const details = page.locator("#machine-panel-motion details").first();
|
||||
await details.evaluate((element) => { element.open = true; }).catch(() => {});
|
||||
await wait(300);
|
||||
return text('[data-axis-control="machine-test-state"]');
|
||||
};
|
||||
const before = await text('[data-axis-control="machine-test-state"]');
|
||||
const beforePose = await poseJson();
|
||||
const inputs = { x: "25", y: "15", z: "-8", a: "30", c: "45" };
|
||||
const firstField = page.locator('[data-axis-control="machine-test-x"]');
|
||||
if (await firstField.isDisabled()) {
|
||||
console.log(" ⚠ 运动验证输入仍为禁用:该模型没有自动识别出关节,需要手动绑定自由度后才能测试");
|
||||
} else {
|
||||
for (const [axis, value] of Object.entries(inputs)) {
|
||||
const field = page.locator(`[data-axis-control="machine-test-${axis}"]`);
|
||||
if (await field.count()) { await field.fill(value); await field.blur().catch(() => {}); await wait(600); }
|
||||
}
|
||||
}
|
||||
await wait(2000);
|
||||
const afterPose = await poseJson();
|
||||
const readBack = {};
|
||||
for (const axis of Object.keys(inputs)) readBack[axis.toUpperCase()] = await page.locator(`[data-axis-control="machine-test-${axis}"]`).inputValue().catch(() => "-");
|
||||
console.log(` 输入前模型姿态:${beforePose || before || "(空)"}`);
|
||||
console.log(` 输入后模型姿态:${afterPose}`);
|
||||
console.log(` 各轴输入回读:${JSON.stringify(readBack)}`);
|
||||
console.log(` 姿态已改变:${beforePose !== afterPose && afterPose ? "是" : "否"}`);
|
||||
await shot("mgr-6-motion-test.png");
|
||||
await wait(pace);
|
||||
|
||||
say(7, "复位运动验证输入,并查看机床标定参数");
|
||||
await page.locator('[data-axis-control="machine-test-reset"]').click();
|
||||
await wait(1500);
|
||||
await page.locator('[data-axis-control="machine-manager-tab-calibration"]').click();
|
||||
await wait(1500);
|
||||
const calibration = await text("#machine-panel-calibration");
|
||||
console.log(` 标定页:${calibration.slice(0, 220)}`);
|
||||
await shot("mgr-7-calibration.png");
|
||||
await wait(pace);
|
||||
|
||||
say(8, "把配置好的机床应用到 Web LinuxCNC");
|
||||
const applyButton = page.locator('[data-axis-control="machine-manager-apply"]');
|
||||
console.log(` 应用按钮:${(await applyButton.isDisabled()) ? "禁用" : "可用"}|检查:${await text('[data-axis-control="machine-manager-validation"]')}`);
|
||||
if (!(await applyButton.isDisabled())) {
|
||||
await applyButton.click();
|
||||
await wait(6000);
|
||||
console.log(` 应用反馈:${await text(".machine-manager-footer")}`);
|
||||
}
|
||||
await shot("mgr-8-applied.png");
|
||||
await wait(pace);
|
||||
|
||||
say(9, "关闭对话框,回到主页确认机床模型已经是导入的那一台");
|
||||
await page.locator('dialog[open] header button[aria-label^="关闭"]').click();
|
||||
await wait(3000);
|
||||
console.log(` 机型:${await text(".xyzac-section-title")}|程序:${await text(".case-status")}`);
|
||||
await shot("mgr-9-home-after-apply.png");
|
||||
await wait(pace);
|
||||
|
||||
say(10, "上电 → HOME ALL → Jog X,验证导入的机床模型跟随运动");
|
||||
const estop = page.locator('[data-axis-control="toolbar-estop"]');
|
||||
if ((await estop.getAttribute("aria-label"))?.includes("Cancel")) { await estop.click(); await wait(1500); }
|
||||
await page.locator('[data-axis-control="toolbar-power"]').click();
|
||||
await waitStatus(/STATE_ON|STATE_IDLE/, 40000);
|
||||
console.log(` 上电:${(await status()).match(/Task state (\S+)/)?.[1]}`);
|
||||
await page.locator('[data-axis-control="manual-home-all"]').click();
|
||||
await waitStatus(/Machine sequence HOMED/, 60000);
|
||||
console.log(` 回零:${(await status()).match(/Machine sequence (NOT HOMED|HOMED)/)?.[1]}|轴位 ${(await axes()).join(" / ")}`);
|
||||
await page.locator('[data-axis-control="manual-axis-x"]').check();
|
||||
await page.locator('[data-axis-control="manual-jog-positive"]').hover();
|
||||
await page.mouse.down();
|
||||
await wait(2000);
|
||||
await page.mouse.up();
|
||||
await wait(1500);
|
||||
console.log(` Jog 后轴位:${(await axes()).join(" / ")}`);
|
||||
await shot("mgr-10-machine-moving.png");
|
||||
|
||||
console.log("\n机床测试演示结束,窗口保持 20 秒后关闭。");
|
||||
await wait(20000);
|
||||
await browser.close();
|
||||
121
docs/user-manual/local-run-demo.mjs
Normal file
121
docs/user-manual/local-run-demo.mjs
Normal file
@@ -0,0 +1,121 @@
|
||||
// 最小演示:打开本地发布站点 → 上电 → HOME ALL → 载入并执行程序。
|
||||
// 默认在桌面开可见窗口执行,逐步打印中文说明,方便人工观察。
|
||||
//
|
||||
// 运行:
|
||||
// DISPLAY=:1 PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs \
|
||||
// node docs/user-manual/local-run-demo.mjs
|
||||
//
|
||||
// 环境变量:DEMO_URL(默认 http://127.0.0.1:5174/)、DEMO_PACE_MS(每步停留,默认 2500)、
|
||||
// DEMO_RUN_HOLD_MS(程序运行观察时长,默认 45000)、CHROME_PATH。
|
||||
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const baseUrl = process.env.DEMO_URL ?? "http://127.0.0.1:5174/";
|
||||
const chromePath = process.env.CHROME_PATH ?? "/usr/bin/google-chrome";
|
||||
const pace = Number(process.env.DEMO_PACE_MS ?? 2500);
|
||||
const runHold = Number(process.env.DEMO_RUN_HOLD_MS ?? 45000);
|
||||
const shotDir = resolve(here, "local-walkthrough");
|
||||
|
||||
const { chromium } = await import(process.env.PLAYWRIGHT_MODULE);
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
headless: false,
|
||||
args: ["--no-sandbox", "--start-maximized", "--disable-infobars", "--enable-unsafe-swiftshader"],
|
||||
});
|
||||
const page = await browser.newPage({ viewport: null });
|
||||
await page.bringToFront().catch(() => {});
|
||||
|
||||
const wait = (ms) => page.waitForTimeout(ms);
|
||||
const status = async () => (await page.locator(".machine-status").innerText()).replace(/\s+/g, " ").trim();
|
||||
const command = async () => (await page.locator(".command-status").innerText()).trim();
|
||||
const axes = async () => page.locator(".xyzac-position-grid output").allInnerTexts();
|
||||
const shot = (name) => page.screenshot({ path: resolve(shotDir, name) });
|
||||
const say = (step, message) => console.log(`\n[${step}] ${message}`);
|
||||
const detail = async () => {
|
||||
const text = await status();
|
||||
return `${text.match(/Task state (\S+)/)?.[1]} / ${text.match(/Interpreter (\S+)/)?.[1]} / ${text.match(/Machine sequence (NOT HOMED|HOMED)/)?.[1]}`;
|
||||
};
|
||||
const waitFor = async (check, timeout, label) => {
|
||||
const deadline = Date.now() + timeout;
|
||||
let last = "";
|
||||
while (Date.now() < deadline) {
|
||||
last = await check();
|
||||
if (last) return last;
|
||||
await wait(400);
|
||||
}
|
||||
throw new Error(`${label} 超时(最后:${last || "无"})`);
|
||||
};
|
||||
const waitStatus = (pattern, timeout = 60000) => waitFor(async () => {
|
||||
const text = await status();
|
||||
return pattern.test(text) ? text : null;
|
||||
}, timeout, `等待 ${pattern}`);
|
||||
const waitCase = () => waitFor(async () => ((await page.locator(".case-status").innerText()).trim().startsWith("Program:") ? "ok" : null), 120000, "程序装载");
|
||||
|
||||
say(1, `打开网站 ${baseUrl}`);
|
||||
await page.goto(baseUrl, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForSelector('[data-axis-control="workspace-case-select"]', { timeout: 60000 });
|
||||
await waitCase();
|
||||
await waitStatus(/STATE_/, 60000);
|
||||
await wait(2000);
|
||||
console.log(` 连接:${(await page.locator(".connection-state").innerText()).trim()}|当前程序:${(await page.locator(".case-status").innerText()).trim()}|状态:${await detail()}`);
|
||||
await shot("demo-1-opened.png");
|
||||
|
||||
say(2, "机床尚未上电,确认运动操作被门控挡住");
|
||||
const jogButton = page.locator('[data-axis-control="manual-jog-positive"]');
|
||||
console.log(` Jog 按钮:${(await jogButton.isDisabled()) ? `禁用(${await jogButton.getAttribute("title")})` : "可用"}|任务模式:${(await status()).match(/Task mode (\S+)/)?.[1]}`);
|
||||
await wait(pace);
|
||||
|
||||
say(3, "急停复位,然后上电");
|
||||
const estop = page.locator('[data-axis-control="toolbar-estop"]');
|
||||
if ((await estop.getAttribute("aria-label"))?.includes("Cancel")) { await estop.click(); await wait(1500); }
|
||||
await page.locator('[data-axis-control="toolbar-power"]').click();
|
||||
const powered = await waitStatus(/STATE_ON|STATE_IDLE/, 40000);
|
||||
console.log(` 上电完成:${powered.match(/Task state (\S+)/)?.[1]}|反馈:${await command()}`);
|
||||
await shot("demo-2-powered.png");
|
||||
await wait(pace);
|
||||
|
||||
say(4, "HOME ALL 全部关节回零");
|
||||
await page.locator('[data-axis-control="manual-home-all"]').click();
|
||||
await waitStatus(/Machine sequence HOMED/, 60000);
|
||||
await wait(1500);
|
||||
console.log(` 回零完成:${await detail()}|轴位:${(await axes()).join(" / ")}`);
|
||||
await shot("demo-3-homed.png");
|
||||
await wait(pace);
|
||||
|
||||
say(5, "载入 XYZAC 船体程序");
|
||||
await page.locator('[data-axis-control="workspace-load-boat"]').click();
|
||||
await waitCase();
|
||||
await wait(3000);
|
||||
console.log(` 程序:${(await page.locator(".case-status").innerText()).trim()}|反馈:${await command()}`);
|
||||
await shot("demo-4-program-loaded.png");
|
||||
await wait(pace);
|
||||
|
||||
say(6, "执行程序,观察解释器与轴运动");
|
||||
await page.locator('[data-axis-control="toolbar-run"]').click();
|
||||
await waitStatus(/STATE_RUNNING|INTERP_READING|INTERP_WAITING/, 60000);
|
||||
console.log(` 运行中:${await detail()}`);
|
||||
const samples = [];
|
||||
const holdUntil = Date.now() + runHold;
|
||||
let index = 0;
|
||||
while (Date.now() < holdUntil) {
|
||||
await wait(3000);
|
||||
const current = await axes();
|
||||
samples.push(current.slice(0, 5).join(" / "));
|
||||
index += 1;
|
||||
console.log(` +${String(index * 3).padStart(2)}s ${current.slice(0, 5).join(" ")}`);
|
||||
if (index % 3 === 0) await shot(`demo-5-running-${index / 3}.png`);
|
||||
}
|
||||
console.log(` 轴运动采样(X / Y / Z / A / C):\n ${samples.join("\n ")}`);
|
||||
await shot("demo-6-running-final.png");
|
||||
|
||||
say(7, "停止程序");
|
||||
if (await page.locator('[data-axis-control="toolbar-stop"]').isEnabled()) await page.locator('[data-axis-control="toolbar-stop"]').click();
|
||||
await wait(3000);
|
||||
console.log(` 已停止:${await detail()}|反馈:${await command()}`);
|
||||
await shot("demo-7-stopped.png");
|
||||
|
||||
console.log("\n演示结束,窗口保持 20 秒后关闭。");
|
||||
await wait(20000);
|
||||
await browser.close();
|
||||
312
docs/user-manual/local-walkthrough.mjs
Normal file
312
docs/user-manual/local-walkthrough.mjs
Normal file
@@ -0,0 +1,312 @@
|
||||
// 按"使用说明书"的操作顺序,在本地发布站点上完整走一遍标准使用流程,并逐步取证。
|
||||
//
|
||||
// 运行:
|
||||
// PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs \
|
||||
// node docs/user-manual/local-walkthrough.mjs
|
||||
//
|
||||
// 环境变量:WALKTHROUGH_URL(默认 http://127.0.0.1:5174/)、CHROME_PATH。
|
||||
// WALKTHROUGH_HEADED=1 在桌面打开可见窗口执行(便于人工观察)
|
||||
// WALKTHROUGH_PACE_MS 每一步之间的停留时间,默认可见模式 1500ms、无头模式 0
|
||||
// WALKTHROUGH_HOLD_MS 结束前保持窗口的时间,默认可见模式 20000ms、无头模式 0
|
||||
// 输出:docs/user-manual/local-walkthrough/(截图 + 分步报告 JSON)
|
||||
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const outDir = resolve(here, "local-walkthrough");
|
||||
const baseUrl = process.env.WALKTHROUGH_URL ?? "http://127.0.0.1:5174/";
|
||||
const chromePath = process.env.CHROME_PATH ?? "/usr/bin/google-chrome";
|
||||
const headed = process.env.WALKTHROUGH_HEADED === "1";
|
||||
const pace = Number(process.env.WALKTHROUGH_PACE_MS ?? (headed ? 1500 : 0));
|
||||
const hold = Number(process.env.WALKTHROUGH_HOLD_MS ?? (headed ? 20000 : 0));
|
||||
|
||||
await mkdir(outDir, { recursive: true });
|
||||
|
||||
const { chromium } = await import(process.env.PLAYWRIGHT_MODULE);
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
headless: !headed,
|
||||
args: headed
|
||||
? ["--no-sandbox", "--start-maximized", "--disable-infobars", "--enable-unsafe-swiftshader"]
|
||||
: ["--no-sandbox", "--enable-unsafe-swiftshader"],
|
||||
});
|
||||
const page = await browser.newPage({ viewport: headed ? null : { width: 1440, height: 900 } });
|
||||
await page.bringToFront().catch(() => {});
|
||||
const consoleErrors = [];
|
||||
page.on("pageerror", (error) => consoleErrors.push(String(error)));
|
||||
page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); });
|
||||
|
||||
const steps = [];
|
||||
const findings = [];
|
||||
const settle = (ms = 500) => page.waitForTimeout(ms);
|
||||
const text = async (selector) => (await page.locator(selector).first().innerText()).replace(/\s+/g, " ").trim();
|
||||
const status = () => text(".machine-status");
|
||||
const command = () => text(".command-status");
|
||||
const axes = async () => page.locator(".xyzac-position-grid output").allInnerTexts();
|
||||
const caseStatus = async () => (await page.locator(".case-status").innerText()).trim();
|
||||
const loadGateState = async () => {
|
||||
const button = page.locator('[data-axis-control="workspace-load-boat"]');
|
||||
return (await button.isDisabled()) ? `blocked:${await button.getAttribute("title")}` : "enabled";
|
||||
};
|
||||
const machineSequence = async () => (await status()).match(/Machine sequence (NOT HOMED|HOMED)/)?.[1] ?? "-";
|
||||
|
||||
const waitFor = async (predicate, timeout, label) => {
|
||||
const deadline = Date.now() + timeout;
|
||||
let last = "";
|
||||
while (Date.now() < deadline) {
|
||||
last = await predicate();
|
||||
if (last) return last;
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
throw new Error(`${label} 超时,最后观察:${String(last).slice(0, 160)}`);
|
||||
};
|
||||
|
||||
const waitStatus = async (pattern, timeout = 60000) => {
|
||||
const deadline = Date.now() + timeout;
|
||||
let last = "";
|
||||
while (Date.now() < deadline) {
|
||||
last = await status();
|
||||
if (pattern.test(last)) return last;
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
throw new Error(`等待 ${pattern} 超时,最后状态:${last.slice(0, 160)}`);
|
||||
};
|
||||
|
||||
const waitCase = (timeout = 120000) => waitFor(async () => {
|
||||
const value = await caseStatus();
|
||||
return value.startsWith("Program:") ? value : null;
|
||||
}, timeout, "案例程序就绪");
|
||||
|
||||
const record = async (index, title, body) => {
|
||||
const started = Date.now();
|
||||
console.log(`\n▶ 第 ${index} 步 ${title}`);
|
||||
try {
|
||||
const detail = await body();
|
||||
const shot = `step-${String(index).padStart(2, "0")}.png`;
|
||||
await page.screenshot({ path: resolve(outDir, shot) });
|
||||
steps.push({ index, title, status: "ok", ms: Date.now() - started, detail, screenshot: shot });
|
||||
console.log(` ✔ 完成${detail ? ` — ${JSON.stringify(detail)}` : ""}`);
|
||||
} catch (error) {
|
||||
const shot = `step-${String(index).padStart(2, "0")}-failed.png`;
|
||||
await page.screenshot({ path: resolve(outDir, shot) }).catch(() => {});
|
||||
steps.push({ index, title, status: "failed", ms: Date.now() - started, detail: String(error?.message ?? error), screenshot: shot });
|
||||
console.log(` ✘ 失败 — ${error?.message ?? error}`);
|
||||
}
|
||||
if (pace) await page.waitForTimeout(pace);
|
||||
};
|
||||
|
||||
console.log(`本地发布站点使用流程演练:${baseUrl}\n`);
|
||||
console.log(headed ? "模式:桌面可见窗口(可以看着它操作)" : "模式:无头浏览器");
|
||||
|
||||
// 1. 打开页面,等待 WASM 会话就绪
|
||||
await record(1, "打开页面并等待仿真会话就绪", async () => {
|
||||
await page.goto(baseUrl, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForSelector('[data-axis-control="workspace-case-select"]', { timeout: 60000 });
|
||||
await waitCase(90000);
|
||||
await waitStatus(/STATE_/, 60000);
|
||||
await settle(2000);
|
||||
return {
|
||||
标题: await page.title(),
|
||||
连接: (await text(".connection-state")),
|
||||
默认案例: await page.locator('[data-axis-control="workspace-case-select"]').inputValue().then((v) => v.split("/").pop()),
|
||||
当前程序: await caseStatus(),
|
||||
可渲染画布: await page.locator("canvas").count(),
|
||||
};
|
||||
});
|
||||
|
||||
// 2. 上电前:确认门控把运动操作锁住
|
||||
await record(2, "上电前确认门控:手动与运动操作被锁", async () => {
|
||||
const jog = page.locator('[data-axis-control="manual-jog-positive"]');
|
||||
const mdi = page.locator('[data-axis-control="mdi-command"]');
|
||||
return {
|
||||
任务状态: (await status()).match(/Task state (\S+)/)?.[1],
|
||||
回零状态: await machineSequence(),
|
||||
装载程序按钮: await loadGateState(),
|
||||
Jog按钮: await jog.isDisabled() ? `禁用(${await jog.getAttribute("title")})` : "可用",
|
||||
MDI输入: await mdi.isDisabled() ? "禁用" : "可用",
|
||||
};
|
||||
});
|
||||
|
||||
// 3. 急停复位 + 上电
|
||||
await record(3, "急停复位并上电", async () => {
|
||||
const estop = page.locator('[data-axis-control="toolbar-estop"]');
|
||||
if ((await estop.getAttribute("aria-label"))?.includes("Cancel")) { await estop.click(); await settle(1500); }
|
||||
const reset = await status();
|
||||
await page.locator('[data-axis-control="toolbar-power"]').click();
|
||||
const on = await waitStatus(/STATE_ON|STATE_IDLE/, 40000);
|
||||
return { 复位后: reset.match(/Task state (\S+)/)?.[1], 上电后: on.match(/Task state (\S+)/)?.[1] };
|
||||
});
|
||||
|
||||
// 4. 回零
|
||||
await record(4, "HOME ALL 全部关节回零", async () => {
|
||||
await page.locator('[data-axis-control="manual-home-all"]').click();
|
||||
await waitStatus(/Machine sequence HOMED/, 60000);
|
||||
await settle(1500);
|
||||
return { 回零状态: await machineSequence(), 轴位: await axes() };
|
||||
});
|
||||
|
||||
// 5. 连续 Jog
|
||||
await record(5, "手动连续 Jog:选中 X 轴,按住 + 移动,松手停止", async () => {
|
||||
await page.locator('[data-axis-control="manual-axis-x"]').check();
|
||||
const before = (await axes())[0];
|
||||
await page.locator('[data-axis-control="manual-jog-positive"]').hover();
|
||||
await page.mouse.down();
|
||||
await settle(1800);
|
||||
await page.mouse.up();
|
||||
await settle(1500);
|
||||
const moved = (await axes())[0];
|
||||
await settle(1500);
|
||||
const stopped = (await axes())[0];
|
||||
return { 移动前: before, 松开瞬间: moved, "1.5s后": stopped, 结论: moved !== before && stopped === moved ? "移动后停止" : "需复核" };
|
||||
});
|
||||
|
||||
// 6. 增量 Jog
|
||||
await record(6, "增量 Jog:0.1000 步长点动一次", async () => {
|
||||
await page.locator('[data-axis-control="manual-jog-increment"]').selectOption("0.1000");
|
||||
const before = Number((await axes())[0]);
|
||||
await page.locator('[data-axis-control="manual-jog-negative"]').hover();
|
||||
await page.mouse.down();
|
||||
await settle(1200);
|
||||
await page.mouse.up();
|
||||
await settle(2000);
|
||||
const after = Number((await axes())[0]);
|
||||
await page.locator('[data-axis-control="manual-jog-increment"]').selectOption("Continuous");
|
||||
return { 步长: "0.1000 mm", 前: before, 后: after, 实际位移: Number((before - after).toFixed(4)) };
|
||||
});
|
||||
|
||||
// 7. MDI
|
||||
await record(7, "MDI 执行 G0 X12.5 Y-4 Z0", async () => {
|
||||
await page.locator('[data-axis-control="tab-mdi"]').click();
|
||||
await page.locator('[data-axis-control="mdi-command"]').fill("G0 X12.5 Y-4 Z0");
|
||||
await page.locator('[data-axis-control="mdi-submit"]').click();
|
||||
await settle(3500);
|
||||
return { 命令: "G0 X12.5 Y-4 Z0", 轴位: await axes(), 反馈: await command(), 历史: await page.locator('[data-axis-control="mdi-history-select"]').first().innerText() };
|
||||
});
|
||||
|
||||
// 8. 载入案例自带的五轴程序
|
||||
await record(8, "MDI 之后切回手动模式(否则装载门控保持失效)", async () => {
|
||||
const afterMdi = await loadGateState();
|
||||
const modeBefore = (await status()).match(/Task mode (\S+)/)?.[1];
|
||||
await page.locator('[data-axis-control="tab-manual"]').click();
|
||||
await settle(3000);
|
||||
const afterManual = await loadGateState();
|
||||
if (afterMdi.startsWith("blocked:xyzac_motion_active") && afterManual === "enabled") {
|
||||
findings.push({
|
||||
id: "FINDING-01",
|
||||
severity: "medium",
|
||||
control: "MDI 之后的装载门控",
|
||||
observed: `MDI 执行完成后(Planner queued=0、Task mode=${modeBefore}),"Load XYZAC boat program" 仍被拒绝,提示 ${afterMdi};此时必须切回手动模式或刷新页面才能载入程序`,
|
||||
expectation: "MDI 运动结束、队列排空后装载程序应当自动恢复可用",
|
||||
workaround: "点一下左侧 Manual Control [F3] 页签(发出一条模式命令刷新门控),或刷新页面",
|
||||
});
|
||||
}
|
||||
return { MDI后: afterMdi, 切回手动后: afterManual, 任务模式: (await status()).match(/Task mode (\S+)/)?.[1] };
|
||||
});
|
||||
|
||||
// 9. 载入案例自带的五轴程序
|
||||
await record(9, "载入 XYZAC 船体程序并生成刀路", async () => {
|
||||
await page.locator('[data-axis-control="tab-preview"]').click();
|
||||
await page.locator('[data-axis-control="workspace-load-boat"]').click();
|
||||
await waitCase();
|
||||
await settle(3000);
|
||||
return { 程序: await caseStatus(), 反馈: await command() };
|
||||
});
|
||||
|
||||
// 10. 运行程序并采样轴运动
|
||||
await record(10, "运行程序,观察解释器与轴运动", async () => {
|
||||
await page.locator('[data-axis-control="toolbar-run"]').click();
|
||||
await waitStatus(/STATE_RUNNING|INTERP_READING|INTERP_WAITING/, 60000);
|
||||
const samples = [];
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
await settle(1200);
|
||||
samples.push((await axes()).slice(0, 4).map(Number));
|
||||
}
|
||||
return { 任务模式: (await status()).match(/Task mode (\S+)/)?.[1], 解释器: (await status()).match(/Interpreter (\S+)/)?.[1], 轴位采样: samples };
|
||||
});
|
||||
|
||||
// 11. 暂停并确认位置冻结
|
||||
await record(11, "暂停程序,确认位置冻结", async () => {
|
||||
await page.locator('[data-axis-control="toolbar-pause-resume"]').click();
|
||||
await waitStatus(/PAUSED|INTERP_PAUSED/, 30000);
|
||||
const first = await axes();
|
||||
await settle(2000);
|
||||
const second = await axes();
|
||||
return { 解释器: (await status()).match(/Interpreter (\S+)/)?.[1], 首次读数: first, "2s后读数": second, 位置冻结: JSON.stringify(first) === JSON.stringify(second) };
|
||||
});
|
||||
|
||||
// 12. 继续并停止
|
||||
await record(12, "继续运行后停止程序", async () => {
|
||||
await page.locator('[data-axis-control="toolbar-pause-resume"]').click();
|
||||
await settle(2500);
|
||||
const resumed = (await status()).match(/Interpreter (\S+)/)?.[1];
|
||||
await page.locator('[data-axis-control="toolbar-stop"]').click();
|
||||
await settle(3000);
|
||||
return { 继续后解释器: resumed, 停止后解释器: (await status()).match(/Interpreter (\S+)/)?.[1], 反馈: await command() };
|
||||
});
|
||||
|
||||
// 13. DRO 读数
|
||||
await record(13, "查看 DRO 位置显示并切换单位", async () => {
|
||||
await page.locator('[data-axis-control="tab-dro"]').click();
|
||||
await page.locator('[data-axis-control="dro-units"]').selectOption("inch");
|
||||
await settle(1200);
|
||||
const inch = await page.locator(".dro-row").allInnerTexts();
|
||||
await page.locator('[data-axis-control="dro-units"]').selectOption("mm");
|
||||
await page.locator('[data-axis-control="dro-coordinate"]').selectOption("relative");
|
||||
await settle(1200);
|
||||
return { 行数: inch.length, 英寸读数示例: inch[0]?.replace(/\s+/g, " "), 页脚: await text(".dro-footer") };
|
||||
});
|
||||
|
||||
// 14. 切换机型
|
||||
await record(14, "切换到 XYZBC TRT 机型,轴组随之改变", async () => {
|
||||
await page.locator('[data-axis-control="workspace-case-select"]').selectOption("linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini");
|
||||
await settle(2500);
|
||||
await waitCase();
|
||||
await settle(2500);
|
||||
const bc = await page.locator(".axis-radio span").allInnerTexts();
|
||||
await page.locator('[data-axis-control="workspace-case-select"]').selectOption("linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini");
|
||||
await settle(2500);
|
||||
return { XYZBC轴组: bc, 当前程序: await caseStatus() };
|
||||
});
|
||||
|
||||
// 15. 打开使用说明书
|
||||
await record(15, "从标题栏按钮打开使用说明书", async () => {
|
||||
const [manual] = await Promise.all([page.waitForEvent("popup"), page.locator('[data-axis-control="user-manual"]').click()]);
|
||||
await manual.waitForLoadState("load", { timeout: 60000 });
|
||||
await manual.waitForTimeout(1500);
|
||||
const info = { 标题: await manual.title(), 地址: manual.url(), 章节: await manual.locator("section").count(), 插图: await manual.locator("figure img").count() };
|
||||
await manual.screenshot({ path: resolve(outDir, "step-15-manual.png") });
|
||||
await manual.close();
|
||||
return info;
|
||||
});
|
||||
|
||||
// 16. 诊断页
|
||||
await record(16, "查看诊断页的启动资源与门控", async () => {
|
||||
await page.locator('[data-axis-control="tab-diagnostics"]').click();
|
||||
await settle(1200);
|
||||
const body = await text("#axis-panel-diagnostics");
|
||||
return { 摘要: body.slice(0, 220) };
|
||||
});
|
||||
|
||||
const report = {
|
||||
report_id: "LINUXCNC-WEB-LOCAL-WALKTHROUGH",
|
||||
url: baseUrl,
|
||||
ran_at: new Date().toISOString(),
|
||||
status: steps.every((step) => step.status === "ok") ? "verified" : "failed",
|
||||
steps,
|
||||
findings,
|
||||
console_errors: consoleErrors,
|
||||
};
|
||||
await writeFile(resolve(outDir, "walkthrough.report.json"), `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(`\n${report.status.toUpperCase()} ${steps.filter((s) => s.status === "ok").length}/${steps.length} 步完成 → ${resolve(outDir, "walkthrough.report.json")}`);
|
||||
if (consoleErrors.length) console.log("页面错误:", consoleErrors.slice(0, 3));
|
||||
if (findings.length) console.log(`\n发现问题 ${findings.length} 项:`);
|
||||
for (const item of findings) console.log(` ${item.id}|${item.control}|${item.observed}`);
|
||||
|
||||
if (hold) {
|
||||
console.log(`演练结束,窗口保持 ${Math.round(hold / 1000)} 秒供查看…`);
|
||||
await page.waitForTimeout(hold);
|
||||
}
|
||||
await browser.close();
|
||||
process.exit(report.status === "verified" ? 0 : 1);
|
||||
564
docs/user-manual/manual-content.html
Normal file
564
docs/user-manual/manual-content.html
Normal file
@@ -0,0 +1,564 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="description" content="LinuxCNC Web/WASM 五轴机床仿真平台使用说明书">
|
||||
<link rel="icon" href="data:,">
|
||||
<title>LinuxCNC Web/WASM 使用说明书</title>
|
||||
<style>
|
||||
:root{--ink:#172632;--muted:#5b7180;--line:#d7e3e8;--paper:#f5f9fa;--card:#fff;--navy:#113a50;--teal:#087f87;--cyan:#18b8be;--orange:#e88b43;--green:#16845e;--red:#c0392b;--code:#0d2633;--shadow:0 16px 45px #173b4a18}
|
||||
*{box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{margin:0;color:var(--ink);background:radial-gradient(circle at 90% 0,#1ab8c01e,transparent 28rem),linear-gradient(180deg,#e8f3f6 0,var(--paper) 27rem,#fff 100%);font:15px/1.75 Inter,"Noto Sans SC","Microsoft YaHei",sans-serif}
|
||||
a{color:var(--teal);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
.topbar{position:sticky;top:0;z-index:5;display:flex;justify-content:space-between;gap:1rem;padding:.72rem clamp(1rem,4vw,4rem);color:#fff;background:#103a50f5;backdrop-filter:blur(14px);box-shadow:0 4px 22px #102d3d25}
|
||||
.brand{display:flex;align-items:center;gap:.65rem;font-weight:800}
|
||||
.mark{display:grid;place-items:center;width:2rem;height:2rem;border-radius:10px;color:#12384a;background:var(--cyan);font-size:.72rem;font-weight:900}
|
||||
.actions{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.actions a,.actions button{border:1px solid #ffffff55;border-radius:999px;padding:.28rem .75rem;color:#fff;background:#ffffff12;cursor:pointer;font:inherit}
|
||||
.actions a:hover,.actions button:hover{background:#ffffff2b;text-decoration:none}
|
||||
main{max-width:1360px;margin:auto;padding:0 clamp(1rem,4vw,4rem) 5rem}
|
||||
.hero{padding:2.6rem 0 1.4rem}
|
||||
.eyebrow{margin:0 0 .6rem;color:var(--teal);font-size:.78rem;font-weight:900;letter-spacing:.16em}
|
||||
.hero h1{margin:0;color:var(--navy);font-size:clamp(2rem,4.4vw,3.4rem);line-height:1.1;letter-spacing:-.045em}
|
||||
.lead{max-width:62rem;margin:1rem 0 0;color:var(--muted);font-size:1.06rem}
|
||||
.tags{display:flex;flex-wrap:wrap;gap:.5rem;margin:1.1rem 0 0}
|
||||
.tag{display:inline-flex;gap:.3rem;border:1px solid var(--line);border-radius:999px;padding:.23rem .68rem;color:var(--muted);background:#ffffffd9;font-size:.82rem}
|
||||
.tag b{color:var(--navy)}
|
||||
.toc{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:.55rem;margin:1.6rem 0 2.4rem}
|
||||
.toc a{display:block;padding:.62rem .8rem;border:1px solid var(--line);border-radius:12px;color:var(--navy);background:var(--card);font-size:.84rem;font-weight:800;box-shadow:0 5px 15px #173b4a08}
|
||||
section{padding:1.6rem 0;border-top:1px solid var(--line)}
|
||||
section>h2{margin:.2rem 0 1rem;color:var(--navy);font-size:clamp(1.35rem,2.4vw,1.9rem);letter-spacing:-.02em}
|
||||
h3{margin:1.6rem 0 .6rem;color:var(--navy);font-size:1.06rem}
|
||||
h4{margin:1.1rem 0 .4rem;color:var(--teal);font-size:.95rem}
|
||||
p{margin:.6rem 0}
|
||||
ul,ol{margin:.5rem 0 .9rem;padding-left:1.35rem}
|
||||
li{margin:.25rem 0}
|
||||
code,kbd{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}
|
||||
code{padding:.1rem .38rem;border-radius:6px;color:#0d3b46;background:#e6f2f4;font-size:.86em}
|
||||
kbd{display:inline-block;min-width:1.5rem;padding:.08rem .4rem;border:1px solid #b9cbd4;border-bottom-width:2px;border-radius:6px;color:#25384a;background:#fff;font-size:.78rem;text-align:center}
|
||||
.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(19rem,1fr));gap:1rem}
|
||||
.card{border:1px solid var(--line);border-radius:16px;padding:1rem 1.15rem;background:var(--card);box-shadow:0 8px 24px #173b4a0f}
|
||||
.card h4{margin-top:0}
|
||||
.note{border-left:4px solid var(--orange);padding:.72rem 1rem;border-radius:0 12px 12px 0;color:#72451f;background:#fff3e7;margin:1rem 0}
|
||||
.danger{border-left:4px solid var(--red);padding:.72rem 1rem;border-radius:0 12px 12px 0;color:#7d241a;background:#fdeceb;margin:1rem 0}
|
||||
.ok{border-left:4px solid var(--green);padding:.72rem 1rem;border-radius:0 12px 12px 0;color:#14503a;background:#e9f7f1;margin:1rem 0}
|
||||
table{width:100%;border-collapse:collapse;margin:.8rem 0 1.2rem;background:var(--card);border:1px solid var(--line);border-radius:12px;overflow:hidden;font-size:.9rem}
|
||||
th,td{padding:.55rem .7rem;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
|
||||
th{color:#fff;background:#17505f;font-weight:700;white-space:nowrap}
|
||||
td,th{overflow-wrap:anywhere}
|
||||
code{overflow-wrap:anywhere}
|
||||
tbody tr:nth-child(even){background:#f7fbfc}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
figure{margin:1rem 0 1.4rem}
|
||||
figure img{width:100%;height:auto;border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);display:block}
|
||||
figcaption{margin-top:.5rem;color:var(--muted);font-size:.84rem}
|
||||
.pill{display:inline-block;padding:.1rem .5rem;border-radius:999px;font-size:.76rem;font-weight:800}
|
||||
.pill.yes{color:#0f5a3f;background:#d9f2e7}
|
||||
.pill.no{color:#8a2b1f;background:#fbe2df}
|
||||
.pill.part{color:#7a5410;background:#fdf0d5}
|
||||
.steps{counter-reset:step;list-style:none;padding-left:0}
|
||||
.steps>li{position:relative;padding-left:3rem;margin:.85rem 0}
|
||||
.steps>li::before{counter-increment:step;content:counter(step);position:absolute;left:0;top:0;display:grid;place-items:center;width:2.1rem;height:2.1rem;border-radius:50%;color:#fff;background:var(--teal);font-weight:800}
|
||||
.steps>li b{color:var(--navy)}
|
||||
.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.86rem;color:#0d3b46;background:#eef6f8;border:1px solid var(--line);border-radius:10px;padding:.6rem .8rem;display:block;overflow-x:auto}
|
||||
footer{padding:2.2rem clamp(1rem,4vw,4rem) 3rem;color:#cfe4ea;background:#103a50;font-size:.86rem}
|
||||
footer a{color:#8fdadf}
|
||||
@media(max-width:640px){table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}th{white-space:normal}}
|
||||
@media print{.topbar,.actions,.toc{display:none}body{background:#fff}section{break-inside:avoid}figure{break-inside:avoid}figure img{box-shadow:none}a{color:var(--ink)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="brand"><span class="mark">CNC</span><span>LinuxCNC Web/WASM 使用说明书</span></div>
|
||||
<div class="actions">
|
||||
<a href="https://49.232.250.35:58010/" target="_blank" rel="noopener">打开软件</a>
|
||||
<button type="button" onclick="window.print()">打印 / 导出 PDF</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="hero">
|
||||
<p class="eyebrow">WEB / WASM · 五轴机床仿真与 RTCP 教学平台</p>
|
||||
<h1>LinuxCNC Web/WASM 使用说明书</h1>
|
||||
<p class="lead">本手册面向机床操作者与教学使用者,说明如何用浏览器访问这套 LinuxCNC 仿真控制器:选择机床案例、上电回零、手动与 MDI 操作、运行 G 代码、使用 RTCP 程序库,以及三维机床模型的导入、标定与保存。全文操作均在浏览器内完成,不需要安装 LinuxCNC,也不连接真实机床与硬件。</p>
|
||||
<div class="tags">
|
||||
<span class="tag">访问地址 <b>https://49.232.250.35:58010/</b></span>
|
||||
<span class="tag">来源基线 <b>LinuxCNC 2.10.0~pre0</b></span>
|
||||
<span class="tag">当前可用机型 <b>XYZAC / XYZBC 转头转台(TRT)</b></span>
|
||||
<span class="tag">功能巡检 <b>26 项通过</b></span>
|
||||
<span class="tag">手册版本 <b>2026-10-08</b></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="toc" aria-label="目录">
|
||||
<a href="#s1">1. 软件概览与访问</a>
|
||||
<a href="#s2">2. 五分钟快速上手</a>
|
||||
<a href="#s3">3. 界面总览</a>
|
||||
<a href="#s4">4. 状态与运行反馈</a>
|
||||
<a href="#s5">5. 机床案例与程序</a>
|
||||
<a href="#s6">6. 手动控制</a>
|
||||
<a href="#s7">7. MDI 与 DRO</a>
|
||||
<a href="#s8">8. 倍率与显示图层</a>
|
||||
<a href="#s9">9. 程序运行控制</a>
|
||||
<a href="#s10">10. 三维预览与机床管理</a>
|
||||
<a href="#s11">11. 诊断信息</a>
|
||||
<a href="#s12">12. 快捷键与鼠标</a>
|
||||
<a href="#s13">13. 支持的机床案例</a>
|
||||
<a href="#s14">14. 已知问题与规避</a>
|
||||
<a href="#s15">15. 常见问题</a>
|
||||
<a href="#s16">16. 附录:功能测试结论</a>
|
||||
</nav>
|
||||
|
||||
<section id="s1">
|
||||
<h2>1. 软件概览与访问</h2>
|
||||
|
||||
<h3>1.1 这是什么</h3>
|
||||
<p>这是一个把 LinuxCNC 仿真行为搬进浏览器的 Web 应用:数值密集的实时任务(解释器、轨迹规划、运动学正逆解)编译成 WebAssembly,在浏览器的独立 Worker 线程里运行;界面参照 LinuxCNC 官方 AXIS 控制面板,另加三维机床模型预览与五轴 RTCP 教学资源。</p>
|
||||
<ul>
|
||||
<li><b>不连接真实设备</b>:没有伺服、主轴、刀库和限位开关,所有运动都是仿真,不会造成任何机械或人身风险,适合教学与工艺验证。</li>
|
||||
<li><b>无需安装</b>:支持 WebAssembly 的现代浏览器(推荐最新版 Chrome 或 Edge)直接打开即可。</li>
|
||||
<li><b>数据不出本机</b>:G 代码解析、仿真运算都在浏览器里执行;导入的机床模型也只保留在页面与浏览器本地存储中。</li>
|
||||
</ul>
|
||||
|
||||
<h3>1.2 访问地址与首次打开</h3>
|
||||
<p>在浏览器地址栏输入或点击:<a href="https://49.232.250.35:58010/" target="_blank" rel="noopener">https://49.232.250.35:58010/</a></p>
|
||||
<div class="note">
|
||||
<b>证书提示:</b>该站点使用 IP 地址的 HTTPS 自签证书,浏览器可能提示"您的连接不是私密连接 / 证书无效"。在确认地址为 <code>49.232.250.35:58010</code> 后,选择"高级 → 继续前往"即可。企业环境下也可以让管理员把证书导入受信任列表。
|
||||
</div>
|
||||
<p>打开后右上角会显示 <b>Connection: READY</b>,下方状态栏从 <code>PENDING</code> 变为具体状态(如 <code>STATE_ESTOP_RESET</code>),即表示 WASM 仿真会话已经就绪。首次加载需要下载约 1.7 MB 的前端与 WASM 资源,之后浏览器会缓存。</p>
|
||||
<p>标题栏右侧还有一个蓝色的 <b>使用说明书</b> 按钮,随时点击即可在新标签页打开本手册;菜单 <b>Help → 使用说明书 / User manual</b> 是同一个入口。</p>
|
||||
|
||||
<h3>1.3 边界说明(请提前知悉)</h3>
|
||||
<ul>
|
||||
<li>当前发布版本只支持 <b>XYZAC TRT</b> 与 <b>XYZBC TRT</b> 两个五轴转头转台案例的完整操作链;其余案例可以选中,但控制器不会就绪,详见 <a href="#s13">第 13 节</a> 与 <a href="#s14">第 14 节</a>。</li>
|
||||
<li>少数 LinuxCNC 原生命令(如程序反向/正向、块删除、可选停止、Rapid 倍率、刀具表编辑)在本 WASM 运行时尚未开放,界面按钮置灰并在悬停时说明原因,这是设计上的"显式边界",不是故障。</li>
|
||||
<li>断电崩溃恢复只覆盖页面刷新与 OPFS 检查点范围,不代表物理断电场景。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="s2">
|
||||
<h2>2. 五分钟快速上手</h2>
|
||||
<p>下面是从零到"跑起来一段五轴程序"的最短路径,按顺序操作即可。</p>
|
||||
<ol class="steps">
|
||||
<li><b>打开页面。</b>访问 <a href="https://49.232.250.35:58010/" target="_blank" rel="noopener">https://49.232.250.35:58010/</a>,等待右上角变成 <code>Connection: READY</code>、案例状态显示 <code>Program: boat-xyzac.ngc</code>。</li>
|
||||
<li><b>确认仿真案例。</b>"Simulation case"下拉框选择 <code>XYZAC TRT</code>(默认值)。</li>
|
||||
<li><b>急停复位并上电。</b>点击工具栏最左侧红色"急停"图标解除急停,再点击其右侧"上电"图标。<b>关键:请按此顺序操作后,才会出现 <code>STATE_ON</code>。</b></li>
|
||||
<li><b>回零。</b>在左下"Manual Control"页签点击 <b>HOME ALL</b>,状态栏的 <code>Machine sequence</code> 变为 <code>HOMED</code>。</li>
|
||||
<li><b>动一下。</b>选轴 <code>X</code>,按住 <b>+</b> 连续 Jog,或切到 <b>MDI [F5]</b> 页签输入 <code>G0 X10 Y0 Z0</code> 后回车。</li>
|
||||
<li><b>跑程序。</b>点击案例条上的 <b>Load XYZAC boat program</b>,等状态显示 <code>Program: boat-xyzac.ngc</code> 后点击工具栏"运行"图标;用"暂停/继续"和"停止"控制执行。</li>
|
||||
</ol>
|
||||
<figure>
|
||||
<img src="screenshots/01-main-overview.png" alt="软件主界面,处于 XYZAC TRT 案例就绪状态">
|
||||
<figcaption>图 1 软件主界面(XYZAC TRT,已载入 boat-xyzac.ngc,等待上电与回零)。点击可放大查看各区域位置,区域说明见第 3 节。</figcaption>
|
||||
</figure>
|
||||
<div class="ok"><b>提示:</b>只要不刷新页面,程序、机床模型和当前位置都会保留。刷新后仿真会按当前配置重新启动;若之前保存过检查点,可看到恢复提示。</div>
|
||||
</section>
|
||||
|
||||
<section id="s3">
|
||||
<h2>3. 界面总览</h2>
|
||||
<p>界面自上而下分为六层,操作时按"选择对象 → 上电 → 回零 → 手动/程序"的次序使用即可。</p>
|
||||
<table>
|
||||
<thead><tr><th>区域</th><th>位置</th><th>作用</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>标题栏</td><td>最上方</td><td>左侧为产品标识;右侧为 <b>使用说明书</b> 按钮(新标签页打开本手册)与连接状态指示 <b>Connection: READY</b></td></tr>
|
||||
<tr><td>菜单栏</td><td>标题栏下方</td><td><b>File</b>(宿主边界项,置灰)、<b>Help → 使用说明书 / About AXIS</b></td></tr>
|
||||
<tr><td>工具栏</td><td>菜单下方图标行</td><td>急停、上电、打开文件、载入案例、重载、运行、单步、暂停/继续、停止、缩放、六个视图方向、拖动旋转、清除轨迹</td></tr>
|
||||
<tr><td>案例与程序条</td><td>工具栏下方</td><td>选择仿真案例、载入内置船体程序、打开 RTCP 程序库、显示当前程序与加载进度</td></tr>
|
||||
<tr><td>状态栏</td><td>案例条下方</td><td>Task/解释器/运动模式/回零等运行状态,以及最近一条命令反馈</td></tr>
|
||||
<tr><td>轴位与实时参数</td><td>状态栏下方</td><td>当前机型的坐标轴读数与关节值(J0…Jn)、偏置,以及主轴/进给/速度/倍率等实时量</td></tr>
|
||||
<tr><td>操作区(左右分栏)</td><td>页面下半部</td><td>左:Manual Control / MDI / Diagnostics 页签;右:Preview(刀路 + 机床模型)/ DRO 页签。底部为倍率滑块与预览图层开关</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="note">界面图标沿用 LinuxCNC 官方 AXIS 位图资源,含义与桌面版一致(绿点=上电、双三角=运行、蓝色暂停=暂停、红色方块=停止)。把鼠标悬停在按钮上会显示名称和快捷键,按钮被禁用时会说明被禁用的原因。</div>
|
||||
</section>
|
||||
|
||||
<section id="s4">
|
||||
<h2>4. 状态与运行反馈</h2>
|
||||
|
||||
<h3>4.1 状态栏字段</h3>
|
||||
<table>
|
||||
<thead><tr><th>字段</th><th>取值示例</th><th>含义</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Task mode</td><td>MANUAL / MDI / AUTO</td><td>任务模式:手动、MDI 单句、自动运行程序</td></tr>
|
||||
<tr><td>Task state</td><td>STATE_ESTOP、STATE_ESTOP_RESET、STATE_ON</td><td>机床状态:急停中、急停已解除、已上电</td></tr>
|
||||
<tr><td>Interpreter</td><td>INTERP_IDLE / INTERP_READING / INTERP_PAUSED</td><td>G 代码解释器状态:空闲、读取中、已暂停</td></tr>
|
||||
<tr><td>Motion mode</td><td>FREE / TELEOP / COORD</td><td>运动模式:自由、手动点动、坐标运动</td></tr>
|
||||
<tr><td>Motion type</td><td>0 / 1 …</td><td>当前运动类型编号(直线、圆弧等)</td></tr>
|
||||
<tr><td>Homing</td><td>IDLE / ACTIVE</td><td>是否正在回零</td></tr>
|
||||
<tr><td>Machine sequence</td><td>NOT HOMED / HOMED</td><td>机床是否已完成回零</td></tr>
|
||||
<tr><td>命令反馈</td><td>PROGRAM_LOAD acknowledged</td><td>最近一条命令的处理结果;被拒时也会在此显示原因</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>4.2 常见"操作被拒绝"及处理</h3>
|
||||
<table>
|
||||
<thead><tr><th>现象</th><th>原因</th><th>处理</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>上电按钮置灰,提示 <code>linuxcnc_wasm_snapshot_pending</code></td><td>仿真会话还没就绪</td><td>等待状态栏出现具体状态;若超过 1 分钟仍为 PENDING,刷新页面</td></tr>
|
||||
<tr><td>Jog / MDI / 主轴按钮置灰,提示 <code>axis_machine_off</code></td><td>尚未上电</td><td>先急停复位,再点上电</td></tr>
|
||||
<tr><td>手动按钮置灰,提示 <code>not_homed</code></td><td>未回零</td><td>点击 HOME ALL</td></tr>
|
||||
<tr><td>加载程序被拒,提示机型不匹配</td><td>程序要求的轴组与当前机型不一致(如 AC 程序用于 BC 机型)</td><td>切换仿真案例,或在 RTCP 程序库中选择当前机型分类下的程序</td></tr>
|
||||
<tr><td>单步/继续被拒</td><td>当前没有处于暂停或运行状态</td><td>先运行或暂停程序</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="danger"><b>安全习惯:</b>本平台只做仿真。但为养成正确操作习惯,请始终遵循"急停复位 → 上电 → 回零 → 运行"的顺序,题目与机床操作一致。</div>
|
||||
</section>
|
||||
|
||||
<section id="s5">
|
||||
<h2>5. 机床案例与程序</h2>
|
||||
|
||||
<h3>5.1 选择仿真案例</h3>
|
||||
<p>"Simulation case"下拉框列出全部案例。切换案例会重新加载该机床的配置(轴数、限位、运动学、默认程序),界面轴位显示与 Jog 轴按钮随之改变。</p>
|
||||
<p>切换到 <b>XYZBC TRT</b> 后,轴组从 <code>X Y Z A C</code> 变为 <code>X Y Z B C</code>,默认程序变为 <code>boat-xyzbc.ngc</code>:</p>
|
||||
<figure>
|
||||
<img src="screenshots/11-case-xyzbc.png" alt="切换到 XYZBC TRT 案例后的界面">
|
||||
<figcaption>图 2 切换到 XYZBC TRT 案例,轴组、机床标定参数与默认程序同步更新。</figcaption>
|
||||
</figure>
|
||||
<div class="note">切换案例时,界面会先自动下电并重载工作区,正在运行的程序会被终止。请先停止程序再切换机型。</div>
|
||||
|
||||
<h3>5.2 内置船体程序与 RTCP 程序库</h3>
|
||||
<p>案例条上提供两个程序入口:</p>
|
||||
<ul>
|
||||
<li><b>Load XYZAC boat program</b>:一键载入该机型自带的五轴船体干运行程序,适合快速演示。</li>
|
||||
<li><b>AC(XYZAC)RTCP 程序 (16)</b>:打开 LinuxCNC 官方 RTCP 程序库。</li>
|
||||
</ul>
|
||||
<figure>
|
||||
<img src="screenshots/04-rtcp-program-library.png" alt="RTCP 程序库对话框">
|
||||
<figcaption>图 3 RTCP 程序库。上方显示当前机型分类数量与可直接加载数量,中间可选择显示范围与具体程序,下方给出类型、机型、特点、源路径与兼容性判断。</figcaption>
|
||||
</figure>
|
||||
<p>程序库按当前机型自动过滤:AC 机型默认显示 16 个相关程序,其中 9 个可作为主程序直接加载;切换到"全部 45 个程序"可以查看和下载其它机型的源程序,但它们不会作为当前机型的可执行项。每个程序都提供:</p>
|
||||
<table>
|
||||
<thead><tr><th>按钮</th><th>作用</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>查看源程序</td><td>在对话框内直接阅读该 <code>.ngc</code> 的完整文本(来自 LinuxCNC 源码树)</td></tr>
|
||||
<tr><td>加载到控制器</td><td>把程序文本交给 LinuxCNC WASM 解释器,刷新刀路预览;机型不匹配时按钮禁用并说明原因</td></tr>
|
||||
<tr><td>下载 .ngc</td><td>保存原始程序文件到本地,便于在其它软件中使用</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<figure>
|
||||
<img src="screenshots/22-rtcp-load.png" alt="RTCP 程序加载成功提示">
|
||||
<figcaption>图 4 加载成功后在对话框底部提示"程序已加载到 LinuxCNC WASM 控制器",同时预览区刷新为新的刀路。</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>5.3 打开本地 G 代码</h3>
|
||||
<p>点击工具栏"打开文件"图标,选择本地 <code>.ngc / .nc / .gcode / .tap</code> 或纯文本文件即可载入。载入前会检查程序中的轴字是否与当前机型匹配,不匹配会拒绝并给出提示,避免用 AC 程序驱动 BC 机型。</p>
|
||||
<p>工具栏"重载"图标(<kbd>Ctrl</kbd>+<kbd>R</kbd>)用于把当前编辑器中的程序重新装载到解释器,适合手工修改后快速重试。</p>
|
||||
<figure>
|
||||
<img src="screenshots/03-file-menu.png" alt="File 菜单中两项宿主边界功能置灰">
|
||||
<figcaption>图 5 File 菜单中"编辑刀具数据""退出"属于宿主进程能力,在浏览器中明确置灰,而不是伪装成可用。</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section id="s6">
|
||||
<h2>6. 手动控制</h2>
|
||||
<p>左下角 <b>Manual Control [F3]</b> 页签是日常操作的主要区域。所有按钮都受"上电 + 已回零 + 无程序运行"的门控约束,条件不满足时置灰并说明原因。</p>
|
||||
|
||||
<h3>6.1 回零</h3>
|
||||
<ul>
|
||||
<li><b>HOME ALL</b>:按机床配置依次回零所有关节,完成后按钮变为 RE-HOME ALL。</li>
|
||||
<li><b>Joint 下拉 + Home Joint</b>:只回零指定关节,便于排查单轴问题。</li>
|
||||
</ul>
|
||||
<figure>
|
||||
<img src="screenshots/12-manual-homed.png" alt="回零完成后的界面">
|
||||
<figcaption>图 6 回零完成:状态栏 <code>Machine sequence</code> 显示 HOMED,轴位归零,手动与程序按钮全部解锁。</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>6.2 Jog 点动</h3>
|
||||
<ol>
|
||||
<li>在 <b>Axis</b> 中选中要移动的轴(如 <code>X</code>)。</li>
|
||||
<li>在 <b>Jog</b> 行按住 <b>-</b> 或 <b>+</b> 连续点动,松开即停。</li>
|
||||
<li>需要精确步进时,把中间的步长下拉框从 <code>Continuous</code> 改为 <code>0.1000 / 0.0100 / 0.0010 / 0.0001</code>,再点按一次按钮,即按该步长走一段。</li>
|
||||
<li>键盘快捷:<kbd>c</kbd> 切回连续,<kbd>i</kbd> 在步长之间循环切换。</li>
|
||||
</ol>
|
||||
<figure>
|
||||
<img src="screenshots/13-manual-jog.png" alt="按住 Jog 正向按钮时 X 轴位置变化">
|
||||
<figcaption>图 7 按住 Jog <b>+</b> 期间 X 轴数值实时跟随 WASM 运动学输出变化;松开后停止。</figcaption>
|
||||
</figure>
|
||||
<div class="note"><b>Jog Speed 倍率</b>位于页面底部,可按比例降低点动速度;五轴机床在回转轴接近行程极限时建议调低,便于观察。</div>
|
||||
|
||||
<h3>6.3 主轴、冷却液与刀具</h3>
|
||||
<table>
|
||||
<thead><tr><th>控件</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>主轴逆时针 / 顺时针图标</td><td>启动主轴相应转向,图标按下表示当前转向</td></tr>
|
||||
<tr><td>Stop Spindle</td><td>停止主轴</td></tr>
|
||||
<tr><td>+ / - 长按</td><td>按住期间以 100 rpm 步进增减转速,松开恢复恒定转速</td></tr>
|
||||
<tr><td>Brake</td><td>主轴制动(快捷键 <kbd>B</kbd> 制动 / <kbd>b</kbd> 松开)</td></tr>
|
||||
<tr><td>Mist / Flood</td><td>雾状 / 大流量冷却液开关</td></tr>
|
||||
<tr><td>Tool / Probe</td><td>显示当前刀具号、刀具状态与探针状态(只读)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<figure>
|
||||
<img src="screenshots/14-spindle-coolant.png" alt="主轴与冷却液操作">
|
||||
<figcaption>图 8 启动主轴与打开雾状冷却液后,上方"Live parameters"面板同步显示主轴转速与指令值。</figcaption>
|
||||
</figure>
|
||||
<div class="note"><b>Touch Off / Tool Touch Off</b>:用于设置工件坐标系与刀具偏置。当前 WASM 运行时未开放刀具偏置 ABI,Tool Touch Off 置灰;Touch Off 的可用性随机型与状态变化。</div>
|
||||
</section>
|
||||
|
||||
<section id="s7">
|
||||
<h2>7. MDI 与 DRO</h2>
|
||||
|
||||
<h3>7.1 MDI 单句执行</h3>
|
||||
<p>切到 <b>MDI [F5]</b> 页签,在"MDI Command"输入框键入一行 G 代码,回车或点 <b>Go</b> 执行。按 <kbd>↑</kbd> / <kbd>↓</kbd> 可在历史命令间翻找,双击历史条目也可以直接执行。</p>
|
||||
<figure>
|
||||
<img src="screenshots/15-mdi.png" alt="MDI 执行 G0 指令后的界面">
|
||||
<figcaption>图 9 执行 <code>G0 X12.5 Y-4 Z0</code> 后,轴位与刀路预览同步更新,命令进入下方 MDI History。</figcaption>
|
||||
</figure>
|
||||
<p>常用 MDI 指令:</p>
|
||||
<span class="mono">G0 X0 Y0 Z0 快速定位到原点<br>G1 X10 F200 以 200 mm/min 直线进给<br>G91 G1 X5 F100 增量方式移动 5 mm<br>M3 S1200 主轴正转 1200 rpm<br>G54 切入 1 号工件坐标系</span>
|
||||
|
||||
<h3>7.2 DRO 位置显示</h3>
|
||||
<p><b>DRO</b> 页签以表格形式显示每个轴的三个读数,并提供三个显示开关:</p>
|
||||
<table>
|
||||
<thead><tr><th>开关</th><th>可选值</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Coordinate</td><td>Machine / Relative</td><td>机床坐标(回零后原点)或相对坐标</td></tr>
|
||||
<tr><td>Display</td><td>Actual / Commanded</td><td>实际反馈位置或指令目标位置</td></tr>
|
||||
<tr><td>Units</td><td>mm / inch</td><td>线性轴单位;旋转轴始终为度</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<figure>
|
||||
<img src="screenshots/16-dro.png" alt="DRO 页签">
|
||||
<figcaption>图 10 DRO 页签显示实际位置、指令位置与相对坐标三列,页脚汇总单位、当前协调模式和仿真 tick。</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section id="s8">
|
||||
<h2>8. 倍率与显示图层</h2>
|
||||
<p>页面底部左侧是四个倍率滑块,右侧是六个预览图层开关。</p>
|
||||
<table>
|
||||
<thead><tr><th>控件</th><th>范围</th><th>用途</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Jog Speed</td><td>6% – 100%</td><td>手动点动速度比例</td></tr>
|
||||
<tr><td>Feed Override</td><td>0% – 上限</td><td>程序进给倍率,调试时降低进给</td></tr>
|
||||
<tr><td>Spindle Override</td><td>0% – 200%</td><td>程序主轴转速倍率</td></tr>
|
||||
<tr><td>Max Velocity</td><td>6% – 100%</td><td>整体最大速度限制</td></tr>
|
||||
<tr><td>Rapid Override</td><td>—</td><td>当前 WASM 轨迹规划器未开放,固定显示 Unavailable</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>预览图层:<code>path</code> 刀具路径、<code>live</code> 实时刀具位置、<code>limits</code> 行程边界、<code>velocity</code> 速度着色、<code>offsets</code> 坐标偏置指示、<code>extents</code> 毛坯包络。按需勾选可以避免画面过密。</p>
|
||||
<figure>
|
||||
<img src="screenshots/17-overrides-layers.png" alt="倍率滑块与预览图层开关">
|
||||
<figcaption>图 11 进给倍率调整到 80%,并临时关闭 velocity 图层观察刀路本体。</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section id="s9">
|
||||
<h2>9. 程序运行控制</h2>
|
||||
<p>工具栏中部的五个按钮控制程序执行,顺序与 LinuxCNC AXIS 一致:</p>
|
||||
<table>
|
||||
<thead><tr><th>按钮</th><th>作用</th><th>前置条件</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>运行</td><td>从当前行开始执行程序</td><td>机床已上电、已回零、程序已装载</td></tr>
|
||||
<tr><td>单步</td><td>只执行下一行</td><td>程序处于运行或暂停状态</td></tr>
|
||||
<tr><td>暂停 / 继续</td><td>暂停当前运动;再次点击继续</td><td>程序运行中 / 已暂停</td></tr>
|
||||
<tr><td>停止</td><td>中止程序并清空运动队列</td><td>程序运行或暂停中</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<figure>
|
||||
<img src="screenshots/18-program-running.png" alt="程序运行中">
|
||||
<figcaption>图 12 运行中:Task mode 为 AUTO、Interpreter 为 INTERP_READING,预览区显示刀具沿刀路移动,编辑器高亮当前执行行。</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="screenshots/19-program-paused.png" alt="程序暂停中">
|
||||
<figcaption>图 13 暂停:Interpreter 变为 INTERP_PAUSED,运动停止但位置保持,可继续、单步或停止。暂停期间 MDI 会被拒绝,这是 LinuxCNC 的既有边界。</figcaption>
|
||||
</figure>
|
||||
<div class="note">程序编辑器支持行号槽与当前行高亮,右侧"Active G-Codes"实时显示生效的模态指令,便于教学逐行讲解。</div>
|
||||
</section>
|
||||
|
||||
<section id="s10">
|
||||
<h2>10. 三维预览与机床管理</h2>
|
||||
|
||||
<h3>10.1 双视图预览</h3>
|
||||
<p>右侧 <b>Preview</b> 页签分成左右两个画布:左边是刀路与刀具仿真,右边是机床三维模型,两者都由同一份 WASM 关节状态驱动。工具栏的六个视图按钮(俯视、旋转俯视、侧视、正视、倒视、透视)、缩放和"拖动旋转"模式同时作用于两个视图。</p>
|
||||
<figure>
|
||||
<img src="screenshots/23-views-cleared.png" alt="切换视图并清除轨迹后的预览区">
|
||||
<figcaption>图 14 切换视图方向并清除实时轨迹后,只保留当前刀路;机床模型姿态与 WASM 关节一致。</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>10.2 三维机床管理</h3>
|
||||
<p>预览区右上角第一个按钮打开<b>三维机床管理</b>对话框,用于把真实机床的 GLB 模型绑定为可驱动的五轴机床。对话框分四个步骤页签:</p>
|
||||
<table>
|
||||
<thead><tr><th>页签</th><th>内容</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>部件与层级</td><td>GLB 内部结构树、搜索定位、节点自由度绑定(每个节点一个唯一自由度)、模型选择与删除、撤销/重做</td></tr>
|
||||
<tr><td>运动验证</td><td>跟随数控系统实时关节位置、按物理零点手动验证、指定轴的层级运动测试</td></tr>
|
||||
<tr><td>机床标定</td><td>C 轴原点、A/B 到 C 轴偏置、机床物理零点(直线 mm / 旋转 deg)</td></tr>
|
||||
<tr><td>文件与恢复</td><td>模型名称与设计说明、保存整机 GLB、本地机床库、13 个 LinuxCNC 案例模板</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<figure>
|
||||
<img src="screenshots/06-machine-manager-structure.png" alt="三维机床管理:部件与层级">
|
||||
<figcaption>图 15 "部件与层级":导入模型后在此展开层级树,选择节点并指定其自由度。</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="screenshots/07-machine-manager-motion.png" alt="三维机床管理:运动验证">
|
||||
<figcaption>图 16 "运动验证":可锁定跟随实时关节,也可按物理零点手动输入 X/Y/Z/A/B/C 检查模型运动是否正确;这些输入只驱动模型,不向控制器发送运动命令。</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="screenshots/08-machine-manager-calibration.png" alt="三维机床管理:机床标定">
|
||||
<figcaption>图 17 "机床标定":填写旋转中心、轴间偏置与物理零点,与 LinuxCNC 原生 TRT 运动学参数一一对应。</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="screenshots/09-machine-manager-files.png" alt="三维机床管理:文件与恢复">
|
||||
<figcaption>图 18 "文件与恢复":命名并保存整机 GLB,或从本浏览器机床库打开、再下载、删除;下方列出 13 个 LinuxCNC 案例模板。</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>10.3 导入机床模型</h3>
|
||||
<p>第二个按钮打开<b>三维机床模型</b>对话框,可以直接试用四套内置示例(AC、BC、AC 桁架、BC 桁架),或导入自己的 GLB 文件。</p>
|
||||
<figure>
|
||||
<img src="screenshots/05-machine-import-dialog.png" alt="导入机床模型对话框">
|
||||
<figcaption>图 19 内置示例可以一键载入或下载;单个文件上限 64 MiB,超限会给出明确提示且不影响当前模型。</figcaption>
|
||||
</figure>
|
||||
<div class="note">导入的模型只在浏览器内解析与显示。若模型带有 <code>JT_X / JT_Y / JT_Z / JT_A / JT_B / JT_C</code> 之类的语义节点名,管理器会自动给出五轴识别结果与运动学建议,方便快速完成绑定。</div>
|
||||
|
||||
<h3>10.4 机床几何参数</h3>
|
||||
<p>TRT 机型还提供<b>机床参数</b>对话框,用于编辑旋转中心与轴间偏置,并支持"应用参数""重新读取""保存 INI"。修改参数后需要重新回零,界面会给出提示。</p>
|
||||
<figure>
|
||||
<img src="screenshots/10-geometry-parameters.png" alt="机床几何参数对话框">
|
||||
<figcaption>图 20 旋转中心(X/Y/Z)与到 C 轴偏置(X/Y/Z)共六个参数直接对应 INI 中的运动学配置。</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section id="s11">
|
||||
<h2>11. 诊断信息</h2>
|
||||
<p><b>Diagnostics</b> 页签汇总运行环境与限制,排查问题时优先看这里:</p>
|
||||
<ul>
|
||||
<li><b>Startup resources</b>:当前案例入口文件、解析成功的资源数与阻塞数、HAL 记录数。</li>
|
||||
<li><b>Disabled operations</b>:列出当前被门控/未开放的操作及原因(如 <code>blocked_by_abi</code>)。</li>
|
||||
<li><b>Source diagnostics</b>:启动过程中缺失或未解析的资源路径与行号。</li>
|
||||
</ul>
|
||||
<figure>
|
||||
<img src="screenshots/20-diagnostics.png" alt="诊断页签">
|
||||
<figcaption>图 21 诊断页显示 tick 计数、状态哈希、启动资源清单与被禁用操作的逐条原因。</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section id="s12">
|
||||
<h2>12. 快捷键与鼠标操作</h2>
|
||||
<table>
|
||||
<thead><tr><th>按键 / 操作</th><th>功能</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><kbd>F1</kbd></td><td>急停 / 解除急停(切换)</td></tr>
|
||||
<tr><td><kbd>F2</kbd></td><td>上电 / 下电</td></tr>
|
||||
<tr><td><kbd>F3</kbd> / <kbd>F5</kbd></td><td>切到 手动控制 / MDI 页签</td></tr>
|
||||
<tr><td><kbd>R</kbd></td><td>运行程序</td></tr>
|
||||
<tr><td><kbd>P</kbd> / <kbd>S</kbd></td><td>暂停 / 继续</td></tr>
|
||||
<tr><td><kbd>T</kbd></td><td>单步执行下一行</td></tr>
|
||||
<tr><td><kbd>Esc</kbd></td><td>停止程序(对话框打开时先关闭对话框)</td></tr>
|
||||
<tr><td><kbd>Ctrl</kbd>+<kbd>R</kbd></td><td>重载当前程序</td></tr>
|
||||
<tr><td><kbd>Ctrl</kbd>+<kbd>K</kbd></td><td>清除实时轨迹</td></tr>
|
||||
<tr><td><kbd>Home</kbd> / <kbd>End</kbd></td><td>回零当前关节 / 对当前关节 Touch Off</td></tr>
|
||||
<tr><td><kbd>Ctrl</kbd>+<kbd>Home</kbd> / <kbd>Ctrl</kbd>+<kbd>End</kbd></td><td>全部回零 / 刀具 Touch Off</td></tr>
|
||||
<tr><td><kbd>B</kbd> / <kbd>b</kbd></td><td>主轴制动 / 松开制动</td></tr>
|
||||
<tr><td><kbd>c</kbd> / <kbd>i</kbd></td><td>Jog 切回连续 / 循环切换增量步长</td></tr>
|
||||
<tr><td>单击机床模型</td><td>选中该导入模型(黄色包围框)</td></tr>
|
||||
<tr><td>按住拖动预览区</td><td>旋转视角(开启"拖动旋转"模式)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>窄屏(手机、平板竖屏)下界面自动改为上下堆叠布局,不会出现横向滚动:</p>
|
||||
<figure>
|
||||
<img src="screenshots/21-mobile-390.png" alt="390 像素宽窄屏布局">
|
||||
<figcaption>图 22 390 px 视口下的完整页面布局(整页截图)。</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section id="s13">
|
||||
<h2>13. 支持的机床案例</h2>
|
||||
<p>"Simulation case"下拉框中共有 13 个案例。当前发布版本中只有两个 TRT 案例可以完成"上电 → 回零 → 运行"的完整操作链,其余 11 个通用运动学案例仍在适配中。</p>
|
||||
<table>
|
||||
<thead><tr><th>案例</th><th>坐标轴</th><th>运动学</th><th>状态</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>XYZAC TRT</td><td>X Y Z A C</td><td>xyzac-trt-kins</td><td><span class="pill yes">可用</span></td><td>A 轴绕 X 倾转、C 轴绕 Z 回转,支持 RTCP 与 M428/M429 切换</td></tr>
|
||||
<tr><td>XYZBC TRT</td><td>X Y Z B C</td><td>xyzbc-trt-kins</td><td><span class="pill yes">可用</span></td><td>B 轴绕 Y 倾转、C 轴绕 Z 回转,支持 RTCP</td></tr>
|
||||
<tr><td>xyzab-tdr</td><td>X Y Z A B</td><td>xyzab-tdr</td><td><span class="pill no">暂不可用</span></td><td>双旋转台;可在 Worker 层运行,浏览器侧尚未就绪</td></tr>
|
||||
<tr><td>bridge-mill-5axiskins</td><td>X Y Z B C W Y</td><td>5axiskins</td><td><span class="pill no">暂不可用</span></td><td>龙门式五轴,带 W 轴沿刀轴进给</td></tr>
|
||||
<tr><td>max5-maxkins</td><td>X Y Z B C</td><td>maxkins</td><td><span class="pill no">暂不可用</span></td><td>MAX5 结构</td></tr>
|
||||
<tr><td>hexapod-genhexkins</td><td>X Y Z A B C</td><td>genhexkins</td><td><span class="pill no">暂不可用</span></td><td>六自由度并联平台</td></tr>
|
||||
<tr><td>pentapod-pentakins</td><td>X Y Z A B</td><td>pentakins</td><td><span class="pill no">暂不可用</span></td><td>五杆并联结构</td></tr>
|
||||
<tr><td>puma-pumakins</td><td>X Y Z A B C</td><td>pumakins</td><td><span class="pill no">暂不可用</span></td><td>串联关节机器人</td></tr>
|
||||
<tr><td>puma-genserkins</td><td>X Y Z A B C</td><td>genserkins</td><td><span class="pill no">暂不可用</span></td><td>串联机器人(通用求解器)</td></tr>
|
||||
<tr><td>trivkins-xyzab</td><td>X Y Z A B</td><td>trivkins</td><td><span class="pill no">暂不可用</span></td><td>直角坐标一一映射基线,无 RTCP</td></tr>
|
||||
<tr><td>trivkins-xyzcw</td><td>X Y Z C W</td><td>trivkins</td><td><span class="pill no">暂不可用</span></td><td>车铣复合映射基线</td></tr>
|
||||
<tr><td>trivkins-xyzya</td><td>X Y Z Y A</td><td>trivkins</td><td><span class="pill no">暂不可用</span></td><td>双 Y 轴映射基线</td></tr>
|
||||
<tr><td>scorbot-kins</td><td>X Y Z A B</td><td>scorbot-kins</td><td><span class="pill no">暂不可用</span></td><td>SCORBOT 机械臂</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="note">"暂不可用"表示可以选中案例、能读出程序名,但控制器停在 PENDING、上电按钮保持禁用。当前请仅使用前两个 TRT 案例;不小心选中其它案例后刷新页面即可恢复(详见 14.2)。</div>
|
||||
</section>
|
||||
|
||||
<section id="s14">
|
||||
<h2>14. 已知问题与规避</h2>
|
||||
<p>以下是本次上线功能巡检(2026-10-08)中发现并已复现的问题,均已给出规避办法。工具栏"载入精确案例"按钮曾经失效,原因是点击事件对象被当成案例路径传入,该问题已修复并重新发布,本次巡检中该按钮已能正常重新载入当前案例。</p>
|
||||
|
||||
<h3>14.1 选中通用运动学案例后控制器卡住</h3>
|
||||
<table>
|
||||
<thead><tr><th>项目</th><th>内容</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>现象</td><td>在案例下拉框中选中 13 个案例里除 XYZAC/XYZBC TRT 之外任意一个(如 xyzab-tdr、max5-maxkins),状态栏所有字段停留在 <code>PENDING</code>,上电按钮提示 <code>linuxcnc_wasm_snapshot_pending</code>,此时下拉框也无法再切回其它案例</td></tr>
|
||||
<tr><td>原因</td><td>通用运动学案例在浏览器侧的启动链尚未完成(MGR-27 进行中):案例引用的运动学源文件未随前端产物发布,请求返回 404,且会话启动失败后前端没有回滚</td></tr>
|
||||
<tr><td>规避</td><td>当前只使用 XYZAC TRT / XYZBC TRT;若误选其它案例,<b>刷新浏览器页面</b>即可恢复</td></tr>
|
||||
<tr><td>影响面</td><td>13 个案例中的 11 个;两个 TRT 案例的功能不受影响</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="s15">
|
||||
<h2>15. 常见问题</h2>
|
||||
|
||||
<h4>页面一直显示 PENDING,上电按钮点不了?</h4>
|
||||
<p>多数情况是仿真会话尚未就绪,等待数秒即可;若超过 1 分钟仍不变,或曾在案例下拉框里选过通用运动学案例,请刷新页面。</p>
|
||||
|
||||
<h4>Jog、MDI、主轴按钮都是灰的?</h4>
|
||||
<p>按"急停复位 → 上电"的顺序操作,并确认状态栏 <code>Task state</code> 为 <code>STATE_ON</code>、<code>Machine sequence</code> 为 <code>HOMED</code>。把鼠标停在灰按钮上会显示具体原因。</p>
|
||||
|
||||
<h4>为什么 MDI 在程序暂停时无法执行?</h4>
|
||||
<p>这是 LinuxCNC 的既有语义:程序运行/暂停期间 MDI 会被拒绝(<code>mdi_while_program_active</code>)。停止或让程序结束后再使用 MDI。</p>
|
||||
|
||||
<h4>加载 G 代码时提示机型不匹配?</h4>
|
||||
<p>程序中的轴字与当前机型不一致。请先用案例下拉框切到对应机型(AC 或 BC),或使用 RTCP 程序库自动按机型过滤后的程序。</p>
|
||||
|
||||
<h4>浏览器提示证书不受信任?</h4>
|
||||
<p>站点使用 IP 地址的 HTTPS 自签证书。确认地址无误后选择"高级 / 继续访问";如需长期使用,可让管理员把证书加入受信任列表。</p>
|
||||
|
||||
<h4>导入的机床模型会丢失吗?</h4>
|
||||
<p>页面刷新后导入的模型需要重新导入;使用"三维机床管理 → 文件与恢复"保存到浏览器机床库的机床,可以在同一浏览器、同一地址下重新打开。清理浏览器站点数据会同时删除机床库记录。</p>
|
||||
|
||||
<h4>能连接真实机床吗?</h4>
|
||||
<p>不能,也不在本项目范围内。本平台是纯仿真环境,硬件驱动保持在浏览器虚拟设备边界。</p>
|
||||
</section>
|
||||
|
||||
<section id="s16">
|
||||
<h2>16. 附录:功能测试结论</h2>
|
||||
<p>本次发布软件的自动化功能巡检于 2026-10-08 在 <code>https://49.232.250.35:58010/</code> 上用真实 Chrome 浏览器执行,覆盖 26 个功能步骤,全部通过;另有 1 项待完成项(通用运动学案例的浏览器运行链,见第 14 节)。</p>
|
||||
<table>
|
||||
<thead><tr><th>范围</th><th>覆盖内容</th><th>结果</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>启动与状态</td><td>页面加载、WASM 会话就绪、状态栏、轴位与实时参数来源</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>菜单</td><td>Help → About、File 菜单宿主边界项置灰</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>程序资源</td><td>RTCP 程序库按机型过滤、加载到控制器、打开本地 G 代码、重载</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>机床模型</td><td>三维机床管理四个页签、导入机床模型、机床几何参数</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>案例切换</td><td>XYZAC ↔ XYZBC 切换及轴组同步</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>机床操作</td><td>急停复位、上电、HOME ALL、连续与增量 Jog</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>辅助功能</td><td>主轴启停、冷却液、Tool/Probe 状态</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>MDI 与 DRO</td><td>MDI 执行与历史、DRO 三种显示切换</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>倍率与图层</td><td>进给倍率调整、预览图层开关</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>程序运行</td><td>载入、运行、暂停、继续、停止、工具栏"载入精确案例"重新载入</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>视图与诊断</td><td>视图切换、清除轨迹、诊断页、快捷键、窄屏布局</td><td><span class="pill yes">通过</span></td></tr>
|
||||
<tr><td>案例矩阵</td><td>13 个案例逐一探测</td><td><span class="pill part">2 可用 / 11 待完成</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>测试脚本与原始报告:<code>docs/user-manual/capture_deployed_manual.mjs</code>、<code>docs/user-manual/deployment-test.report.json</code>;截图位于 <code>docs/user-manual/screenshots/</code>。重新执行:</p>
|
||||
<span class="mono">PLAYWRIGHT_MODULE=<playwright> node docs/user-manual/capture_deployed_manual.mjs</span>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p><b>LinuxCNC Web/WASM 使用说明书</b> 版本 2026-10-08 适用站点:https://49.232.250.35:58010/</p>
|
||||
<p>软件基于 LinuxCNC 2.10.0~pre0 源码移植,仿真在浏览器 WebAssembly 中运行,不连接真实机床。本手册可与 <code>docs/glb-machine-manager/00-三维机床管理详细功能文档.html</code> 配合阅读。</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user