feat: sync latest run execution updates
585
qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs
Normal file
@@ -0,0 +1,585 @@
|
||||
import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { PNG } from "pngjs";
|
||||
import {
|
||||
AlignmentType,
|
||||
Document,
|
||||
HeadingLevel,
|
||||
ImageRun,
|
||||
Packer,
|
||||
Paragraph,
|
||||
Table,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TextRun,
|
||||
WidthType,
|
||||
} from "docx";
|
||||
|
||||
const REPO_ROOT = path.resolve("/home/meswork/cnc_wams");
|
||||
const QA_ROOT = path.join(REPO_ROOT, "qa/web-rtcp-5axis-site-test");
|
||||
const OUTPUT_DIR = path.join(QA_ROOT, "output");
|
||||
const SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "test-linuxcnc-source-run");
|
||||
const TEST_SOURCE_DIR = path.join(REPO_ROOT, "web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source");
|
||||
const TEST_INI_PATH = path.join(TEST_SOURCE_DIR, "xyzac-trt.ini");
|
||||
const TEST_GCODE_PATH = path.join(TEST_SOURCE_DIR, "impeller-7bl-xyzac.ngc");
|
||||
const VENDORED_GCODE_REL = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc";
|
||||
const CHROME_PATH = process.env.CHROME_PATH || "/usr/bin/google-chrome";
|
||||
const FIXTURE_URL = "/web-rtcp-5axis-sim-plan/app/index.html";
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
const iniText = await fs.readFile(TEST_INI_PATH, "utf8");
|
||||
const gcodeText = await fs.readFile(TEST_GCODE_PATH, "utf8");
|
||||
const sourceEvidence = {
|
||||
iniPath: TEST_INI_PATH,
|
||||
iniSha256: sha256(iniText),
|
||||
gcodePath: TEST_GCODE_PATH,
|
||||
gcodeSha256: sha256(gcodeText),
|
||||
gcodeLineCount: gcodeText.split(/\r?\n/).length,
|
||||
gcodeBytes: Buffer.byteLength(gcodeText),
|
||||
};
|
||||
|
||||
const server = createStaticServer(REPO_ROOT);
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("failed to start static server");
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: CHROME_PATH,
|
||||
defaultViewport: { width: 1600, height: 1200, deviceScaleFactor: 1 },
|
||||
args: [
|
||||
"--disable-gpu",
|
||||
"--enable-webgl",
|
||||
"--use-angle=swiftshader",
|
||||
"--enable-unsafe-swiftshader",
|
||||
"--no-sandbox",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
const consoleErrors = [];
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") consoleErrors.push(msg.text());
|
||||
});
|
||||
page.on("pageerror", (error) => consoleErrors.push(error.message));
|
||||
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: `${baseUrl}${FIXTURE_URL}`,
|
||||
chromePath: CHROME_PATH,
|
||||
sourceEvidence,
|
||||
screenshots: {},
|
||||
steps: [],
|
||||
checks: [],
|
||||
samples: [],
|
||||
consoleErrors,
|
||||
};
|
||||
|
||||
try {
|
||||
await page.goto(`${baseUrl}${FIXTURE_URL}`, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
|
||||
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 });
|
||||
await waitForState((state) => state.kinematicsRuntimeReadiness?.loaded === true, 20000, "kinematics runtime ready");
|
||||
await waitForCanvasReady();
|
||||
await wait(800);
|
||||
await captureStep("01-initial-ui", "初始界面", "确认浏览器应用、五轴预览区、G-code 区和 DRO 面板已渲染。");
|
||||
|
||||
await page.evaluate(async ({ ini }) => {
|
||||
await window.webRtcp5AxisSimulation.stageMachineFiles({ iniText: ini, storageMode: "memory" });
|
||||
}, { ini: iniText });
|
||||
await waitForState((state) => state.machineFileStaging?.status === "staged", 20000, "test INI staged");
|
||||
await captureStep("02-stage-test-ini", "Stage test_linuxcnc_source INI", "使用 working_run/test_linuxcnc_source/xyzac-trt.ini 重新 staging machine files。");
|
||||
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', VENDORED_GCODE_REL);
|
||||
await waitForState((state) => (
|
||||
state.activeProgram?.endsWith("impeller-7bl-xyzac.ngc") &&
|
||||
state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" &&
|
||||
state.taskHalSession?.programPath?.endsWith("impeller-7bl-xyzac.ngc")
|
||||
), 25000, "impeller program loaded");
|
||||
await waitForCanvasReady();
|
||||
await wait(1200);
|
||||
await captureStep("03-toolpath-preview", "刀具路径预览", "加载 test_linuxcnc_source/impeller-7bl-xyzac.ngc 对应程序,确认 canonical motion 预览路径、TCP 球和刀轴线可见。");
|
||||
|
||||
await setMachineReady();
|
||||
await captureStep("04-ready-before-run", "RUN 前准备", "POWER ON、HOME、AUTO、TCP 模式就绪,RUN gate 所需条件已满足。");
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="RUN"]').click());
|
||||
await waitForState((state) => (
|
||||
["running", "complete"].includes(state.runState) &&
|
||||
state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm"
|
||||
), 15000, "RUN feedback started");
|
||||
|
||||
const startedAt = Date.now();
|
||||
for (const [elapsedMs, title] of [
|
||||
[200, "G-code 执行 200ms"],
|
||||
[500, "G-code 执行 500ms"],
|
||||
[1000, "G-code 执行 1000ms"],
|
||||
[2000, "G-code 执行 2000ms"],
|
||||
[5000, "G-code 执行 5000ms"],
|
||||
[17000, "G-code 执行 17000ms"],
|
||||
]) {
|
||||
const delay = Math.max(startedAt + elapsedMs - Date.now(), 0);
|
||||
if (delay > 0) await wait(delay);
|
||||
await waitForCanvasReady();
|
||||
await captureRunSample(elapsedMs, `05-run-${String(elapsedMs).padStart(4, "0")}ms`, title);
|
||||
}
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="STOP"]').click());
|
||||
await waitForState((state) => state.taskHalStatusLoop?.active === false, 10000, "RUN stopped");
|
||||
await captureStep("06-stop-after-run", "STOP 后状态", "停止 RUN,确认 status loop 停止且执行证据已保留。");
|
||||
|
||||
addChecks();
|
||||
report.status = report.checks.every((check) => check.pass) ? "PASS" : "FAIL";
|
||||
|
||||
const jsonPath = path.join(OUTPUT_DIR, "test-linuxcnc-source-run-report.json");
|
||||
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
report.jsonPath = jsonPath;
|
||||
|
||||
const docxPath = await writeDocxReport(report);
|
||||
report.docxPath = docxPath;
|
||||
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
console.log(`test_linuxcnc_source_run_status=${report.status}`);
|
||||
console.log(`test_linuxcnc_source_run_json=${jsonPath}`);
|
||||
console.log(`test_linuxcnc_source_run_docx=${docxPath}`);
|
||||
} finally {
|
||||
await page.close().catch(() => {});
|
||||
await browser.close().catch(() => {});
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
|
||||
async function setMachineReady() {
|
||||
await page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
if (state.machine?.taskState !== "on") document.querySelector('[data-action="power"]').click();
|
||||
});
|
||||
await waitForState((state) => state.machine.taskState === "on", 10000, "machine power on");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
|
||||
await waitForState((state) => state.machine.mode === "manual", 10000, "manual mode");
|
||||
await page.evaluate(() => document.querySelector('[data-action="HOME"]').click());
|
||||
await waitForState((state) => state.machine.allHomed === true, 10000, "homed");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-auto"]').click());
|
||||
await waitForState((state) => state.machine.mode === "auto", 10000, "auto mode");
|
||||
await page.evaluate(() => document.querySelector('[data-action="kins-tcp"]').click());
|
||||
await waitForState((state) => state.rtcpState === "on" && state.kinsType === "tcp-xyzac", 10000, "tcp mode");
|
||||
}
|
||||
|
||||
async function captureRunSample(elapsedMs, name, title) {
|
||||
const step = await captureStep(name, title, "采集 G-code 当前高亮行、执行轨迹、实时 DRO/axisPose 和 task/HAL feedback。");
|
||||
const state = step.state;
|
||||
const sample = {
|
||||
elapsedMs,
|
||||
screenshotPath: step.screenshotPath,
|
||||
activeLine: state.activeLine,
|
||||
activeUiLine: state.activeUiLine,
|
||||
activeLineMatchesUi: state.activeLine === state.activeUiLine,
|
||||
runState: state.runState,
|
||||
axisPose: state.axisPose,
|
||||
dro: state.dro,
|
||||
droMatchesAxisPose: axesMatch(state.dro, state.programRuntimeFeedback?.axisPose),
|
||||
velocity: state.programRuntimeFeedback?.currentVelocityMmPerMin ?? null,
|
||||
expectedVelocity: state.currentTimingSegment?.velocityMmPerMin ?? null,
|
||||
feedRate: state.currentTimingSegment?.feedRate ?? null,
|
||||
feedMode: state.currentTimingSegment?.feedMode ?? null,
|
||||
segmentDurationSeconds: state.currentTimingSegment?.durationSeconds ?? null,
|
||||
distanceToGo: state.programRuntimeFeedback?.distanceToGo ?? null,
|
||||
feedbackSource: state.programRuntimeFeedback?.sourceMode ?? null,
|
||||
taskCycle: state.programRuntimeFeedback?.taskCycle ?? null,
|
||||
servoCycle: state.programRuntimeFeedback?.cycle ?? null,
|
||||
historyLength: state.programRuntimeFeedbackHistory?.length || 0,
|
||||
executedPathPoints: Number(step.dataset.threeExecutedPathPoints || 0),
|
||||
currentSegmentHighlight: step.dataset.threeCurrentSegmentHighlight,
|
||||
};
|
||||
report.samples.push(sample);
|
||||
return sample;
|
||||
}
|
||||
|
||||
async function captureStep(name, title, description) {
|
||||
const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
const dataset = await getCanvasDataset();
|
||||
const state = await getStateWithUi();
|
||||
const pixelStats = await analyzePng(screenshotPath);
|
||||
const step = {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
screenshotPath,
|
||||
dataset,
|
||||
pixelStats,
|
||||
state: summarizeState(state),
|
||||
};
|
||||
report.screenshots[name] = screenshotPath;
|
||||
report.steps.push(step);
|
||||
return step;
|
||||
}
|
||||
|
||||
function addChecks() {
|
||||
const finalState = report.steps.at(-1)?.state || {};
|
||||
const previewStep = report.steps.find((step) => step.name === "03-toolpath-preview");
|
||||
const runSamples = report.samples;
|
||||
const movingVelocities = runSamples
|
||||
.map((sample) => Number(sample.velocity || 0))
|
||||
.filter((velocity) => velocity > 0);
|
||||
const distinctVelocities = new Set(movingVelocities.map((velocity) => Math.round(velocity * 1000) / 1000));
|
||||
const sourceState = previewStep?.state || {};
|
||||
report.checks.push(
|
||||
check("test_linuxcnc_source INI used", sourceState.iniPath?.endsWith("xyzac-trt.ini"), sourceState.iniPath || "-"),
|
||||
check("test_linuxcnc_source G-code hash matches loaded source", sourceEvidence.gcodeSha256 === "e90f0b4b6c43809da94a8170ee1029b5afc3e1bbe9bf2ae66298a8baefad013c", sourceEvidence.gcodeSha256),
|
||||
check("刀具路径预览点可见", Number(previewStep?.dataset?.threePathPoints || 0) > 100, `pathPoints=${previewStep?.dataset?.threePathPoints}`),
|
||||
check("刀具路径使用完整 canonical motion 点", Number(previewStep?.dataset?.threePathPoints || 0) === Number(sourceState.programSummary?.motionEventCount || 0), `threePathPoints=${previewStep?.dataset?.threePathPoints}, motionEventCount=${sourceState.programSummary?.motionEventCount}`),
|
||||
check("TCP/刀轴线可见", previewStep?.dataset?.threeTcpMarker === "sphere" && previewStep?.dataset?.threeToolAxisMarker === "line", `tcp=${previewStep?.dataset?.threeTcpMarker}, axis=${previewStep?.dataset?.threeToolAxisMarker}`),
|
||||
check("RUN 采样数量", runSamples.length >= 5, `samples=${runSamples.length}`),
|
||||
check("每行执行高亮同步", runSamples.every((sample) => sample.activeLineMatchesUi), runSamples.map((sample) => `${sample.activeLine}/${sample.activeUiLine}`).join(", ")),
|
||||
check("执行轨迹可见", runSamples.every((sample) => sample.executedPathPoints >= 1 && sample.currentSegmentHighlight === "ok"), runSamples.map((sample) => `${sample.executedPathPoints}/${sample.currentSegmentHighlight}`).join(", ")),
|
||||
check("实时轴值来自 task/HAL feedback", runSamples.every((sample) => sample.feedbackSource === "linuxcnc-task-motion-hal-wasm" && sample.droMatchesAxisPose), runSamples.map((sample) => `${sample.feedbackSource}/${sample.droMatchesAxisPose}`).join(", ")),
|
||||
check("RUN feed 不是固定 3600 mm/min", movingVelocities.length > 0 && movingVelocities.every((velocity) => Math.abs(velocity - 3600) > 0.001), runSamples.map((sample) => `${sample.elapsedMs}ms=${sample.velocity}`).join(", ")),
|
||||
check("RUN feed 随实际 G-code F/G93 段变化", distinctVelocities.size >= 3, [...distinctVelocities].join(", ")),
|
||||
check("RUN 使用 G93 inverse-time feed", runSamples.some((sample) => sample.feedMode === "inverse-time" && Number(sample.segmentDurationSeconds || 0) > 0), runSamples.map((sample) => `${sample.elapsedMs}ms F${sample.feedRate} ${sample.feedMode} ${sample.segmentDurationSeconds}s`).join(", ")),
|
||||
check("task/HAL cycle 推进", runSamples.some((sample) => Number(sample.taskCycle || 0) > 0 && Number(sample.servoCycle || 0) > 0), runSamples.map((sample) => `${sample.taskCycle}/${sample.servoCycle}`).join(", ")),
|
||||
check("STOP 后 loop 停止", finalState.taskHalStatusLoop?.active === false, `active=${finalState.taskHalStatusLoop?.active}, stopReason=${finalState.taskHalStatusLoop?.stopReason}`),
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeState(state) {
|
||||
const currentTimingSegment = currentTimingSegmentForState(state);
|
||||
return {
|
||||
activeProgram: state.activeProgram,
|
||||
programSource: state.programSource,
|
||||
programExecutionSourceMode: state.programExecutionSourceMode,
|
||||
programSummary: state.programExecution?.summary || null,
|
||||
machineProfile: state.machineProfile,
|
||||
iniPath: state.iniConfigReadiness?.path || state.linuxCncIniConfig?.path || null,
|
||||
selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel || null,
|
||||
taskHalProgramPath: state.taskHalSession?.programPath || null,
|
||||
runState: state.runState,
|
||||
activeLine: state.activeLine,
|
||||
activeUiLine: state.__activeUiLine,
|
||||
machine: state.machine,
|
||||
rtcpState: state.rtcpState,
|
||||
kinsType: state.kinsType,
|
||||
dro: pickAxes(state.dro),
|
||||
axisPose: pickAxes(state.axisPose),
|
||||
programRuntimeFeedback: state.programRuntimeFeedback,
|
||||
currentTimingSegment,
|
||||
feedbackHistoryLength: state.programRuntimeFeedbackHistory?.length || 0,
|
||||
taskHalStatusLoop: state.taskHalStatusLoop,
|
||||
taskHalStatus: state.taskHalStatus ? {
|
||||
taskState: state.taskHalStatus.ui?.taskState || state.taskHalStatus.task?.state || null,
|
||||
taskMode: state.taskHalStatus.ui?.taskMode || state.taskHalStatus.task?.mode || null,
|
||||
interpState: state.taskHalStatus.ui?.interpState || state.taskHalStatus.task?.interpState || null,
|
||||
activeLine: state.taskHalStatus.ui?.activeLine || null,
|
||||
taskCycle: state.taskHalStatus.ui?.taskCycle || null,
|
||||
servoCycle: state.taskHalStatus.ui?.servoCycle || null,
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
|
||||
function currentTimingSegmentForState(state) {
|
||||
const activeLine = Number(state.activeLine || 0);
|
||||
const segments = state.programExecutionTiming?.segments || [];
|
||||
if (!Number.isFinite(activeLine) || !Array.isArray(segments) || segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return segments.find((segment) => Number(segment.line) === activeLine)
|
||||
|| [...segments].reverse().find((segment) => Number(segment.line) <= activeLine)
|
||||
|| null;
|
||||
}
|
||||
|
||||
async function wait(ms) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitForState(predicate, timeoutMs, label) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const state = await getStateWithUi();
|
||||
if (predicate(state)) return state;
|
||||
await wait(100);
|
||||
}
|
||||
throw new Error(`timeout waiting for ${label}`);
|
||||
}
|
||||
|
||||
async function waitForCanvasReady(timeoutMs = 20000) {
|
||||
await page.waitForFunction(() => {
|
||||
const canvas = document.querySelector("[data-five-axis-canvas]");
|
||||
return canvas?.dataset?.threeReady === "true"
|
||||
&& canvas?.dataset?.threePreviewScope === "machine-reference-and-toolpath";
|
||||
}, { timeout: timeoutMs });
|
||||
}
|
||||
|
||||
async function getStateWithUi() {
|
||||
return page.evaluate(() => {
|
||||
const state = JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState()));
|
||||
state.__activeUiLine = Number(document.querySelector(".gcode-row.active")?.dataset.programLine || 0);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
async function getCanvasDataset() {
|
||||
return page.$eval("[data-five-axis-canvas]", (canvas) => ({ ...canvas.dataset }));
|
||||
}
|
||||
|
||||
function createStaticServer(rootDir) {
|
||||
return http.createServer(async (request, response) => {
|
||||
try {
|
||||
const requestPath = decodeURIComponent(new URL(request.url || "/", "http://127.0.0.1").pathname);
|
||||
const relativePath = requestPath === "/" ? "/index.html" : requestPath;
|
||||
const targetPath = path.resolve(rootDir, `.${relativePath}`);
|
||||
if (!targetPath.startsWith(rootDir)) {
|
||||
response.writeHead(403);
|
||||
response.end("forbidden");
|
||||
return;
|
||||
}
|
||||
let filePath = targetPath;
|
||||
let stat = await fs.stat(filePath).catch(() => null);
|
||||
if (stat?.isDirectory()) {
|
||||
filePath = path.join(filePath, "index.html");
|
||||
stat = await fs.stat(filePath).catch(() => null);
|
||||
}
|
||||
if (!stat?.isFile()) {
|
||||
response.writeHead(404);
|
||||
response.end("not found");
|
||||
return;
|
||||
}
|
||||
const body = await fs.readFile(filePath);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": contentTypeFor(filePath),
|
||||
"Content-Length": String(body.byteLength),
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
response.end(body);
|
||||
} catch (error) {
|
||||
response.writeHead(500);
|
||||
response.end(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function contentTypeFor(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return {
|
||||
".css": "text/css; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".wasm": "application/wasm",
|
||||
".xml": "application/xml; charset=utf-8",
|
||||
}[ext] || "application/octet-stream";
|
||||
}
|
||||
|
||||
async function analyzePng(filePath) {
|
||||
const png = PNG.sync.read(await fs.readFile(filePath));
|
||||
let luminanceSum = 0;
|
||||
let nonBlack = 0;
|
||||
for (let index = 0; index < png.data.length; index += 4) {
|
||||
const luminance = png.data[index] * 0.2126 + png.data[index + 1] * 0.7152 + png.data[index + 2] * 0.0722;
|
||||
luminanceSum += luminance;
|
||||
if (luminance > 8) nonBlack += 1;
|
||||
}
|
||||
const total = png.width * png.height;
|
||||
return {
|
||||
width: png.width,
|
||||
height: png.height,
|
||||
averageLuminance: Number((luminanceSum / total).toFixed(2)),
|
||||
nonBlackRatio: Number((nonBlack / total).toFixed(4)),
|
||||
};
|
||||
}
|
||||
|
||||
async function writeDocxReport(data) {
|
||||
const generated = new Date(data.generatedAt);
|
||||
const ymd = generated.toISOString().slice(0, 10);
|
||||
const docxPath = path.join(OUTPUT_DIR, `test-linuxcnc-source-run-report-${ymd}.docx`);
|
||||
const children = [
|
||||
new Paragraph({
|
||||
text: "test_linuxcnc_source RUN 功能测试报告",
|
||||
heading: HeadingLevel.TITLE,
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
centered(`测试对象:${data.targetUrl}`),
|
||||
centered(`生成时间:${formatDateTime(generated)}`),
|
||||
blank(),
|
||||
heading("1. 测试结论"),
|
||||
para(`本次使用 working_run/test_linuxcnc_source 中的 INI 与 G-code 文件执行 RUN 测试,结果:${data.status}。`),
|
||||
checksTable(data.checks),
|
||||
heading("2. 测试源文件"),
|
||||
kvTable([
|
||||
["INI", data.sourceEvidence.iniPath],
|
||||
["INI SHA-256", data.sourceEvidence.iniSha256],
|
||||
["G-code", data.sourceEvidence.gcodePath],
|
||||
["G-code SHA-256", data.sourceEvidence.gcodeSha256],
|
||||
["G-code 行数/字节", `${data.sourceEvidence.gcodeLineCount} / ${data.sourceEvidence.gcodeBytes}`],
|
||||
]),
|
||||
heading("3. 测试过程"),
|
||||
];
|
||||
|
||||
for (const step of data.steps) {
|
||||
children.push(...(await stepBlock(step)));
|
||||
}
|
||||
|
||||
children.push(
|
||||
heading("4. RUN 采样明细"),
|
||||
sampleTable(data.samples),
|
||||
heading("5. 原始证据"),
|
||||
kvTable([
|
||||
["JSON", data.jsonPath || path.join(OUTPUT_DIR, "test-linuxcnc-source-run-report.json")],
|
||||
["截图目录", SCREENSHOT_DIR],
|
||||
["Word", docxPath],
|
||||
["Console error", String(data.consoleErrors.length)],
|
||||
["Chrome", data.chromePath],
|
||||
]),
|
||||
);
|
||||
|
||||
if (data.consoleErrors.length > 0) {
|
||||
children.push(heading("6. Console Error"), ...data.consoleErrors.map((entry) => para(entry)));
|
||||
}
|
||||
|
||||
const doc = new Document({ sections: [{ properties: {}, children }] });
|
||||
await fs.writeFile(docxPath, await Packer.toBuffer(doc));
|
||||
return docxPath;
|
||||
}
|
||||
|
||||
async function stepBlock(step) {
|
||||
const image = await fs.readFile(step.screenshotPath);
|
||||
return [
|
||||
new Paragraph({
|
||||
text: `${step.name} - ${step.title}`,
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
spacing: { before: 180, after: 120 },
|
||||
}),
|
||||
para(step.description),
|
||||
kvTable([
|
||||
["activeProgram", step.state.activeProgram],
|
||||
["runState / activeLine", `${step.state.runState} / ${step.state.activeLine}`],
|
||||
["program source", `${step.state.programSource} / ${step.state.programExecutionSourceMode}`],
|
||||
["selected G-code", step.state.selectedGcodeSourceRel],
|
||||
["task/HAL program", step.state.taskHalProgramPath],
|
||||
["DRO XYZAC", axesText(step.state.dro)],
|
||||
["axisPose XYZAC", axesText(step.state.axisPose)],
|
||||
["path/executed", `${step.dataset.threePathPoints || 0} / ${step.dataset.threeExecutedPathPoints || 0}`],
|
||||
["RTCP / kins", `${step.state.rtcpState} / ${step.state.kinsType}`],
|
||||
["pixel", `luma=${step.pixelStats.averageLuminance}, nonBlack=${step.pixelStats.nonBlackRatio}`],
|
||||
]),
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new ImageRun({
|
||||
data: image,
|
||||
type: "png",
|
||||
transformation: { width: 520, height: 390 },
|
||||
})],
|
||||
}),
|
||||
para(`截图文件:${step.screenshotPath}`),
|
||||
];
|
||||
}
|
||||
|
||||
function sampleTable(samples) {
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: ["时间", "行号", "状态", "DRO XYZAC", "速度/feed", "轨迹点", "反馈"].map((text) => cell(text, true)),
|
||||
}),
|
||||
...samples.map((sample) => new TableRow({
|
||||
children: [
|
||||
cell(`${sample.elapsedMs}ms`),
|
||||
cell(`${sample.activeLine} / UI ${sample.activeUiLine}`),
|
||||
cell(sample.runState),
|
||||
cell(axesText(sample.dro)),
|
||||
cell(`${sample.velocity ?? "-"} mm/min\nF${sample.feedRate ?? "-"} ${sample.feedMode ?? "-"}`),
|
||||
cell(String(sample.executedPathPoints)),
|
||||
cell(`${sample.feedbackSource}\ncycle=${sample.taskCycle}/${sample.servoCycle}`),
|
||||
],
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function checksTable(checks) {
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({ children: [cell("检查项", true), cell("结果", true), cell("证据", true)] }),
|
||||
...checks.map((item) => new TableRow({
|
||||
children: [cell(item.name), cell(item.pass ? "PASS" : "FAIL"), cell(item.detail)],
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function kvTable(rows) {
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({ children: [cell("项目", true), cell("内容", true)] }),
|
||||
...rows.map(([key, value]) => new TableRow({ children: [cell(key), cell(value)] })),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function centered(text) {
|
||||
return new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun(String(text))] });
|
||||
}
|
||||
|
||||
function heading(text) {
|
||||
return new Paragraph({ text, heading: HeadingLevel.HEADING_1, spacing: { before: 240, after: 120 } });
|
||||
}
|
||||
|
||||
function blank() {
|
||||
return new Paragraph({ text: "" });
|
||||
}
|
||||
|
||||
function para(text) {
|
||||
return new Paragraph({ children: [new TextRun(String(text ?? "-"))], spacing: { after: 100 } });
|
||||
}
|
||||
|
||||
function cell(text, bold = false) {
|
||||
return new TableCell({
|
||||
width: { size: 25, type: WidthType.PERCENTAGE },
|
||||
children: String(text ?? "-").split("\n").map((line) => new Paragraph({
|
||||
children: [new TextRun({ text: line, bold })],
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
function check(name, pass, detail) {
|
||||
return { name, pass: Boolean(pass), detail: String(detail ?? "-") };
|
||||
}
|
||||
|
||||
function pickAxes(value = {}) {
|
||||
return {
|
||||
x: Number(value.x || 0),
|
||||
y: Number(value.y || 0),
|
||||
z: Number(value.z || 0),
|
||||
a: Number(value.a || 0),
|
||||
b: Number(value.b || 0),
|
||||
c: Number(value.c || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function axesMatch(left = {}, right = {}) {
|
||||
return ["x", "y", "z", "a", "b", "c"].every((axis) => Math.abs(Number(left?.[axis] || 0) - Number(right?.[axis] || 0)) < 1e-9);
|
||||
}
|
||||
|
||||
function axesText(value = {}) {
|
||||
const axes = pickAxes(value);
|
||||
return `X=${fmt(axes.x)} Y=${fmt(axes.y)} Z=${fmt(axes.z)} A=${fmt(axes.a)} B=${fmt(axes.b)} C=${fmt(axes.c)}`;
|
||||
}
|
||||
|
||||
function fmt(value) {
|
||||
return Number(value || 0).toFixed(3);
|
||||
}
|
||||
|
||||
function sha256(text) {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
function formatDateTime(date) {
|
||||
return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
|
||||
}
|
||||
@@ -8,11 +8,15 @@ const REPO_ROOT = path.resolve("/home/meswork/cnc_wams");
|
||||
const QA_ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
|
||||
const OUTPUT_DIR = path.join(QA_ROOT, "output");
|
||||
const SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "toolpath-preview-cases");
|
||||
const RUN_FEEDBACK_SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "run-preconditions-feedback");
|
||||
const METER_SCENE_SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "meter-scene-evidence");
|
||||
const CHROME_PATH = process.env.CHROME_PATH || "/usr/bin/google-chrome";
|
||||
const FIXTURE_URL = "/web-rtcp-5axis-sim-plan/app/index.html";
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
await fs.mkdir(RUN_FEEDBACK_SCREENSHOT_DIR, { recursive: true });
|
||||
await fs.mkdir(METER_SCENE_SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
const MIME_TYPES = {
|
||||
".css": "text/css; charset=utf-8",
|
||||
@@ -144,6 +148,22 @@ async function captureCase(name) {
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function captureRunFeedbackFrame(name) {
|
||||
const screenshotPath = path.join(RUN_FEEDBACK_SCREENSHOT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function captureMeterSceneFrame(name) {
|
||||
const screenshotPath = path.join(METER_SCENE_SCREENSHOT_DIR, `${name}.png`);
|
||||
const canvas = await page.$("[data-five-axis-canvas]");
|
||||
if (!canvas) {
|
||||
throw new Error(`missing canvas for ${name}`);
|
||||
}
|
||||
await canvas.screenshot({ path: screenshotPath });
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function recordCase(name, summary, checks = []) {
|
||||
const dataset = await getCanvasDataset();
|
||||
const state = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
|
||||
@@ -211,6 +231,44 @@ function previewChecks(dataset, {
|
||||
return checks;
|
||||
}
|
||||
|
||||
function meterSceneChecks(dataset, pixelStats = null) {
|
||||
const bounds = parseJson(dataset.threePathBoundsMeters);
|
||||
return [
|
||||
check("scene units are meters", dataset.threeSceneUnits === "m", `sceneUnits=${dataset.threeSceneUnits}`),
|
||||
check("linear scale visible", Number(dataset.threeLinearUnitScaleToMeters || 0) > 0, `scale=${dataset.threeLinearUnitScaleToMeters}`),
|
||||
check("path fit bounds", dataset.threePathFitBounds === "ok", `fit=${dataset.threePathFitBounds}`),
|
||||
check("path bounds in meters", Number(bounds?.maxSpan || 0) > 0 && Number(bounds?.maxSpan || 0) < 5, `bounds=${dataset.threePathBoundsMeters}`),
|
||||
check("canvas nonblank", !pixelStats || pixelStats.nonBlackRatio > 0.015, `nonBlack=${pixelStats?.nonBlackRatio ?? "-"}`),
|
||||
];
|
||||
}
|
||||
|
||||
function mixedUnitsChecks(dataset, state) {
|
||||
const motion = state.programExecution?.motion || [];
|
||||
const units = [...new Set(motion.map((event) => event.linearUnits).filter(Boolean))];
|
||||
const xMeters = motion.map((event) => sceneMeterX(event)).filter(Number.isFinite);
|
||||
return [
|
||||
check("G20/G21 motion units", units.includes("inch") && units.includes("mm"), `units=${units.join(",")}`),
|
||||
check("scene units are meters", dataset.threeSceneUnits === "m", `sceneUnits=${dataset.threeSceneUnits}`),
|
||||
check("mixed unit path visible", Number(dataset.threePathPoints || 0) >= 2, `pathPoints=${dataset.threePathPoints}`),
|
||||
check("1 inch equals 25.4 mm in scene", xMeters.some((value) => Math.abs(value - 0.0254) < 1e-9), `xMeters=${xMeters.join(",")}`),
|
||||
check("no unit fallback in canonical motion", state.programExecutionSourceMode === "linuxcnc-interpreter-wasm", `source=${state.programExecutionSourceMode}`),
|
||||
];
|
||||
}
|
||||
|
||||
function sceneMeterX(event) {
|
||||
const factor = event?.linearUnits === "inch" ? 0.0254 : event?.linearUnits === "m" ? 1 : 0.001;
|
||||
const value = Number(event?.axes?.x);
|
||||
return Number.isFinite(value) ? value * factor : null;
|
||||
}
|
||||
|
||||
function parseJson(text) {
|
||||
try {
|
||||
return JSON.parse(text || "null");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function check(name, pass, detail) {
|
||||
return { name, pass: Boolean(pass), detail };
|
||||
}
|
||||
@@ -222,6 +280,111 @@ function assertChecks(caseName, checks) {
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeRunFeedbackState(state, dataset, elapsedMs, screenshotPath) {
|
||||
const activeRowLine = Number(documentActiveLineFromState(state));
|
||||
return {
|
||||
elapsedMs,
|
||||
screenshotPath,
|
||||
canvas: {
|
||||
threeReady: dataset.threeReady,
|
||||
rtcpState: dataset.threeRtcpState,
|
||||
executedPathPoints: Number(dataset.threeExecutedPathPoints || 0),
|
||||
currentSegmentHighlight: dataset.threeCurrentSegmentHighlight,
|
||||
toolExecutionTraceSource: dataset.threeToolExecutionTraceSource,
|
||||
},
|
||||
state: {
|
||||
profileId: state.machineProfile,
|
||||
iniReady: state.iniConfigReadiness?.ready === true,
|
||||
iniPath: state.iniConfigReadiness?.path || null,
|
||||
coordinates: state.iniConfigReadiness?.coordinates || null,
|
||||
kinematicsModuleId: state.profile?.kinematicsModuleId || null,
|
||||
selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel || null,
|
||||
taskHalSessionProgramPath: state.taskHalSession?.programPath || null,
|
||||
taskHalStatusLoop: state.taskHalStatusLoop || null,
|
||||
runState: state.runState,
|
||||
activeLine: state.activeLine,
|
||||
activeRowLine,
|
||||
activeLineMatchesUi: activeRowLine === Number(state.activeLine),
|
||||
droAxisPose: state.dro ? {
|
||||
x: state.dro.x,
|
||||
y: state.dro.y,
|
||||
z: state.dro.z,
|
||||
a: state.dro.a,
|
||||
b: state.dro.b,
|
||||
c: state.dro.c,
|
||||
} : null,
|
||||
feedbackAxisPose: state.programRuntimeFeedback?.axisPose || null,
|
||||
droMatchesFeedback: axisPoseMatchesDro(state.dro, state.programRuntimeFeedback?.axisPose),
|
||||
rtcpState: state.rtcpState,
|
||||
kinsType: state.kinsType,
|
||||
feedback: state.programRuntimeFeedback ? {
|
||||
sourceMode: state.programRuntimeFeedback.sourceMode,
|
||||
semanticBoundary: state.programRuntimeFeedback.semanticBoundary,
|
||||
line: state.programRuntimeFeedback.line,
|
||||
taskCycle: state.programRuntimeFeedback.taskCycle,
|
||||
servoCycle: state.programRuntimeFeedback.cycle,
|
||||
velocity: state.programRuntimeFeedback.currentVelocityMmPerMin,
|
||||
distanceToGo: state.programRuntimeFeedback.distanceToGo,
|
||||
} : null,
|
||||
feedbackHistoryLength: state.programRuntimeFeedbackHistory?.length || 0,
|
||||
feedbackHistorySourceModes: [...new Set((state.programRuntimeFeedbackHistory || []).map((entry) => entry.sourceMode))],
|
||||
taskHalStatus: state.taskHalStatus ? {
|
||||
taskState: state.taskHalStatus.ui?.taskState || state.taskHalStatus.task?.state || null,
|
||||
taskMode: state.taskHalStatus.ui?.taskMode || state.taskHalStatus.task?.mode || null,
|
||||
interpState: state.taskHalStatus.ui?.interpState || state.taskHalStatus.task?.interpState || null,
|
||||
taskCycle: state.taskHalStatus.ui?.taskCycle || null,
|
||||
servoCycle: state.taskHalStatus.ui?.servoCycle || null,
|
||||
} : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function documentActiveLineFromState(state) {
|
||||
return state.__activeRowLine ?? null;
|
||||
}
|
||||
|
||||
function axisPoseMatchesDro(dro, axisPose) {
|
||||
if (!dro || !axisPose) return false;
|
||||
return ["x", "y", "z", "a", "b", "c"].every((axis) => (
|
||||
Math.abs(Number(dro[axis] || 0) - Number(axisPose[axis] || 0)) < 1e-9
|
||||
));
|
||||
}
|
||||
|
||||
async function snapshotRunFeedback(elapsedMs, screenshotName) {
|
||||
const screenshotPath = await captureRunFeedbackFrame(screenshotName);
|
||||
const dataset = await getCanvasDataset();
|
||||
const state = await page.evaluate(() => {
|
||||
const snapshot = JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState()));
|
||||
snapshot.__activeRowLine = Number(document.querySelector(".gcode-row.active")?.dataset.programLine || 0);
|
||||
return snapshot;
|
||||
});
|
||||
return summarizeRunFeedbackState(state, dataset, elapsedMs, screenshotPath);
|
||||
}
|
||||
|
||||
function runFeedbackChecks(samples, baseline = null) {
|
||||
const states = samples.map((sample) => sample.state);
|
||||
const histories = states.map((state) => Number(state.feedbackHistoryLength || 0));
|
||||
const ticks = states.map((state) => Number(state.taskHalStatusLoop?.tickCount || 0));
|
||||
const baselineSequence = Number(baseline?.state?.taskHalStatusLoop?.sequence || 0);
|
||||
const sourceModes = states.flatMap((state) => state.feedbackHistorySourceModes || []);
|
||||
return [
|
||||
check("INI ready", states.every((state) => state.iniReady), `iniReady=${states.map((state) => state.iniReady).join(",")}`),
|
||||
check("selected LinuxCNC G-code", states.every((state) => state.selectedGcodeSourceRel?.endsWith("xyzac_switchkins_test_1.ngc")), states.at(-1)?.selectedGcodeSourceRel || "-"),
|
||||
check("task/HAL session opened selected G-code", states.every((state) => state.taskHalSessionProgramPath?.endsWith("xyzac_switchkins_test_1.ngc")), states.at(-1)?.taskHalSessionProgramPath || "-"),
|
||||
check("taskHalStatusLoop 新 RUN sequence", states.every((state) => Number(state.taskHalStatusLoop?.sequence || 0) > baselineSequence), `baselineSequence=${baselineSequence}, sequences=${states.map((state) => state.taskHalStatusLoop?.sequence || 0).join(",")}`),
|
||||
check("taskHalStatusLoop 本次 RUN tickCount", Math.max(...ticks) >= 3, `ticks=${ticks.join(",")}`),
|
||||
check("programRuntimeFeedbackHistory 本次 RUN 采样", Math.max(...histories) >= 3, `history=${histories.join(",")}`),
|
||||
check("feedback sourceMode 来自 task/HAL", sourceModes.length > 0 && sourceModes.every((mode) => mode === "linuxcnc-task-motion-hal-wasm"), `sourceModes=${sourceModes.join(",")}`),
|
||||
check("无 fixture-line-playback feedback", !sourceModes.includes("fixture-line-playback"), `sourceModes=${sourceModes.join(",")}`),
|
||||
check("semantic boundary", states.every((state) => state.feedback?.semanticBoundary === "linuxcnc_task_motion_hal_wasm_simulation_runtime"), states.map((state) => state.feedback?.semanticBoundary || "-").join(",")),
|
||||
check("activeLine 等于 UI 高亮行", states.every((state) => state.activeLineMatchesUi), `active=${states.map((state) => `${state.activeLine}/${state.activeRowLine}`).join(",")}`),
|
||||
check("DRO 等于 runtime feedback axisPose", states.every((state) => state.droMatchesFeedback), "droMatchesFeedback=true for all samples"),
|
||||
check("RTCP canvas 与 state 一致", samples.every((sample) => sample.canvas.rtcpState === sample.state.rtcpState), samples.map((sample) => `${sample.canvas.rtcpState}/${sample.state.rtcpState}`).join(",")),
|
||||
check("runtime cycle 可见", states.every((state) => Number(state.feedback?.taskCycle || 0) > 0 && Number(state.feedback?.servoCycle || 0) > 0), states.map((state) => `${state.feedback?.taskCycle || 0}/${state.feedback?.servoCycle || 0}`).join(",")),
|
||||
check("canvas 执行轨迹", samples.every((sample) => sample.canvas.threeReady === "true" && sample.canvas.currentSegmentHighlight === "ok" && sample.canvas.executedPathPoints >= 1), samples.map((sample) => `${sample.canvas.threeReady}/${sample.canvas.executedPathPoints}/${sample.canvas.currentSegmentHighlight}`).join(",")),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
await page.goto(`${baseUrl}${FIXTURE_URL}`, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
|
||||
@@ -303,6 +466,92 @@ try {
|
||||
await captureCase("05-vendored-impeller-toolpath");
|
||||
await recordCase("05-vendored-impeller-toolpath", "LinuxCNC vendored 五轴 impeller 程序:验证长路径、switchkins/RTCP 状态、rapid/feed 图层和 TCP 执行轨迹", checks);
|
||||
|
||||
const meterSceneSamples = [];
|
||||
for (const viewport of [
|
||||
{ name: "desktop", width: 1600, height: 1200, deviceScaleFactor: 1 },
|
||||
{ name: "mobile", width: 390, height: 844, deviceScaleFactor: 2 },
|
||||
]) {
|
||||
await page.setViewport(viewport);
|
||||
await waitForCanvasReady();
|
||||
await wait(600);
|
||||
const screenshotPath = await captureMeterSceneFrame(`08-meter-scene-${viewport.name}`);
|
||||
const sampleDataset = await getCanvasDataset();
|
||||
const pixelStats = await analyzePng(screenshotPath);
|
||||
const sampleChecks = [
|
||||
...previewChecks(sampleDataset, { requirePathPoints: true, requireExecutedPath: true }),
|
||||
...meterSceneChecks(sampleDataset, pixelStats),
|
||||
];
|
||||
assertChecks(`08-meter-scene-${viewport.name}`, sampleChecks);
|
||||
meterSceneSamples.push({
|
||||
viewport,
|
||||
screenshotPath,
|
||||
pixelStats,
|
||||
dataset: sampleDataset,
|
||||
status: sampleChecks.every((item) => item.pass) ? "PASS" : "FAIL",
|
||||
checks: sampleChecks,
|
||||
});
|
||||
}
|
||||
await page.setViewport({ width: 1600, height: 1200, deviceScaleFactor: 1 });
|
||||
const meterSceneChecksAll = meterSceneSamples.flatMap((sample) => sample.checks);
|
||||
const meterSceneReport = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: report.targetUrl,
|
||||
caseName: "08-meter-scene-desktop-mobile",
|
||||
summary: "浏览器米尺度 Three.js 证据:desktop/mobile canvas 非空、路径 bounds 为米、预览稳定居中",
|
||||
status: meterSceneChecksAll.every((item) => item.pass) ? "PASS" : "FAIL",
|
||||
samples: meterSceneSamples,
|
||||
consoleErrors,
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "meter-scene-evidence.json"),
|
||||
`${JSON.stringify(meterSceneReport, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
report.cases.push({
|
||||
name: "08-meter-scene-desktop-mobile",
|
||||
summary: meterSceneReport.summary,
|
||||
status: meterSceneReport.status,
|
||||
screenshotDir: METER_SCENE_SCREENSHOT_DIR,
|
||||
output: path.join(OUTPUT_DIR, "meter-scene-evidence.json"),
|
||||
sampleCount: meterSceneSamples.length,
|
||||
});
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.webRtcp5AxisSimulation.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "operator-g20-g21-mixed-units.ngc",
|
||||
content: [
|
||||
"G90 G20",
|
||||
"G1 X1.0 Y0 Z0 F10",
|
||||
"G21",
|
||||
"G1 X25.4 Y25.4 Z0 F254",
|
||||
"M2",
|
||||
].join("\n"),
|
||||
});
|
||||
});
|
||||
await waitForState((state) => (
|
||||
state.activeProgram === "operator-g20-g21-mixed-units.ngc" &&
|
||||
state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" &&
|
||||
(state.programExecution?.motion || []).some((event) => event.linearUnits === "inch") &&
|
||||
(state.programExecution?.motion || []).some((event) => event.linearUnits === "mm")
|
||||
), 20000, "G20/G21 mixed unit program loaded");
|
||||
await waitForCanvasReady();
|
||||
await wait(600);
|
||||
dataset = await getCanvasDataset();
|
||||
const mixedUnitsState = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
|
||||
checks = [
|
||||
...previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true }),
|
||||
...mixedUnitsChecks(dataset, mixedUnitsState),
|
||||
];
|
||||
assertChecks("09-g20-g21-mixed-units-preview", checks);
|
||||
await captureCase("09-g20-g21-mixed-units-preview");
|
||||
await recordCase("09-g20-g21-mixed-units-preview", "G20/G21 混合单位程序:验证 canonical motion 保留 inch/mm,Three.js 统一按米绘制", checks);
|
||||
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc");
|
||||
await waitForState((state) => state.activeProgram?.endsWith("impeller-7bl-xyzac.ngc"), 25000, "vendored impeller restored before run");
|
||||
await waitForCanvasReady();
|
||||
await wait(800);
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="power"]').click());
|
||||
await waitForState((state) => state.machine.taskState === "on", 10000, "machine powered on");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
|
||||
@@ -323,6 +572,88 @@ try {
|
||||
await captureCase("06-running-rtcp-toolpath");
|
||||
await recordCase("06-running-rtcp-toolpath", "G-code 运行态:验证 task/HAL runtime feedback 驱动执行轨迹、当前段高亮、TCP 球和刀轴线跟随", checks);
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="STOP"]').click());
|
||||
await waitForState((state) => state.taskHalStatusLoop?.active === false, 10000, "previous run stopped");
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc");
|
||||
await waitForState((state) => (
|
||||
state.activeProgram?.endsWith("xyzac_switchkins_test_1.ngc") &&
|
||||
state.taskHalSession?.programPath?.endsWith("xyzac_switchkins_test_1.ngc") &&
|
||||
state.iniConfigReadiness?.ready === true &&
|
||||
state.profile?.kinematicsModuleId === "xyzac-trt"
|
||||
), 25000, "run feedback selected program ready");
|
||||
await waitForCanvasReady();
|
||||
await page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
if (state.machine?.taskState !== "on") {
|
||||
document.querySelector('[data-action="power"]').click();
|
||||
}
|
||||
});
|
||||
await waitForState((state) => state.machine.taskState === "on", 10000, "machine powered on for run feedback");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
|
||||
await waitForState((state) => state.machine.mode === "manual", 10000, "manual mode selected for run feedback");
|
||||
await page.evaluate(() => document.querySelector('[data-action="HOME"]').click());
|
||||
await waitForState((state) => state.machine.allHomed === true, 10000, "machine homed for run feedback");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-auto"]').click());
|
||||
await waitForState((state) => state.machine.mode === "auto", 10000, "auto mode selected for run feedback");
|
||||
const runFeedbackBaseline = await snapshotRunFeedback(0, "07-run-preconditions-and-feedback-0000ms-before-run");
|
||||
await page.evaluate(() => document.querySelector('[data-action="RUN"]').click());
|
||||
try {
|
||||
await waitForState((state) => (
|
||||
["running", "complete"].includes(state.runState) &&
|
||||
(state.taskHalStatusLoop?.tickCount > runFeedbackBaseline.state.taskHalStatusLoop.tickCount ||
|
||||
(state.programRuntimeFeedbackHistory?.length || 0) >= 3) &&
|
||||
state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm"
|
||||
), 15000, "run feedback status loop produced feedback");
|
||||
} catch (error) {
|
||||
const failureSnapshot = await snapshotRunFeedback(15000, "07-run-preconditions-and-feedback-timeout");
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "run-preconditions-feedback-timeout.json"),
|
||||
`${JSON.stringify({
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
baseline: runFeedbackBaseline,
|
||||
failureSnapshot,
|
||||
consoleErrors,
|
||||
}, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
const runFeedbackStartedAt = Date.now();
|
||||
const runFeedbackSamples = [];
|
||||
for (const elapsedMs of [200, 500, 1000, 2000, 5000]) {
|
||||
const waitMs = Math.max(runFeedbackStartedAt + elapsedMs - Date.now(), 0);
|
||||
if (waitMs > 0) await wait(waitMs);
|
||||
await waitForCanvasReady();
|
||||
runFeedbackSamples.push(await snapshotRunFeedback(elapsedMs, `07-run-preconditions-and-feedback-${elapsedMs}ms`));
|
||||
}
|
||||
const runFeedbackCaseChecks = runFeedbackChecks(runFeedbackSamples, runFeedbackBaseline);
|
||||
assertChecks("07-run-preconditions-and-feedback", runFeedbackCaseChecks);
|
||||
const runFeedbackReport = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: report.targetUrl,
|
||||
caseName: "07-run-preconditions-and-feedback",
|
||||
summary: "浏览器 RUN 证据:INI/profile/kinematics/task-HAL 同一上下文,taskHalStatusLoop 持续推进,UI 消费 task/HAL/motion feedback",
|
||||
status: runFeedbackCaseChecks.every((item) => item.pass) ? "PASS" : "FAIL",
|
||||
checks: runFeedbackCaseChecks,
|
||||
baseline: runFeedbackBaseline,
|
||||
samples: runFeedbackSamples,
|
||||
consoleErrors,
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "run-preconditions-feedback.json"),
|
||||
`${JSON.stringify(runFeedbackReport, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
report.cases.push({
|
||||
name: "07-run-preconditions-and-feedback",
|
||||
summary: runFeedbackReport.summary,
|
||||
status: runFeedbackReport.status,
|
||||
checks: runFeedbackCaseChecks,
|
||||
screenshotDir: RUN_FEEDBACK_SCREENSHOT_DIR,
|
||||
output: path.join(OUTPUT_DIR, "run-preconditions-feedback.json"),
|
||||
sampleCount: runFeedbackSamples.length,
|
||||
});
|
||||
|
||||
report.consoleErrors = consoleErrors;
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "toolpath-preview-cases.json"),
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
{
|
||||
"generatedAt": "2026-06-23T01:38:37.231Z",
|
||||
"url": "http://127.0.0.1:35283/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"sourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
|
||||
"activeLines": [
|
||||
9,
|
||||
10,
|
||||
12
|
||||
],
|
||||
"historyLines": [
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12
|
||||
],
|
||||
"synced": true,
|
||||
"samples": [
|
||||
{
|
||||
"elapsedMs": 100,
|
||||
"activeLine": 9,
|
||||
"uiLine": 9,
|
||||
"runState": "running",
|
||||
"velocity": 2100,
|
||||
"line": 9,
|
||||
"motionProgramLine": 9,
|
||||
"halProgramLine": 9,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 250,
|
||||
"activeLine": 10,
|
||||
"uiLine": 10,
|
||||
"runState": "running",
|
||||
"velocity": 2100,
|
||||
"line": 10,
|
||||
"motionProgramLine": 10,
|
||||
"halProgramLine": 10,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 500,
|
||||
"activeLine": 10,
|
||||
"uiLine": 10,
|
||||
"runState": "running",
|
||||
"velocity": 2100,
|
||||
"line": 10,
|
||||
"motionProgramLine": 10,
|
||||
"halProgramLine": 10,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 1000,
|
||||
"activeLine": 12,
|
||||
"uiLine": 12,
|
||||
"runState": "running",
|
||||
"velocity": 100.0002,
|
||||
"line": 12,
|
||||
"motionProgramLine": 12,
|
||||
"halProgramLine": 12,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 11,
|
||||
"motion": 11,
|
||||
"hal": 11,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 2000,
|
||||
"activeLine": 12,
|
||||
"uiLine": 12,
|
||||
"runState": "running",
|
||||
"velocity": 100.0002,
|
||||
"line": 12,
|
||||
"motionProgramLine": 12,
|
||||
"halProgramLine": 12,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 11,
|
||||
"motion": 11,
|
||||
"hal": 11,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 4000,
|
||||
"activeLine": 12,
|
||||
"uiLine": 12,
|
||||
"runState": "running",
|
||||
"velocity": 100.0002,
|
||||
"line": 12,
|
||||
"motionProgramLine": 12,
|
||||
"halProgramLine": 12,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"final": {
|
||||
"runState": "running",
|
||||
"activeLine": 12,
|
||||
"feedbackHistoryLength": 24
|
||||
},
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)",
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
]
|
||||
}
|
||||
267
qa/web-rtcp-5axis-site-test/output/meter-scene-evidence.json
Normal file
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"generatedAt": "2026-06-22T22:09:41.825Z",
|
||||
"targetUrl": "http://127.0.0.1:45757/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"caseName": "08-meter-scene-desktop-mobile",
|
||||
"summary": "浏览器米尺度 Three.js 证据:desktop/mobile canvas 非空、路径 bounds 为米、预览稳定居中",
|
||||
"status": "PASS",
|
||||
"samples": [
|
||||
{
|
||||
"viewport": {
|
||||
"name": "desktop",
|
||||
"width": 1600,
|
||||
"height": 1200,
|
||||
"deviceScaleFactor": 1
|
||||
},
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/meter-scene-evidence/08-meter-scene-desktop.png",
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 65.18,
|
||||
"nonBlackRatio": 0.8173
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":36.474,\"y\":-22.486,\"z\":-13.749,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "186",
|
||||
"threeFeedPathPoints": "1436",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "1",
|
||||
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
|
||||
},
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=1498"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "scene units are meters",
|
||||
"pass": true,
|
||||
"detail": "sceneUnits=m"
|
||||
},
|
||||
{
|
||||
"name": "linear scale visible",
|
||||
"pass": true,
|
||||
"detail": "scale=0.001"
|
||||
},
|
||||
{
|
||||
"name": "path fit bounds",
|
||||
"pass": true,
|
||||
"detail": "fit=ok"
|
||||
},
|
||||
{
|
||||
"name": "path bounds in meters",
|
||||
"pass": true,
|
||||
"detail": "bounds={\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}"
|
||||
},
|
||||
{
|
||||
"name": "canvas nonblank",
|
||||
"pass": true,
|
||||
"detail": "nonBlack=0.8173"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"viewport": {
|
||||
"name": "mobile",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"deviceScaleFactor": 2
|
||||
},
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/meter-scene-evidence/08-meter-scene-mobile.png",
|
||||
"pixelStats": {
|
||||
"width": 1146,
|
||||
"height": 1036,
|
||||
"averageLuminance": 67.87,
|
||||
"nonBlackRatio": 0.838
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":36.474,\"y\":-22.486,\"z\":-13.749,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "186",
|
||||
"threeFeedPathPoints": "1436",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "1",
|
||||
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
|
||||
},
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=1498"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "scene units are meters",
|
||||
"pass": true,
|
||||
"detail": "sceneUnits=m"
|
||||
},
|
||||
{
|
||||
"name": "linear scale visible",
|
||||
"pass": true,
|
||||
"detail": "scale=0.001"
|
||||
},
|
||||
{
|
||||
"name": "path fit bounds",
|
||||
"pass": true,
|
||||
"detail": "fit=ok"
|
||||
},
|
||||
{
|
||||
"name": "path bounds in meters",
|
||||
"pass": true,
|
||||
"detail": "bounds={\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}"
|
||||
},
|
||||
{
|
||||
"name": "canvas nonblank",
|
||||
"pass": true,
|
||||
"detail": "nonBlack=0.838"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)",
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
]
|
||||
}
|
||||
5962
qa/web-rtcp-5axis-site-test/output/public-https-run-debug.json
Normal file
@@ -0,0 +1,565 @@
|
||||
{
|
||||
"generatedAt": "2026-06-22T22:10:40.219Z",
|
||||
"targetUrl": "http://127.0.0.1:45757/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"caseName": "07-run-preconditions-and-feedback",
|
||||
"summary": "浏览器 RUN 证据:INI/profile/kinematics/task-HAL 同一上下文,taskHalStatusLoop 持续推进,UI 消费 task/HAL/motion feedback",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "INI ready",
|
||||
"pass": true,
|
||||
"detail": "iniReady=true,true,true,true,true"
|
||||
},
|
||||
{
|
||||
"name": "selected LinuxCNC G-code",
|
||||
"pass": true,
|
||||
"detail": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc"
|
||||
},
|
||||
{
|
||||
"name": "task/HAL session opened selected G-code",
|
||||
"pass": true,
|
||||
"detail": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc"
|
||||
},
|
||||
{
|
||||
"name": "taskHalStatusLoop 新 RUN sequence",
|
||||
"pass": true,
|
||||
"detail": "baselineSequence=1, sequences=2,2,2,2,2"
|
||||
},
|
||||
{
|
||||
"name": "taskHalStatusLoop 本次 RUN tickCount",
|
||||
"pass": true,
|
||||
"detail": "ticks=4,4,4,4,4"
|
||||
},
|
||||
{
|
||||
"name": "programRuntimeFeedbackHistory 本次 RUN 采样",
|
||||
"pass": true,
|
||||
"detail": "history=5,5,5,5,5"
|
||||
},
|
||||
{
|
||||
"name": "feedback sourceMode 来自 task/HAL",
|
||||
"pass": true,
|
||||
"detail": "sourceModes=linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm"
|
||||
},
|
||||
{
|
||||
"name": "无 fixture-line-playback feedback",
|
||||
"pass": true,
|
||||
"detail": "sourceModes=linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm"
|
||||
},
|
||||
{
|
||||
"name": "semantic boundary",
|
||||
"pass": true,
|
||||
"detail": "linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime"
|
||||
},
|
||||
{
|
||||
"name": "activeLine 等于 UI 高亮行",
|
||||
"pass": true,
|
||||
"detail": "active=29/29,29/29,29/29,29/29,29/29"
|
||||
},
|
||||
{
|
||||
"name": "DRO 等于 runtime feedback axisPose",
|
||||
"pass": true,
|
||||
"detail": "droMatchesFeedback=true for all samples"
|
||||
},
|
||||
{
|
||||
"name": "RTCP canvas 与 state 一致",
|
||||
"pass": true,
|
||||
"detail": "off/off,off/off,off/off,off/off,off/off"
|
||||
},
|
||||
{
|
||||
"name": "runtime cycle 可见",
|
||||
"pass": true,
|
||||
"detail": "33/330,33/330,33/330,33/330,33/330"
|
||||
},
|
||||
{
|
||||
"name": "canvas 执行轨迹",
|
||||
"pass": true,
|
||||
"detail": "true/1/ok,true/1/ok,true/1/ok,true/1/ok,true/1/ok"
|
||||
}
|
||||
],
|
||||
"baseline": {
|
||||
"elapsedMs": 0,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-0000ms-before-run.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 1,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 12,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:27.362Z",
|
||||
"lastError": null,
|
||||
"stopReason": "running",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "idle",
|
||||
"activeLine": 1,
|
||||
"activeRowLine": 1,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 1,
|
||||
"taskCycle": 3,
|
||||
"servoCycle": 30,
|
||||
"velocity": 0,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 18,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 30
|
||||
}
|
||||
}
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"elapsedMs": 200,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-200ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"elapsedMs": 500,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-500ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"elapsedMs": 1000,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-1000ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"elapsedMs": 2000,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-2000ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"elapsedMs": 5000,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-5000ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)",
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"generatedAt": "2026-06-22T13:12:11.146Z",
|
||||
"targetUrl": "http://127.0.0.1:43627/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"generatedAt": "2026-06-22T22:09:28.057Z",
|
||||
"targetUrl": "http://127.0.0.1:45757/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"projectPath": "/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"chromePath": "/usr/bin/google-chrome",
|
||||
"screenshots": {
|
||||
@@ -9,6 +9,7 @@
|
||||
"03-arc-demo-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/03-arc-demo-toolpath.png",
|
||||
"04-clear-preview-reference": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/04-clear-preview-reference.png",
|
||||
"05-vendored-impeller-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/05-vendored-impeller-toolpath.png",
|
||||
"09-g20-g21-mixed-units-preview": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/09-g20-g21-mixed-units-preview.png",
|
||||
"06-running-rtcp-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/06-running-rtcp-toolpath.png"
|
||||
},
|
||||
"cases": [
|
||||
@@ -66,8 +67,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 62.52,
|
||||
"nonBlackRatio": 0.7491
|
||||
"averageLuminance": 68.18,
|
||||
"nonBlackRatio": 0.8681
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -77,7 +78,10 @@
|
||||
"threePathPoints": "2",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -95,6 +99,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":0,\"y\":0,\"z\":0.002},\"size\":{\"x\":0,\"y\":0,\"z\":0.005},\"maxSpan\":0.005}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -207,8 +212,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 59.4,
|
||||
"nonBlackRatio": 0.7092
|
||||
"averageLuminance": 66.03,
|
||||
"nonBlackRatio": 0.854
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -218,7 +223,10 @@
|
||||
"threePathPoints": "6",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -236,6 +244,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":0.005,\"y\":0.005,\"z\":0.002},\"size\":{\"x\":0.01,\"y\":0.01,\"z\":0.005},\"maxSpan\":0.01}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -346,8 +355,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 65.26,
|
||||
"nonBlackRatio": 0.7885
|
||||
"averageLuminance": 67.56,
|
||||
"nonBlackRatio": 0.8687
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -357,7 +366,10 @@
|
||||
"threePathPoints": "2",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":1,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -375,6 +387,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":0.001,\"y\":0.001,\"z\":0},\"size\":{\"x\":0.001,\"y\":0.001,\"z\":0},\"maxSpan\":0.001}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -487,8 +500,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 64.72,
|
||||
"nonBlackRatio": 0.7874
|
||||
"averageLuminance": 67.78,
|
||||
"nonBlackRatio": 0.8701
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -498,7 +511,10 @@
|
||||
"threePathPoints": "0",
|
||||
"threeExecutedPathPoints": "0",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":1,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -516,6 +532,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "null",
|
||||
"threeCurrentSegmentHighlight": "pending",
|
||||
"threeRapidFeedVisualDistinction": "pending",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -643,8 +660,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 35.56,
|
||||
"nonBlackRatio": 0.4577
|
||||
"averageLuminance": 65.18,
|
||||
"nonBlackRatio": 0.8173
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -654,7 +671,10 @@
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":36.474,\"y\":-22.486,\"z\":-13.749,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -672,6 +692,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -726,6 +747,176 @@
|
||||
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "08-meter-scene-desktop-mobile",
|
||||
"summary": "浏览器米尺度 Three.js 证据:desktop/mobile canvas 非空、路径 bounds 为米、预览稳定居中",
|
||||
"status": "PASS",
|
||||
"screenshotDir": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/meter-scene-evidence",
|
||||
"output": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/meter-scene-evidence.json",
|
||||
"sampleCount": 2
|
||||
},
|
||||
{
|
||||
"name": "09-g20-g21-mixed-units-preview",
|
||||
"summary": "G20/G21 混合单位程序:验证 canonical motion 保留 inch/mm,Three.js 统一按米绘制",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=2"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "G20/G21 motion units",
|
||||
"pass": true,
|
||||
"detail": "units=inch,mm"
|
||||
},
|
||||
{
|
||||
"name": "scene units are meters",
|
||||
"pass": true,
|
||||
"detail": "sceneUnits=m"
|
||||
},
|
||||
{
|
||||
"name": "mixed unit path visible",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=2"
|
||||
},
|
||||
{
|
||||
"name": "1 inch equals 25.4 mm in scene",
|
||||
"pass": true,
|
||||
"detail": "xMeters=0.0254,0.0254"
|
||||
},
|
||||
{
|
||||
"name": "no unit fallback in canonical motion",
|
||||
"pass": true,
|
||||
"detail": "source=linuxcnc-interpreter-wasm"
|
||||
}
|
||||
],
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 64.71,
|
||||
"nonBlackRatio": 0.849
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "2",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":43,\"y\":-32.15,\"z\":-11.306,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":0.013,\"y\":0.013,\"z\":0},\"size\":{\"x\":0.025,\"y\":0.025,\"z\":0},\"maxSpan\":0.0254}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "0",
|
||||
"threeFeedPathPoints": "2",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "1",
|
||||
"threeCurrentSegmentType": "STRAIGHT_FEED"
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "operator-g20-g21-mixed-units.ngc",
|
||||
"activeLine": 2,
|
||||
"programSource": "operator-file",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
"ready": true,
|
||||
"programLineCount": 5,
|
||||
"canonicalEventCount": 22,
|
||||
"motionEventCount": 2,
|
||||
"motionTypes": [
|
||||
"STRAIGHT_FEED"
|
||||
],
|
||||
"finalAxes": {
|
||||
"x": 25.4,
|
||||
"y": 25.4,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"w": 0
|
||||
},
|
||||
"switchkinsEventCount": 0,
|
||||
"switchkinsCodes": [],
|
||||
"switchkinsRemapBoundary": null,
|
||||
"remapRuntimeReady": false,
|
||||
"plannerRuntimeReady": true,
|
||||
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
"machineFileExecutionReady": false,
|
||||
"fullLinuxCncProgramExecutionReady": false
|
||||
},
|
||||
"runState": "idle",
|
||||
"rtcpState": "on",
|
||||
"kinsType": "tcp-xyzac",
|
||||
"programExecutionMotionIndex": 0,
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "06-running-rtcp-toolpath",
|
||||
"summary": "G-code 运行态:验证 task/HAL runtime feedback 驱动执行轨迹、当前段高亮、TCP 球和刀轴线跟随",
|
||||
@@ -790,8 +981,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 36.05,
|
||||
"nonBlackRatio": 0.4607
|
||||
"averageLuminance": 65.86,
|
||||
"nonBlackRatio": 0.8173
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -801,9 +992,12 @@
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0.26,\"y\":-0.458,\"z\":1.485}",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":7.417,\"y\":-13.098,\"z\":28.366,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeToolhead": "{\"x\":-0.023,\"y\":-0.033,\"z\":0.001}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0.621,\"y\":0.503,\"z\":0.601}",
|
||||
"threeTcpPose": "{\"x\":-75.066,\"y\":-75.473,\"z\":-49.344,\"a\":-53.043,\"c\":-50.969}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
@@ -819,6 +1013,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -830,7 +1025,7 @@
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
"activeLine": 5,
|
||||
"activeLine": 65,
|
||||
"programSource": "linuxcnc-vendored-5axis-gcode",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
@@ -872,6 +1067,86 @@
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-task-motion-hal-wasm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "07-run-preconditions-and-feedback",
|
||||
"summary": "浏览器 RUN 证据:INI/profile/kinematics/task-HAL 同一上下文,taskHalStatusLoop 持续推进,UI 消费 task/HAL/motion feedback",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "INI ready",
|
||||
"pass": true,
|
||||
"detail": "iniReady=true,true,true,true,true"
|
||||
},
|
||||
{
|
||||
"name": "selected LinuxCNC G-code",
|
||||
"pass": true,
|
||||
"detail": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc"
|
||||
},
|
||||
{
|
||||
"name": "task/HAL session opened selected G-code",
|
||||
"pass": true,
|
||||
"detail": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc"
|
||||
},
|
||||
{
|
||||
"name": "taskHalStatusLoop 新 RUN sequence",
|
||||
"pass": true,
|
||||
"detail": "baselineSequence=1, sequences=2,2,2,2,2"
|
||||
},
|
||||
{
|
||||
"name": "taskHalStatusLoop 本次 RUN tickCount",
|
||||
"pass": true,
|
||||
"detail": "ticks=4,4,4,4,4"
|
||||
},
|
||||
{
|
||||
"name": "programRuntimeFeedbackHistory 本次 RUN 采样",
|
||||
"pass": true,
|
||||
"detail": "history=5,5,5,5,5"
|
||||
},
|
||||
{
|
||||
"name": "feedback sourceMode 来自 task/HAL",
|
||||
"pass": true,
|
||||
"detail": "sourceModes=linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm"
|
||||
},
|
||||
{
|
||||
"name": "无 fixture-line-playback feedback",
|
||||
"pass": true,
|
||||
"detail": "sourceModes=linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm"
|
||||
},
|
||||
{
|
||||
"name": "semantic boundary",
|
||||
"pass": true,
|
||||
"detail": "linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime"
|
||||
},
|
||||
{
|
||||
"name": "activeLine 等于 UI 高亮行",
|
||||
"pass": true,
|
||||
"detail": "active=29/29,29/29,29/29,29/29,29/29"
|
||||
},
|
||||
{
|
||||
"name": "DRO 等于 runtime feedback axisPose",
|
||||
"pass": true,
|
||||
"detail": "droMatchesFeedback=true for all samples"
|
||||
},
|
||||
{
|
||||
"name": "RTCP canvas 与 state 一致",
|
||||
"pass": true,
|
||||
"detail": "off/off,off/off,off/off,off/off,off/off"
|
||||
},
|
||||
{
|
||||
"name": "runtime cycle 可见",
|
||||
"pass": true,
|
||||
"detail": "33/330,33/330,33/330,33/330,33/330"
|
||||
},
|
||||
{
|
||||
"name": "canvas 执行轨迹",
|
||||
"pass": true,
|
||||
"detail": "true/1/ok,true/1/ok,true/1/ok,true/1/ok,true/1/ok"
|
||||
}
|
||||
],
|
||||
"screenshotDir": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback",
|
||||
"output": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/run-preconditions-feedback.json",
|
||||
"sampleCount": 5
|
||||
}
|
||||
],
|
||||
"consoleErrors": [
|
||||
|
||||
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 28 KiB |