feat: sync latest run execution updates

This commit is contained in:
2026-06-22 21:47:16 -04:00
parent 0b1aad39e1
commit 8d3177cb73
92 changed files with 22837 additions and 246 deletions

View 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");
}

View File

@@ -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/mmThree.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"),

View File

@@ -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)"
]
}

View 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)"
]
}

File diff suppressed because one or more lines are too long

View 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)"
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -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/mmThree.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": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB