Files
cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/tools/trace-pause-position-json.mjs
2026-07-07 09:40:33 -04:00

303 lines
12 KiB
JavaScript

import { mkdir, writeFile } from "node:fs/promises";
import { createReadStream, statSync } from "node:fs";
import { createServer } from "node:http";
import { resolve } from "node:path";
import { chromium } from "../app/node_modules/playwright/index.mjs";
const repoRoot = resolve(import.meta.dirname, "../..");
const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan");
const appUrlPath = process.env.APP_URL_PATH || "/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html";
const sourceRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc";
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
const outputDir = resolve(projectRoot, "working/pause-position-traces", `pause-position-${timestamp}`);
const chromiumExecutable = process.env.CHROMIUM || findSystemChromium();
await mkdir(outputDir, { recursive: true });
const server = await startStaticServer(repoRoot);
let browser;
try {
browser = await chromium.launch({
headless: true,
executablePath: chromiumExecutable || undefined,
args: ["--disable-gpu", "--no-sandbox"],
});
const context = await browser.newContext({
viewport: { width: 1600, height: 1000 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
page.setDefaultTimeout(45000);
const url = `http://127.0.0.1:${server.port}${appUrlPath}`;
await page.goto(url, { waitUntil: "networkidle" });
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState));
await Promise.all([
page.evaluate(() => window.webRtcp5AxisSimulation.machineFileSeedReady),
page.evaluate(() => window.webRtcp5AxisSimulation.interpreterRuntimeReady),
page.evaluate(() => window.webRtcp5AxisSimulation.taskHalRuntimeReady),
]);
await page.evaluate((selectedSourceRel) => {
window.webRtcp5AxisSimulation.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: selectedSourceRel });
}, sourceRel);
await waitForState(page, (state) => (
state.machineFileStaging?.selectedGcodeSourceRel === sourceRel &&
Number(state.programAxisPreviewPath?.sampleCount || 0) > 0
), "G-code loaded");
await ensureRunReady(page);
await page.click('[data-tool-id="btn_run"]');
await waitForState(page, (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
Number(state.programExecutionSampleIndex || 0) >= 0
), "program running", 60000);
await page.waitForTimeout(1800);
const beforePause = await readTraceSample(page, "before-pause");
await page.click('[data-tool-id="tbtn_pause"]');
await waitForState(page, (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
), "pause button set paused");
const pauseStart = await readTraceSample(page, "pause-start");
const samples = [beforePause, pauseStart];
const startedAt = Date.now();
while (Date.now() - startedAt < 3500) {
await page.waitForTimeout(100);
samples.push(await readTraceSample(page, `pause-hold-${Date.now() - startedAt}`));
}
const analysis = analyzePauseTrace(samples);
const manifest = {
apiName: "xyzbc-trt-pause-position-json-trace",
status: analysis.positionChangedAfterPause ? "failed-position-changed" : "passed-position-frozen",
capturedAt: new Date().toISOString(),
appUrlPath,
url,
sourceRel,
outputDir,
pauseSelector: '[data-tool-id="tbtn_pause"]',
analysis,
samples,
};
await writeFile(resolve(outputDir, "trace.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
console.log(`pause_position_status=${manifest.status}`);
console.log(`trace=${resolve(outputDir, "trace.json")}`);
console.log(`app_url_path=${appUrlPath}`);
console.log(`position_changed=${analysis.positionChangedAfterPause}`);
console.log(`changed_fields=${analysis.changedFields.join(",")}`);
} finally {
await browser?.close().catch(() => {});
await server.close();
}
async function ensureRunReady(page) {
await page.evaluate(() => window.webRtcp5AxisSimulation.dispatch({ type: "ESTOP" }));
await waitForState(page, (state) => state.machine?.taskState === "estop", "estop active");
await page.evaluate(() => window.webRtcp5AxisSimulation.dispatch({ type: "RESET" }));
await waitForState(page, (state) => state.machine?.taskState === "estop-reset", "estop reset");
await page.evaluate(() => window.webRtcp5AxisSimulation.dispatch({ type: "TOGGLE_POWER" }));
await waitForState(page, (state) => state.machine?.taskState === "on" && state.machine?.powerOn === true, "power on");
await page.evaluate(() => window.webRtcp5AxisSimulation.dispatch({ type: "HOME" }));
await waitForState(page, (state) => state.machine?.allHomed === true, "home all");
}
async function readTraceSample(page, label) {
return page.evaluate((sampleLabel) => {
const state = window.webRtcp5AxisSimulation.getState();
const canvas = document.querySelector("[data-five-axis-canvas]");
const parseJson = (value) => {
try {
return value ? JSON.parse(value) : null;
} catch {
return null;
}
};
return {
label: sampleLabel,
capturedAt: new Date().toISOString(),
runState: state.runState,
taskState: state.machine?.taskState,
mode: state.machine?.mode,
interpState: state.machine?.interpState,
taskPaused: state.machine?.taskPaused,
activeLine: state.activeLine,
sampleIndex: state.programExecutionSampleIndex,
currentVelocity: state.feed?.currentVelocity,
axisPose: pickAxes(state.axisPose),
dro: pickDro(state.dro),
runtimeAxisPose: pickAxes(state.programRuntimeFeedback?.axisPose),
runtimeTcp: pickTcp(state.programRuntimeFeedback?.tcp),
uiExecution: {
status: state.programUiExecution?.status,
sampleIndex: state.programUiExecution?.sampleIndex,
sourceFile: state.programUiExecution?.sourceFile,
line: state.programUiExecution?.line,
statement: state.programUiExecution?.statement,
joint: pickAxes(state.programUiExecution?.joint),
tcp: pickTcp(state.programUiExecution?.tcp),
},
canvasToolhead: parseJson(canvas?.dataset?.threeToolhead),
canvasToolAxis: parseJson(canvas?.dataset?.threeToolAxis),
pauseButton: {
action: document.querySelector('[data-tool-id="tbtn_pause"]')?.dataset?.action || null,
paused: document.querySelector('[data-tool-id="tbtn_pause"]')?.dataset?.paused || null,
title: document.querySelector('[data-tool-id="tbtn_pause"]')?.title || null,
},
};
function pickAxes(value = {}) {
return value ? {
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),
} : null;
}
function pickDro(value = {}) {
return value ? {
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),
tcpX: Number(value.tcpX || 0),
tcpY: Number(value.tcpY || 0),
tcpZ: Number(value.tcpZ || 0),
} : null;
}
function pickTcp(value = {}) {
return value ? {
x: Number(value.x || 0),
y: Number(value.y || 0),
z: Number(value.z || 0),
} : null;
}
}, label);
}
function analyzePauseTrace(samples) {
const pauseSamples = samples.filter((sample) => sample.runState === "paused");
const baseline = pauseSamples[0] || null;
const changedFields = [];
if (baseline) {
for (const sample of pauseSamples.slice(1)) {
compareVector("axisPose", baseline.axisPose, sample.axisPose, changedFields);
compareVector("dro", baseline.dro, sample.dro, changedFields);
compareVector("runtimeAxisPose", baseline.runtimeAxisPose, sample.runtimeAxisPose, changedFields);
compareVector("runtimeTcp", baseline.runtimeTcp, sample.runtimeTcp, changedFields);
compareVector("uiExecution.joint", baseline.uiExecution?.joint, sample.uiExecution?.joint, changedFields);
compareVector("uiExecution.tcp", baseline.uiExecution?.tcp, sample.uiExecution?.tcp, changedFields);
compareVector("canvasToolhead", baseline.canvasToolhead, sample.canvasToolhead, changedFields, 1e-4);
if (Number(sample.sampleIndex) !== Number(baseline.sampleIndex)) changedFields.push("sampleIndex");
if (Number(sample.currentVelocity) !== 0) changedFields.push("currentVelocity");
}
}
return {
baselineLabel: baseline?.label || null,
pausedSampleCount: pauseSamples.length,
changedFields: [...new Set(changedFields)],
positionChangedAfterPause: changedFields.length > 0,
};
}
function compareVector(name, before, after, changedFields, tolerance = 1e-6) {
if (!before || !after) return;
for (const key of Object.keys(before)) {
const delta = Math.abs(Number(after[key] || 0) - Number(before[key] || 0));
if (delta > tolerance) changedFields.push(`${name}.${key}`);
}
}
async function waitForState(page, predicate, label, timeoutMs = 30000) {
const predicateText = predicate.toString();
await page.waitForFunction(
([source, selectedSourceRel]) => {
const state = window.webRtcp5AxisSimulation?.getState?.();
if (!state) return false;
const sourceRel = selectedSourceRel;
return Function("state", "sourceRel", `return (${source})(state, sourceRel);`)(state, sourceRel);
},
[predicateText, sourceRel],
{ timeout: timeoutMs },
).catch(async (error) => {
const state = await page.evaluate(() => window.webRtcp5AxisSimulation?.getState?.()).catch(() => null);
throw new Error(`Timed out waiting for ${label}: ${error.message}\n${JSON.stringify({
runState: state?.runState,
taskState: state?.machine?.taskState,
mode: state?.machine?.mode,
interpState: state?.machine?.interpState,
sampleIndex: state?.programExecutionSampleIndex,
operatorMessage: state?.operatorMessage,
}, null, 2)}`);
});
}
function startStaticServer(root) {
const server = createServer((request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
const decoded = decodeURIComponent(url.pathname);
const relative = decoded === "/" ? "/index.html" : decoded;
const target = resolve(root, `.${relative}`);
if (!target.startsWith(root)) {
response.writeHead(403);
response.end("Forbidden");
return;
}
let itemStat;
try {
itemStat = statSync(target);
if (!itemStat.isFile()) throw new Error("not a file");
} catch {
response.writeHead(404);
response.end("Not found");
return;
}
response.writeHead(200, {
"content-type": contentType(target),
"content-length": itemStat.size,
"cache-control": "no-store",
});
createReadStream(target).pipe(response);
});
return new Promise((resolveStart, rejectStart) => {
server.on("error", rejectStart);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
resolveStart({
port: address.port,
close: () => new Promise((resolveClose) => server.close(resolveClose)),
});
});
});
}
function contentType(file) {
if (file.endsWith(".html")) return "text/html; charset=utf-8";
if (file.endsWith(".js") || file.endsWith(".mjs")) return "text/javascript; charset=utf-8";
if (file.endsWith(".css")) return "text/css; charset=utf-8";
if (file.endsWith(".json")) return "application/json; charset=utf-8";
if (file.endsWith(".wasm")) return "application/wasm";
if (file.endsWith(".svg")) return "image/svg+xml";
if (file.endsWith(".png")) return "image/png";
return "application/octet-stream";
}
function findSystemChromium() {
for (const candidate of ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"]) {
try {
statSync(candidate);
return candidate;
} catch {
// Continue probing.
}
}
return null;
}