feat: sync latest run execution updates
13
AGENTS.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Scope
|
||||
|
||||
This file defines workspace-level agent operating rules for
|
||||
`/home/meswork/cnc_wams`.
|
||||
|
||||
## Logging Rule
|
||||
|
||||
After every GPT/Codex execution completes, append the full execution process
|
||||
log to:
|
||||
|
||||
`/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
|
||||
585
qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs
Normal file
@@ -0,0 +1,585 @@
|
||||
import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { PNG } from "pngjs";
|
||||
import {
|
||||
AlignmentType,
|
||||
Document,
|
||||
HeadingLevel,
|
||||
ImageRun,
|
||||
Packer,
|
||||
Paragraph,
|
||||
Table,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TextRun,
|
||||
WidthType,
|
||||
} from "docx";
|
||||
|
||||
const REPO_ROOT = path.resolve("/home/meswork/cnc_wams");
|
||||
const QA_ROOT = path.join(REPO_ROOT, "qa/web-rtcp-5axis-site-test");
|
||||
const OUTPUT_DIR = path.join(QA_ROOT, "output");
|
||||
const SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "test-linuxcnc-source-run");
|
||||
const TEST_SOURCE_DIR = path.join(REPO_ROOT, "web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source");
|
||||
const TEST_INI_PATH = path.join(TEST_SOURCE_DIR, "xyzac-trt.ini");
|
||||
const TEST_GCODE_PATH = path.join(TEST_SOURCE_DIR, "impeller-7bl-xyzac.ngc");
|
||||
const VENDORED_GCODE_REL = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc";
|
||||
const CHROME_PATH = process.env.CHROME_PATH || "/usr/bin/google-chrome";
|
||||
const FIXTURE_URL = "/web-rtcp-5axis-sim-plan/app/index.html";
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
const iniText = await fs.readFile(TEST_INI_PATH, "utf8");
|
||||
const gcodeText = await fs.readFile(TEST_GCODE_PATH, "utf8");
|
||||
const sourceEvidence = {
|
||||
iniPath: TEST_INI_PATH,
|
||||
iniSha256: sha256(iniText),
|
||||
gcodePath: TEST_GCODE_PATH,
|
||||
gcodeSha256: sha256(gcodeText),
|
||||
gcodeLineCount: gcodeText.split(/\r?\n/).length,
|
||||
gcodeBytes: Buffer.byteLength(gcodeText),
|
||||
};
|
||||
|
||||
const server = createStaticServer(REPO_ROOT);
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("failed to start static server");
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: CHROME_PATH,
|
||||
defaultViewport: { width: 1600, height: 1200, deviceScaleFactor: 1 },
|
||||
args: [
|
||||
"--disable-gpu",
|
||||
"--enable-webgl",
|
||||
"--use-angle=swiftshader",
|
||||
"--enable-unsafe-swiftshader",
|
||||
"--no-sandbox",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
const consoleErrors = [];
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") consoleErrors.push(msg.text());
|
||||
});
|
||||
page.on("pageerror", (error) => consoleErrors.push(error.message));
|
||||
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: `${baseUrl}${FIXTURE_URL}`,
|
||||
chromePath: CHROME_PATH,
|
||||
sourceEvidence,
|
||||
screenshots: {},
|
||||
steps: [],
|
||||
checks: [],
|
||||
samples: [],
|
||||
consoleErrors,
|
||||
};
|
||||
|
||||
try {
|
||||
await page.goto(`${baseUrl}${FIXTURE_URL}`, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
|
||||
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 });
|
||||
await waitForState((state) => state.kinematicsRuntimeReadiness?.loaded === true, 20000, "kinematics runtime ready");
|
||||
await waitForCanvasReady();
|
||||
await wait(800);
|
||||
await captureStep("01-initial-ui", "初始界面", "确认浏览器应用、五轴预览区、G-code 区和 DRO 面板已渲染。");
|
||||
|
||||
await page.evaluate(async ({ ini }) => {
|
||||
await window.webRtcp5AxisSimulation.stageMachineFiles({ iniText: ini, storageMode: "memory" });
|
||||
}, { ini: iniText });
|
||||
await waitForState((state) => state.machineFileStaging?.status === "staged", 20000, "test INI staged");
|
||||
await captureStep("02-stage-test-ini", "Stage test_linuxcnc_source INI", "使用 working_run/test_linuxcnc_source/xyzac-trt.ini 重新 staging machine files。");
|
||||
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', VENDORED_GCODE_REL);
|
||||
await waitForState((state) => (
|
||||
state.activeProgram?.endsWith("impeller-7bl-xyzac.ngc") &&
|
||||
state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" &&
|
||||
state.taskHalSession?.programPath?.endsWith("impeller-7bl-xyzac.ngc")
|
||||
), 25000, "impeller program loaded");
|
||||
await waitForCanvasReady();
|
||||
await wait(1200);
|
||||
await captureStep("03-toolpath-preview", "刀具路径预览", "加载 test_linuxcnc_source/impeller-7bl-xyzac.ngc 对应程序,确认 canonical motion 预览路径、TCP 球和刀轴线可见。");
|
||||
|
||||
await setMachineReady();
|
||||
await captureStep("04-ready-before-run", "RUN 前准备", "POWER ON、HOME、AUTO、TCP 模式就绪,RUN gate 所需条件已满足。");
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="RUN"]').click());
|
||||
await waitForState((state) => (
|
||||
["running", "complete"].includes(state.runState) &&
|
||||
state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm"
|
||||
), 15000, "RUN feedback started");
|
||||
|
||||
const startedAt = Date.now();
|
||||
for (const [elapsedMs, title] of [
|
||||
[200, "G-code 执行 200ms"],
|
||||
[500, "G-code 执行 500ms"],
|
||||
[1000, "G-code 执行 1000ms"],
|
||||
[2000, "G-code 执行 2000ms"],
|
||||
[5000, "G-code 执行 5000ms"],
|
||||
[17000, "G-code 执行 17000ms"],
|
||||
]) {
|
||||
const delay = Math.max(startedAt + elapsedMs - Date.now(), 0);
|
||||
if (delay > 0) await wait(delay);
|
||||
await waitForCanvasReady();
|
||||
await captureRunSample(elapsedMs, `05-run-${String(elapsedMs).padStart(4, "0")}ms`, title);
|
||||
}
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="STOP"]').click());
|
||||
await waitForState((state) => state.taskHalStatusLoop?.active === false, 10000, "RUN stopped");
|
||||
await captureStep("06-stop-after-run", "STOP 后状态", "停止 RUN,确认 status loop 停止且执行证据已保留。");
|
||||
|
||||
addChecks();
|
||||
report.status = report.checks.every((check) => check.pass) ? "PASS" : "FAIL";
|
||||
|
||||
const jsonPath = path.join(OUTPUT_DIR, "test-linuxcnc-source-run-report.json");
|
||||
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
report.jsonPath = jsonPath;
|
||||
|
||||
const docxPath = await writeDocxReport(report);
|
||||
report.docxPath = docxPath;
|
||||
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
console.log(`test_linuxcnc_source_run_status=${report.status}`);
|
||||
console.log(`test_linuxcnc_source_run_json=${jsonPath}`);
|
||||
console.log(`test_linuxcnc_source_run_docx=${docxPath}`);
|
||||
} finally {
|
||||
await page.close().catch(() => {});
|
||||
await browser.close().catch(() => {});
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
|
||||
async function setMachineReady() {
|
||||
await page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
if (state.machine?.taskState !== "on") document.querySelector('[data-action="power"]').click();
|
||||
});
|
||||
await waitForState((state) => state.machine.taskState === "on", 10000, "machine power on");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
|
||||
await waitForState((state) => state.machine.mode === "manual", 10000, "manual mode");
|
||||
await page.evaluate(() => document.querySelector('[data-action="HOME"]').click());
|
||||
await waitForState((state) => state.machine.allHomed === true, 10000, "homed");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-auto"]').click());
|
||||
await waitForState((state) => state.machine.mode === "auto", 10000, "auto mode");
|
||||
await page.evaluate(() => document.querySelector('[data-action="kins-tcp"]').click());
|
||||
await waitForState((state) => state.rtcpState === "on" && state.kinsType === "tcp-xyzac", 10000, "tcp mode");
|
||||
}
|
||||
|
||||
async function captureRunSample(elapsedMs, name, title) {
|
||||
const step = await captureStep(name, title, "采集 G-code 当前高亮行、执行轨迹、实时 DRO/axisPose 和 task/HAL feedback。");
|
||||
const state = step.state;
|
||||
const sample = {
|
||||
elapsedMs,
|
||||
screenshotPath: step.screenshotPath,
|
||||
activeLine: state.activeLine,
|
||||
activeUiLine: state.activeUiLine,
|
||||
activeLineMatchesUi: state.activeLine === state.activeUiLine,
|
||||
runState: state.runState,
|
||||
axisPose: state.axisPose,
|
||||
dro: state.dro,
|
||||
droMatchesAxisPose: axesMatch(state.dro, state.programRuntimeFeedback?.axisPose),
|
||||
velocity: state.programRuntimeFeedback?.currentVelocityMmPerMin ?? null,
|
||||
expectedVelocity: state.currentTimingSegment?.velocityMmPerMin ?? null,
|
||||
feedRate: state.currentTimingSegment?.feedRate ?? null,
|
||||
feedMode: state.currentTimingSegment?.feedMode ?? null,
|
||||
segmentDurationSeconds: state.currentTimingSegment?.durationSeconds ?? null,
|
||||
distanceToGo: state.programRuntimeFeedback?.distanceToGo ?? null,
|
||||
feedbackSource: state.programRuntimeFeedback?.sourceMode ?? null,
|
||||
taskCycle: state.programRuntimeFeedback?.taskCycle ?? null,
|
||||
servoCycle: state.programRuntimeFeedback?.cycle ?? null,
|
||||
historyLength: state.programRuntimeFeedbackHistory?.length || 0,
|
||||
executedPathPoints: Number(step.dataset.threeExecutedPathPoints || 0),
|
||||
currentSegmentHighlight: step.dataset.threeCurrentSegmentHighlight,
|
||||
};
|
||||
report.samples.push(sample);
|
||||
return sample;
|
||||
}
|
||||
|
||||
async function captureStep(name, title, description) {
|
||||
const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
const dataset = await getCanvasDataset();
|
||||
const state = await getStateWithUi();
|
||||
const pixelStats = await analyzePng(screenshotPath);
|
||||
const step = {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
screenshotPath,
|
||||
dataset,
|
||||
pixelStats,
|
||||
state: summarizeState(state),
|
||||
};
|
||||
report.screenshots[name] = screenshotPath;
|
||||
report.steps.push(step);
|
||||
return step;
|
||||
}
|
||||
|
||||
function addChecks() {
|
||||
const finalState = report.steps.at(-1)?.state || {};
|
||||
const previewStep = report.steps.find((step) => step.name === "03-toolpath-preview");
|
||||
const runSamples = report.samples;
|
||||
const movingVelocities = runSamples
|
||||
.map((sample) => Number(sample.velocity || 0))
|
||||
.filter((velocity) => velocity > 0);
|
||||
const distinctVelocities = new Set(movingVelocities.map((velocity) => Math.round(velocity * 1000) / 1000));
|
||||
const sourceState = previewStep?.state || {};
|
||||
report.checks.push(
|
||||
check("test_linuxcnc_source INI used", sourceState.iniPath?.endsWith("xyzac-trt.ini"), sourceState.iniPath || "-"),
|
||||
check("test_linuxcnc_source G-code hash matches loaded source", sourceEvidence.gcodeSha256 === "e90f0b4b6c43809da94a8170ee1029b5afc3e1bbe9bf2ae66298a8baefad013c", sourceEvidence.gcodeSha256),
|
||||
check("刀具路径预览点可见", Number(previewStep?.dataset?.threePathPoints || 0) > 100, `pathPoints=${previewStep?.dataset?.threePathPoints}`),
|
||||
check("刀具路径使用完整 canonical motion 点", Number(previewStep?.dataset?.threePathPoints || 0) === Number(sourceState.programSummary?.motionEventCount || 0), `threePathPoints=${previewStep?.dataset?.threePathPoints}, motionEventCount=${sourceState.programSummary?.motionEventCount}`),
|
||||
check("TCP/刀轴线可见", previewStep?.dataset?.threeTcpMarker === "sphere" && previewStep?.dataset?.threeToolAxisMarker === "line", `tcp=${previewStep?.dataset?.threeTcpMarker}, axis=${previewStep?.dataset?.threeToolAxisMarker}`),
|
||||
check("RUN 采样数量", runSamples.length >= 5, `samples=${runSamples.length}`),
|
||||
check("每行执行高亮同步", runSamples.every((sample) => sample.activeLineMatchesUi), runSamples.map((sample) => `${sample.activeLine}/${sample.activeUiLine}`).join(", ")),
|
||||
check("执行轨迹可见", runSamples.every((sample) => sample.executedPathPoints >= 1 && sample.currentSegmentHighlight === "ok"), runSamples.map((sample) => `${sample.executedPathPoints}/${sample.currentSegmentHighlight}`).join(", ")),
|
||||
check("实时轴值来自 task/HAL feedback", runSamples.every((sample) => sample.feedbackSource === "linuxcnc-task-motion-hal-wasm" && sample.droMatchesAxisPose), runSamples.map((sample) => `${sample.feedbackSource}/${sample.droMatchesAxisPose}`).join(", ")),
|
||||
check("RUN feed 不是固定 3600 mm/min", movingVelocities.length > 0 && movingVelocities.every((velocity) => Math.abs(velocity - 3600) > 0.001), runSamples.map((sample) => `${sample.elapsedMs}ms=${sample.velocity}`).join(", ")),
|
||||
check("RUN feed 随实际 G-code F/G93 段变化", distinctVelocities.size >= 3, [...distinctVelocities].join(", ")),
|
||||
check("RUN 使用 G93 inverse-time feed", runSamples.some((sample) => sample.feedMode === "inverse-time" && Number(sample.segmentDurationSeconds || 0) > 0), runSamples.map((sample) => `${sample.elapsedMs}ms F${sample.feedRate} ${sample.feedMode} ${sample.segmentDurationSeconds}s`).join(", ")),
|
||||
check("task/HAL cycle 推进", runSamples.some((sample) => Number(sample.taskCycle || 0) > 0 && Number(sample.servoCycle || 0) > 0), runSamples.map((sample) => `${sample.taskCycle}/${sample.servoCycle}`).join(", ")),
|
||||
check("STOP 后 loop 停止", finalState.taskHalStatusLoop?.active === false, `active=${finalState.taskHalStatusLoop?.active}, stopReason=${finalState.taskHalStatusLoop?.stopReason}`),
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeState(state) {
|
||||
const currentTimingSegment = currentTimingSegmentForState(state);
|
||||
return {
|
||||
activeProgram: state.activeProgram,
|
||||
programSource: state.programSource,
|
||||
programExecutionSourceMode: state.programExecutionSourceMode,
|
||||
programSummary: state.programExecution?.summary || null,
|
||||
machineProfile: state.machineProfile,
|
||||
iniPath: state.iniConfigReadiness?.path || state.linuxCncIniConfig?.path || null,
|
||||
selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel || null,
|
||||
taskHalProgramPath: state.taskHalSession?.programPath || null,
|
||||
runState: state.runState,
|
||||
activeLine: state.activeLine,
|
||||
activeUiLine: state.__activeUiLine,
|
||||
machine: state.machine,
|
||||
rtcpState: state.rtcpState,
|
||||
kinsType: state.kinsType,
|
||||
dro: pickAxes(state.dro),
|
||||
axisPose: pickAxes(state.axisPose),
|
||||
programRuntimeFeedback: state.programRuntimeFeedback,
|
||||
currentTimingSegment,
|
||||
feedbackHistoryLength: state.programRuntimeFeedbackHistory?.length || 0,
|
||||
taskHalStatusLoop: state.taskHalStatusLoop,
|
||||
taskHalStatus: state.taskHalStatus ? {
|
||||
taskState: state.taskHalStatus.ui?.taskState || state.taskHalStatus.task?.state || null,
|
||||
taskMode: state.taskHalStatus.ui?.taskMode || state.taskHalStatus.task?.mode || null,
|
||||
interpState: state.taskHalStatus.ui?.interpState || state.taskHalStatus.task?.interpState || null,
|
||||
activeLine: state.taskHalStatus.ui?.activeLine || null,
|
||||
taskCycle: state.taskHalStatus.ui?.taskCycle || null,
|
||||
servoCycle: state.taskHalStatus.ui?.servoCycle || null,
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
|
||||
function currentTimingSegmentForState(state) {
|
||||
const activeLine = Number(state.activeLine || 0);
|
||||
const segments = state.programExecutionTiming?.segments || [];
|
||||
if (!Number.isFinite(activeLine) || !Array.isArray(segments) || segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return segments.find((segment) => Number(segment.line) === activeLine)
|
||||
|| [...segments].reverse().find((segment) => Number(segment.line) <= activeLine)
|
||||
|| null;
|
||||
}
|
||||
|
||||
async function wait(ms) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitForState(predicate, timeoutMs, label) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const state = await getStateWithUi();
|
||||
if (predicate(state)) return state;
|
||||
await wait(100);
|
||||
}
|
||||
throw new Error(`timeout waiting for ${label}`);
|
||||
}
|
||||
|
||||
async function waitForCanvasReady(timeoutMs = 20000) {
|
||||
await page.waitForFunction(() => {
|
||||
const canvas = document.querySelector("[data-five-axis-canvas]");
|
||||
return canvas?.dataset?.threeReady === "true"
|
||||
&& canvas?.dataset?.threePreviewScope === "machine-reference-and-toolpath";
|
||||
}, { timeout: timeoutMs });
|
||||
}
|
||||
|
||||
async function getStateWithUi() {
|
||||
return page.evaluate(() => {
|
||||
const state = JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState()));
|
||||
state.__activeUiLine = Number(document.querySelector(".gcode-row.active")?.dataset.programLine || 0);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
async function getCanvasDataset() {
|
||||
return page.$eval("[data-five-axis-canvas]", (canvas) => ({ ...canvas.dataset }));
|
||||
}
|
||||
|
||||
function createStaticServer(rootDir) {
|
||||
return http.createServer(async (request, response) => {
|
||||
try {
|
||||
const requestPath = decodeURIComponent(new URL(request.url || "/", "http://127.0.0.1").pathname);
|
||||
const relativePath = requestPath === "/" ? "/index.html" : requestPath;
|
||||
const targetPath = path.resolve(rootDir, `.${relativePath}`);
|
||||
if (!targetPath.startsWith(rootDir)) {
|
||||
response.writeHead(403);
|
||||
response.end("forbidden");
|
||||
return;
|
||||
}
|
||||
let filePath = targetPath;
|
||||
let stat = await fs.stat(filePath).catch(() => null);
|
||||
if (stat?.isDirectory()) {
|
||||
filePath = path.join(filePath, "index.html");
|
||||
stat = await fs.stat(filePath).catch(() => null);
|
||||
}
|
||||
if (!stat?.isFile()) {
|
||||
response.writeHead(404);
|
||||
response.end("not found");
|
||||
return;
|
||||
}
|
||||
const body = await fs.readFile(filePath);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": contentTypeFor(filePath),
|
||||
"Content-Length": String(body.byteLength),
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
response.end(body);
|
||||
} catch (error) {
|
||||
response.writeHead(500);
|
||||
response.end(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function contentTypeFor(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return {
|
||||
".css": "text/css; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".wasm": "application/wasm",
|
||||
".xml": "application/xml; charset=utf-8",
|
||||
}[ext] || "application/octet-stream";
|
||||
}
|
||||
|
||||
async function analyzePng(filePath) {
|
||||
const png = PNG.sync.read(await fs.readFile(filePath));
|
||||
let luminanceSum = 0;
|
||||
let nonBlack = 0;
|
||||
for (let index = 0; index < png.data.length; index += 4) {
|
||||
const luminance = png.data[index] * 0.2126 + png.data[index + 1] * 0.7152 + png.data[index + 2] * 0.0722;
|
||||
luminanceSum += luminance;
|
||||
if (luminance > 8) nonBlack += 1;
|
||||
}
|
||||
const total = png.width * png.height;
|
||||
return {
|
||||
width: png.width,
|
||||
height: png.height,
|
||||
averageLuminance: Number((luminanceSum / total).toFixed(2)),
|
||||
nonBlackRatio: Number((nonBlack / total).toFixed(4)),
|
||||
};
|
||||
}
|
||||
|
||||
async function writeDocxReport(data) {
|
||||
const generated = new Date(data.generatedAt);
|
||||
const ymd = generated.toISOString().slice(0, 10);
|
||||
const docxPath = path.join(OUTPUT_DIR, `test-linuxcnc-source-run-report-${ymd}.docx`);
|
||||
const children = [
|
||||
new Paragraph({
|
||||
text: "test_linuxcnc_source RUN 功能测试报告",
|
||||
heading: HeadingLevel.TITLE,
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
centered(`测试对象:${data.targetUrl}`),
|
||||
centered(`生成时间:${formatDateTime(generated)}`),
|
||||
blank(),
|
||||
heading("1. 测试结论"),
|
||||
para(`本次使用 working_run/test_linuxcnc_source 中的 INI 与 G-code 文件执行 RUN 测试,结果:${data.status}。`),
|
||||
checksTable(data.checks),
|
||||
heading("2. 测试源文件"),
|
||||
kvTable([
|
||||
["INI", data.sourceEvidence.iniPath],
|
||||
["INI SHA-256", data.sourceEvidence.iniSha256],
|
||||
["G-code", data.sourceEvidence.gcodePath],
|
||||
["G-code SHA-256", data.sourceEvidence.gcodeSha256],
|
||||
["G-code 行数/字节", `${data.sourceEvidence.gcodeLineCount} / ${data.sourceEvidence.gcodeBytes}`],
|
||||
]),
|
||||
heading("3. 测试过程"),
|
||||
];
|
||||
|
||||
for (const step of data.steps) {
|
||||
children.push(...(await stepBlock(step)));
|
||||
}
|
||||
|
||||
children.push(
|
||||
heading("4. RUN 采样明细"),
|
||||
sampleTable(data.samples),
|
||||
heading("5. 原始证据"),
|
||||
kvTable([
|
||||
["JSON", data.jsonPath || path.join(OUTPUT_DIR, "test-linuxcnc-source-run-report.json")],
|
||||
["截图目录", SCREENSHOT_DIR],
|
||||
["Word", docxPath],
|
||||
["Console error", String(data.consoleErrors.length)],
|
||||
["Chrome", data.chromePath],
|
||||
]),
|
||||
);
|
||||
|
||||
if (data.consoleErrors.length > 0) {
|
||||
children.push(heading("6. Console Error"), ...data.consoleErrors.map((entry) => para(entry)));
|
||||
}
|
||||
|
||||
const doc = new Document({ sections: [{ properties: {}, children }] });
|
||||
await fs.writeFile(docxPath, await Packer.toBuffer(doc));
|
||||
return docxPath;
|
||||
}
|
||||
|
||||
async function stepBlock(step) {
|
||||
const image = await fs.readFile(step.screenshotPath);
|
||||
return [
|
||||
new Paragraph({
|
||||
text: `${step.name} - ${step.title}`,
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
spacing: { before: 180, after: 120 },
|
||||
}),
|
||||
para(step.description),
|
||||
kvTable([
|
||||
["activeProgram", step.state.activeProgram],
|
||||
["runState / activeLine", `${step.state.runState} / ${step.state.activeLine}`],
|
||||
["program source", `${step.state.programSource} / ${step.state.programExecutionSourceMode}`],
|
||||
["selected G-code", step.state.selectedGcodeSourceRel],
|
||||
["task/HAL program", step.state.taskHalProgramPath],
|
||||
["DRO XYZAC", axesText(step.state.dro)],
|
||||
["axisPose XYZAC", axesText(step.state.axisPose)],
|
||||
["path/executed", `${step.dataset.threePathPoints || 0} / ${step.dataset.threeExecutedPathPoints || 0}`],
|
||||
["RTCP / kins", `${step.state.rtcpState} / ${step.state.kinsType}`],
|
||||
["pixel", `luma=${step.pixelStats.averageLuminance}, nonBlack=${step.pixelStats.nonBlackRatio}`],
|
||||
]),
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new ImageRun({
|
||||
data: image,
|
||||
type: "png",
|
||||
transformation: { width: 520, height: 390 },
|
||||
})],
|
||||
}),
|
||||
para(`截图文件:${step.screenshotPath}`),
|
||||
];
|
||||
}
|
||||
|
||||
function sampleTable(samples) {
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: ["时间", "行号", "状态", "DRO XYZAC", "速度/feed", "轨迹点", "反馈"].map((text) => cell(text, true)),
|
||||
}),
|
||||
...samples.map((sample) => new TableRow({
|
||||
children: [
|
||||
cell(`${sample.elapsedMs}ms`),
|
||||
cell(`${sample.activeLine} / UI ${sample.activeUiLine}`),
|
||||
cell(sample.runState),
|
||||
cell(axesText(sample.dro)),
|
||||
cell(`${sample.velocity ?? "-"} mm/min\nF${sample.feedRate ?? "-"} ${sample.feedMode ?? "-"}`),
|
||||
cell(String(sample.executedPathPoints)),
|
||||
cell(`${sample.feedbackSource}\ncycle=${sample.taskCycle}/${sample.servoCycle}`),
|
||||
],
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function checksTable(checks) {
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({ children: [cell("检查项", true), cell("结果", true), cell("证据", true)] }),
|
||||
...checks.map((item) => new TableRow({
|
||||
children: [cell(item.name), cell(item.pass ? "PASS" : "FAIL"), cell(item.detail)],
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function kvTable(rows) {
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({ children: [cell("项目", true), cell("内容", true)] }),
|
||||
...rows.map(([key, value]) => new TableRow({ children: [cell(key), cell(value)] })),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function centered(text) {
|
||||
return new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun(String(text))] });
|
||||
}
|
||||
|
||||
function heading(text) {
|
||||
return new Paragraph({ text, heading: HeadingLevel.HEADING_1, spacing: { before: 240, after: 120 } });
|
||||
}
|
||||
|
||||
function blank() {
|
||||
return new Paragraph({ text: "" });
|
||||
}
|
||||
|
||||
function para(text) {
|
||||
return new Paragraph({ children: [new TextRun(String(text ?? "-"))], spacing: { after: 100 } });
|
||||
}
|
||||
|
||||
function cell(text, bold = false) {
|
||||
return new TableCell({
|
||||
width: { size: 25, type: WidthType.PERCENTAGE },
|
||||
children: String(text ?? "-").split("\n").map((line) => new Paragraph({
|
||||
children: [new TextRun({ text: line, bold })],
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
function check(name, pass, detail) {
|
||||
return { name, pass: Boolean(pass), detail: String(detail ?? "-") };
|
||||
}
|
||||
|
||||
function pickAxes(value = {}) {
|
||||
return {
|
||||
x: Number(value.x || 0),
|
||||
y: Number(value.y || 0),
|
||||
z: Number(value.z || 0),
|
||||
a: Number(value.a || 0),
|
||||
b: Number(value.b || 0),
|
||||
c: Number(value.c || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function axesMatch(left = {}, right = {}) {
|
||||
return ["x", "y", "z", "a", "b", "c"].every((axis) => Math.abs(Number(left?.[axis] || 0) - Number(right?.[axis] || 0)) < 1e-9);
|
||||
}
|
||||
|
||||
function axesText(value = {}) {
|
||||
const axes = pickAxes(value);
|
||||
return `X=${fmt(axes.x)} Y=${fmt(axes.y)} Z=${fmt(axes.z)} A=${fmt(axes.a)} B=${fmt(axes.b)} C=${fmt(axes.c)}`;
|
||||
}
|
||||
|
||||
function fmt(value) {
|
||||
return Number(value || 0).toFixed(3);
|
||||
}
|
||||
|
||||
function sha256(text) {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
function formatDateTime(date) {
|
||||
return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
|
||||
}
|
||||
@@ -8,11 +8,15 @@ const REPO_ROOT = path.resolve("/home/meswork/cnc_wams");
|
||||
const QA_ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
|
||||
const OUTPUT_DIR = path.join(QA_ROOT, "output");
|
||||
const SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "toolpath-preview-cases");
|
||||
const RUN_FEEDBACK_SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "run-preconditions-feedback");
|
||||
const METER_SCENE_SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "meter-scene-evidence");
|
||||
const CHROME_PATH = process.env.CHROME_PATH || "/usr/bin/google-chrome";
|
||||
const FIXTURE_URL = "/web-rtcp-5axis-sim-plan/app/index.html";
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
await fs.mkdir(RUN_FEEDBACK_SCREENSHOT_DIR, { recursive: true });
|
||||
await fs.mkdir(METER_SCENE_SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
const MIME_TYPES = {
|
||||
".css": "text/css; charset=utf-8",
|
||||
@@ -144,6 +148,22 @@ async function captureCase(name) {
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function captureRunFeedbackFrame(name) {
|
||||
const screenshotPath = path.join(RUN_FEEDBACK_SCREENSHOT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function captureMeterSceneFrame(name) {
|
||||
const screenshotPath = path.join(METER_SCENE_SCREENSHOT_DIR, `${name}.png`);
|
||||
const canvas = await page.$("[data-five-axis-canvas]");
|
||||
if (!canvas) {
|
||||
throw new Error(`missing canvas for ${name}`);
|
||||
}
|
||||
await canvas.screenshot({ path: screenshotPath });
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function recordCase(name, summary, checks = []) {
|
||||
const dataset = await getCanvasDataset();
|
||||
const state = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
|
||||
@@ -211,6 +231,44 @@ function previewChecks(dataset, {
|
||||
return checks;
|
||||
}
|
||||
|
||||
function meterSceneChecks(dataset, pixelStats = null) {
|
||||
const bounds = parseJson(dataset.threePathBoundsMeters);
|
||||
return [
|
||||
check("scene units are meters", dataset.threeSceneUnits === "m", `sceneUnits=${dataset.threeSceneUnits}`),
|
||||
check("linear scale visible", Number(dataset.threeLinearUnitScaleToMeters || 0) > 0, `scale=${dataset.threeLinearUnitScaleToMeters}`),
|
||||
check("path fit bounds", dataset.threePathFitBounds === "ok", `fit=${dataset.threePathFitBounds}`),
|
||||
check("path bounds in meters", Number(bounds?.maxSpan || 0) > 0 && Number(bounds?.maxSpan || 0) < 5, `bounds=${dataset.threePathBoundsMeters}`),
|
||||
check("canvas nonblank", !pixelStats || pixelStats.nonBlackRatio > 0.015, `nonBlack=${pixelStats?.nonBlackRatio ?? "-"}`),
|
||||
];
|
||||
}
|
||||
|
||||
function mixedUnitsChecks(dataset, state) {
|
||||
const motion = state.programExecution?.motion || [];
|
||||
const units = [...new Set(motion.map((event) => event.linearUnits).filter(Boolean))];
|
||||
const xMeters = motion.map((event) => sceneMeterX(event)).filter(Number.isFinite);
|
||||
return [
|
||||
check("G20/G21 motion units", units.includes("inch") && units.includes("mm"), `units=${units.join(",")}`),
|
||||
check("scene units are meters", dataset.threeSceneUnits === "m", `sceneUnits=${dataset.threeSceneUnits}`),
|
||||
check("mixed unit path visible", Number(dataset.threePathPoints || 0) >= 2, `pathPoints=${dataset.threePathPoints}`),
|
||||
check("1 inch equals 25.4 mm in scene", xMeters.some((value) => Math.abs(value - 0.0254) < 1e-9), `xMeters=${xMeters.join(",")}`),
|
||||
check("no unit fallback in canonical motion", state.programExecutionSourceMode === "linuxcnc-interpreter-wasm", `source=${state.programExecutionSourceMode}`),
|
||||
];
|
||||
}
|
||||
|
||||
function sceneMeterX(event) {
|
||||
const factor = event?.linearUnits === "inch" ? 0.0254 : event?.linearUnits === "m" ? 1 : 0.001;
|
||||
const value = Number(event?.axes?.x);
|
||||
return Number.isFinite(value) ? value * factor : null;
|
||||
}
|
||||
|
||||
function parseJson(text) {
|
||||
try {
|
||||
return JSON.parse(text || "null");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function check(name, pass, detail) {
|
||||
return { name, pass: Boolean(pass), detail };
|
||||
}
|
||||
@@ -222,6 +280,111 @@ function assertChecks(caseName, checks) {
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeRunFeedbackState(state, dataset, elapsedMs, screenshotPath) {
|
||||
const activeRowLine = Number(documentActiveLineFromState(state));
|
||||
return {
|
||||
elapsedMs,
|
||||
screenshotPath,
|
||||
canvas: {
|
||||
threeReady: dataset.threeReady,
|
||||
rtcpState: dataset.threeRtcpState,
|
||||
executedPathPoints: Number(dataset.threeExecutedPathPoints || 0),
|
||||
currentSegmentHighlight: dataset.threeCurrentSegmentHighlight,
|
||||
toolExecutionTraceSource: dataset.threeToolExecutionTraceSource,
|
||||
},
|
||||
state: {
|
||||
profileId: state.machineProfile,
|
||||
iniReady: state.iniConfigReadiness?.ready === true,
|
||||
iniPath: state.iniConfigReadiness?.path || null,
|
||||
coordinates: state.iniConfigReadiness?.coordinates || null,
|
||||
kinematicsModuleId: state.profile?.kinematicsModuleId || null,
|
||||
selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel || null,
|
||||
taskHalSessionProgramPath: state.taskHalSession?.programPath || null,
|
||||
taskHalStatusLoop: state.taskHalStatusLoop || null,
|
||||
runState: state.runState,
|
||||
activeLine: state.activeLine,
|
||||
activeRowLine,
|
||||
activeLineMatchesUi: activeRowLine === Number(state.activeLine),
|
||||
droAxisPose: state.dro ? {
|
||||
x: state.dro.x,
|
||||
y: state.dro.y,
|
||||
z: state.dro.z,
|
||||
a: state.dro.a,
|
||||
b: state.dro.b,
|
||||
c: state.dro.c,
|
||||
} : null,
|
||||
feedbackAxisPose: state.programRuntimeFeedback?.axisPose || null,
|
||||
droMatchesFeedback: axisPoseMatchesDro(state.dro, state.programRuntimeFeedback?.axisPose),
|
||||
rtcpState: state.rtcpState,
|
||||
kinsType: state.kinsType,
|
||||
feedback: state.programRuntimeFeedback ? {
|
||||
sourceMode: state.programRuntimeFeedback.sourceMode,
|
||||
semanticBoundary: state.programRuntimeFeedback.semanticBoundary,
|
||||
line: state.programRuntimeFeedback.line,
|
||||
taskCycle: state.programRuntimeFeedback.taskCycle,
|
||||
servoCycle: state.programRuntimeFeedback.cycle,
|
||||
velocity: state.programRuntimeFeedback.currentVelocityMmPerMin,
|
||||
distanceToGo: state.programRuntimeFeedback.distanceToGo,
|
||||
} : null,
|
||||
feedbackHistoryLength: state.programRuntimeFeedbackHistory?.length || 0,
|
||||
feedbackHistorySourceModes: [...new Set((state.programRuntimeFeedbackHistory || []).map((entry) => entry.sourceMode))],
|
||||
taskHalStatus: state.taskHalStatus ? {
|
||||
taskState: state.taskHalStatus.ui?.taskState || state.taskHalStatus.task?.state || null,
|
||||
taskMode: state.taskHalStatus.ui?.taskMode || state.taskHalStatus.task?.mode || null,
|
||||
interpState: state.taskHalStatus.ui?.interpState || state.taskHalStatus.task?.interpState || null,
|
||||
taskCycle: state.taskHalStatus.ui?.taskCycle || null,
|
||||
servoCycle: state.taskHalStatus.ui?.servoCycle || null,
|
||||
} : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function documentActiveLineFromState(state) {
|
||||
return state.__activeRowLine ?? null;
|
||||
}
|
||||
|
||||
function axisPoseMatchesDro(dro, axisPose) {
|
||||
if (!dro || !axisPose) return false;
|
||||
return ["x", "y", "z", "a", "b", "c"].every((axis) => (
|
||||
Math.abs(Number(dro[axis] || 0) - Number(axisPose[axis] || 0)) < 1e-9
|
||||
));
|
||||
}
|
||||
|
||||
async function snapshotRunFeedback(elapsedMs, screenshotName) {
|
||||
const screenshotPath = await captureRunFeedbackFrame(screenshotName);
|
||||
const dataset = await getCanvasDataset();
|
||||
const state = await page.evaluate(() => {
|
||||
const snapshot = JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState()));
|
||||
snapshot.__activeRowLine = Number(document.querySelector(".gcode-row.active")?.dataset.programLine || 0);
|
||||
return snapshot;
|
||||
});
|
||||
return summarizeRunFeedbackState(state, dataset, elapsedMs, screenshotPath);
|
||||
}
|
||||
|
||||
function runFeedbackChecks(samples, baseline = null) {
|
||||
const states = samples.map((sample) => sample.state);
|
||||
const histories = states.map((state) => Number(state.feedbackHistoryLength || 0));
|
||||
const ticks = states.map((state) => Number(state.taskHalStatusLoop?.tickCount || 0));
|
||||
const baselineSequence = Number(baseline?.state?.taskHalStatusLoop?.sequence || 0);
|
||||
const sourceModes = states.flatMap((state) => state.feedbackHistorySourceModes || []);
|
||||
return [
|
||||
check("INI ready", states.every((state) => state.iniReady), `iniReady=${states.map((state) => state.iniReady).join(",")}`),
|
||||
check("selected LinuxCNC G-code", states.every((state) => state.selectedGcodeSourceRel?.endsWith("xyzac_switchkins_test_1.ngc")), states.at(-1)?.selectedGcodeSourceRel || "-"),
|
||||
check("task/HAL session opened selected G-code", states.every((state) => state.taskHalSessionProgramPath?.endsWith("xyzac_switchkins_test_1.ngc")), states.at(-1)?.taskHalSessionProgramPath || "-"),
|
||||
check("taskHalStatusLoop 新 RUN sequence", states.every((state) => Number(state.taskHalStatusLoop?.sequence || 0) > baselineSequence), `baselineSequence=${baselineSequence}, sequences=${states.map((state) => state.taskHalStatusLoop?.sequence || 0).join(",")}`),
|
||||
check("taskHalStatusLoop 本次 RUN tickCount", Math.max(...ticks) >= 3, `ticks=${ticks.join(",")}`),
|
||||
check("programRuntimeFeedbackHistory 本次 RUN 采样", Math.max(...histories) >= 3, `history=${histories.join(",")}`),
|
||||
check("feedback sourceMode 来自 task/HAL", sourceModes.length > 0 && sourceModes.every((mode) => mode === "linuxcnc-task-motion-hal-wasm"), `sourceModes=${sourceModes.join(",")}`),
|
||||
check("无 fixture-line-playback feedback", !sourceModes.includes("fixture-line-playback"), `sourceModes=${sourceModes.join(",")}`),
|
||||
check("semantic boundary", states.every((state) => state.feedback?.semanticBoundary === "linuxcnc_task_motion_hal_wasm_simulation_runtime"), states.map((state) => state.feedback?.semanticBoundary || "-").join(",")),
|
||||
check("activeLine 等于 UI 高亮行", states.every((state) => state.activeLineMatchesUi), `active=${states.map((state) => `${state.activeLine}/${state.activeRowLine}`).join(",")}`),
|
||||
check("DRO 等于 runtime feedback axisPose", states.every((state) => state.droMatchesFeedback), "droMatchesFeedback=true for all samples"),
|
||||
check("RTCP canvas 与 state 一致", samples.every((sample) => sample.canvas.rtcpState === sample.state.rtcpState), samples.map((sample) => `${sample.canvas.rtcpState}/${sample.state.rtcpState}`).join(",")),
|
||||
check("runtime cycle 可见", states.every((state) => Number(state.feedback?.taskCycle || 0) > 0 && Number(state.feedback?.servoCycle || 0) > 0), states.map((state) => `${state.feedback?.taskCycle || 0}/${state.feedback?.servoCycle || 0}`).join(",")),
|
||||
check("canvas 执行轨迹", samples.every((sample) => sample.canvas.threeReady === "true" && sample.canvas.currentSegmentHighlight === "ok" && sample.canvas.executedPathPoints >= 1), samples.map((sample) => `${sample.canvas.threeReady}/${sample.canvas.executedPathPoints}/${sample.canvas.currentSegmentHighlight}`).join(",")),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
await page.goto(`${baseUrl}${FIXTURE_URL}`, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
|
||||
@@ -303,6 +466,92 @@ try {
|
||||
await captureCase("05-vendored-impeller-toolpath");
|
||||
await recordCase("05-vendored-impeller-toolpath", "LinuxCNC vendored 五轴 impeller 程序:验证长路径、switchkins/RTCP 状态、rapid/feed 图层和 TCP 执行轨迹", checks);
|
||||
|
||||
const meterSceneSamples = [];
|
||||
for (const viewport of [
|
||||
{ name: "desktop", width: 1600, height: 1200, deviceScaleFactor: 1 },
|
||||
{ name: "mobile", width: 390, height: 844, deviceScaleFactor: 2 },
|
||||
]) {
|
||||
await page.setViewport(viewport);
|
||||
await waitForCanvasReady();
|
||||
await wait(600);
|
||||
const screenshotPath = await captureMeterSceneFrame(`08-meter-scene-${viewport.name}`);
|
||||
const sampleDataset = await getCanvasDataset();
|
||||
const pixelStats = await analyzePng(screenshotPath);
|
||||
const sampleChecks = [
|
||||
...previewChecks(sampleDataset, { requirePathPoints: true, requireExecutedPath: true }),
|
||||
...meterSceneChecks(sampleDataset, pixelStats),
|
||||
];
|
||||
assertChecks(`08-meter-scene-${viewport.name}`, sampleChecks);
|
||||
meterSceneSamples.push({
|
||||
viewport,
|
||||
screenshotPath,
|
||||
pixelStats,
|
||||
dataset: sampleDataset,
|
||||
status: sampleChecks.every((item) => item.pass) ? "PASS" : "FAIL",
|
||||
checks: sampleChecks,
|
||||
});
|
||||
}
|
||||
await page.setViewport({ width: 1600, height: 1200, deviceScaleFactor: 1 });
|
||||
const meterSceneChecksAll = meterSceneSamples.flatMap((sample) => sample.checks);
|
||||
const meterSceneReport = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: report.targetUrl,
|
||||
caseName: "08-meter-scene-desktop-mobile",
|
||||
summary: "浏览器米尺度 Three.js 证据:desktop/mobile canvas 非空、路径 bounds 为米、预览稳定居中",
|
||||
status: meterSceneChecksAll.every((item) => item.pass) ? "PASS" : "FAIL",
|
||||
samples: meterSceneSamples,
|
||||
consoleErrors,
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "meter-scene-evidence.json"),
|
||||
`${JSON.stringify(meterSceneReport, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
report.cases.push({
|
||||
name: "08-meter-scene-desktop-mobile",
|
||||
summary: meterSceneReport.summary,
|
||||
status: meterSceneReport.status,
|
||||
screenshotDir: METER_SCENE_SCREENSHOT_DIR,
|
||||
output: path.join(OUTPUT_DIR, "meter-scene-evidence.json"),
|
||||
sampleCount: meterSceneSamples.length,
|
||||
});
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.webRtcp5AxisSimulation.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "operator-g20-g21-mixed-units.ngc",
|
||||
content: [
|
||||
"G90 G20",
|
||||
"G1 X1.0 Y0 Z0 F10",
|
||||
"G21",
|
||||
"G1 X25.4 Y25.4 Z0 F254",
|
||||
"M2",
|
||||
].join("\n"),
|
||||
});
|
||||
});
|
||||
await waitForState((state) => (
|
||||
state.activeProgram === "operator-g20-g21-mixed-units.ngc" &&
|
||||
state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" &&
|
||||
(state.programExecution?.motion || []).some((event) => event.linearUnits === "inch") &&
|
||||
(state.programExecution?.motion || []).some((event) => event.linearUnits === "mm")
|
||||
), 20000, "G20/G21 mixed unit program loaded");
|
||||
await waitForCanvasReady();
|
||||
await wait(600);
|
||||
dataset = await getCanvasDataset();
|
||||
const mixedUnitsState = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
|
||||
checks = [
|
||||
...previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true }),
|
||||
...mixedUnitsChecks(dataset, mixedUnitsState),
|
||||
];
|
||||
assertChecks("09-g20-g21-mixed-units-preview", checks);
|
||||
await captureCase("09-g20-g21-mixed-units-preview");
|
||||
await recordCase("09-g20-g21-mixed-units-preview", "G20/G21 混合单位程序:验证 canonical motion 保留 inch/mm,Three.js 统一按米绘制", checks);
|
||||
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc");
|
||||
await waitForState((state) => state.activeProgram?.endsWith("impeller-7bl-xyzac.ngc"), 25000, "vendored impeller restored before run");
|
||||
await waitForCanvasReady();
|
||||
await wait(800);
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="power"]').click());
|
||||
await waitForState((state) => state.machine.taskState === "on", 10000, "machine powered on");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
|
||||
@@ -323,6 +572,88 @@ try {
|
||||
await captureCase("06-running-rtcp-toolpath");
|
||||
await recordCase("06-running-rtcp-toolpath", "G-code 运行态:验证 task/HAL runtime feedback 驱动执行轨迹、当前段高亮、TCP 球和刀轴线跟随", checks);
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="STOP"]').click());
|
||||
await waitForState((state) => state.taskHalStatusLoop?.active === false, 10000, "previous run stopped");
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc");
|
||||
await waitForState((state) => (
|
||||
state.activeProgram?.endsWith("xyzac_switchkins_test_1.ngc") &&
|
||||
state.taskHalSession?.programPath?.endsWith("xyzac_switchkins_test_1.ngc") &&
|
||||
state.iniConfigReadiness?.ready === true &&
|
||||
state.profile?.kinematicsModuleId === "xyzac-trt"
|
||||
), 25000, "run feedback selected program ready");
|
||||
await waitForCanvasReady();
|
||||
await page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
if (state.machine?.taskState !== "on") {
|
||||
document.querySelector('[data-action="power"]').click();
|
||||
}
|
||||
});
|
||||
await waitForState((state) => state.machine.taskState === "on", 10000, "machine powered on for run feedback");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
|
||||
await waitForState((state) => state.machine.mode === "manual", 10000, "manual mode selected for run feedback");
|
||||
await page.evaluate(() => document.querySelector('[data-action="HOME"]').click());
|
||||
await waitForState((state) => state.machine.allHomed === true, 10000, "machine homed for run feedback");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-auto"]').click());
|
||||
await waitForState((state) => state.machine.mode === "auto", 10000, "auto mode selected for run feedback");
|
||||
const runFeedbackBaseline = await snapshotRunFeedback(0, "07-run-preconditions-and-feedback-0000ms-before-run");
|
||||
await page.evaluate(() => document.querySelector('[data-action="RUN"]').click());
|
||||
try {
|
||||
await waitForState((state) => (
|
||||
["running", "complete"].includes(state.runState) &&
|
||||
(state.taskHalStatusLoop?.tickCount > runFeedbackBaseline.state.taskHalStatusLoop.tickCount ||
|
||||
(state.programRuntimeFeedbackHistory?.length || 0) >= 3) &&
|
||||
state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm"
|
||||
), 15000, "run feedback status loop produced feedback");
|
||||
} catch (error) {
|
||||
const failureSnapshot = await snapshotRunFeedback(15000, "07-run-preconditions-and-feedback-timeout");
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "run-preconditions-feedback-timeout.json"),
|
||||
`${JSON.stringify({
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
baseline: runFeedbackBaseline,
|
||||
failureSnapshot,
|
||||
consoleErrors,
|
||||
}, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
const runFeedbackStartedAt = Date.now();
|
||||
const runFeedbackSamples = [];
|
||||
for (const elapsedMs of [200, 500, 1000, 2000, 5000]) {
|
||||
const waitMs = Math.max(runFeedbackStartedAt + elapsedMs - Date.now(), 0);
|
||||
if (waitMs > 0) await wait(waitMs);
|
||||
await waitForCanvasReady();
|
||||
runFeedbackSamples.push(await snapshotRunFeedback(elapsedMs, `07-run-preconditions-and-feedback-${elapsedMs}ms`));
|
||||
}
|
||||
const runFeedbackCaseChecks = runFeedbackChecks(runFeedbackSamples, runFeedbackBaseline);
|
||||
assertChecks("07-run-preconditions-and-feedback", runFeedbackCaseChecks);
|
||||
const runFeedbackReport = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: report.targetUrl,
|
||||
caseName: "07-run-preconditions-and-feedback",
|
||||
summary: "浏览器 RUN 证据:INI/profile/kinematics/task-HAL 同一上下文,taskHalStatusLoop 持续推进,UI 消费 task/HAL/motion feedback",
|
||||
status: runFeedbackCaseChecks.every((item) => item.pass) ? "PASS" : "FAIL",
|
||||
checks: runFeedbackCaseChecks,
|
||||
baseline: runFeedbackBaseline,
|
||||
samples: runFeedbackSamples,
|
||||
consoleErrors,
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "run-preconditions-feedback.json"),
|
||||
`${JSON.stringify(runFeedbackReport, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
report.cases.push({
|
||||
name: "07-run-preconditions-and-feedback",
|
||||
summary: runFeedbackReport.summary,
|
||||
status: runFeedbackReport.status,
|
||||
checks: runFeedbackCaseChecks,
|
||||
screenshotDir: RUN_FEEDBACK_SCREENSHOT_DIR,
|
||||
output: path.join(OUTPUT_DIR, "run-preconditions-feedback.json"),
|
||||
sampleCount: runFeedbackSamples.length,
|
||||
});
|
||||
|
||||
report.consoleErrors = consoleErrors;
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "toolpath-preview-cases.json"),
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
{
|
||||
"generatedAt": "2026-06-23T01:38:37.231Z",
|
||||
"url": "http://127.0.0.1:35283/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"sourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
|
||||
"activeLines": [
|
||||
9,
|
||||
10,
|
||||
12
|
||||
],
|
||||
"historyLines": [
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12
|
||||
],
|
||||
"synced": true,
|
||||
"samples": [
|
||||
{
|
||||
"elapsedMs": 100,
|
||||
"activeLine": 9,
|
||||
"uiLine": 9,
|
||||
"runState": "running",
|
||||
"velocity": 2100,
|
||||
"line": 9,
|
||||
"motionProgramLine": 9,
|
||||
"halProgramLine": 9,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 250,
|
||||
"activeLine": 10,
|
||||
"uiLine": 10,
|
||||
"runState": "running",
|
||||
"velocity": 2100,
|
||||
"line": 10,
|
||||
"motionProgramLine": 10,
|
||||
"halProgramLine": 10,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 500,
|
||||
"activeLine": 10,
|
||||
"uiLine": 10,
|
||||
"runState": "running",
|
||||
"velocity": 2100,
|
||||
"line": 10,
|
||||
"motionProgramLine": 10,
|
||||
"halProgramLine": 10,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 1000,
|
||||
"activeLine": 12,
|
||||
"uiLine": 12,
|
||||
"runState": "running",
|
||||
"velocity": 100.0002,
|
||||
"line": 12,
|
||||
"motionProgramLine": 12,
|
||||
"halProgramLine": 12,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 11,
|
||||
"motion": 11,
|
||||
"hal": 11,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 2000,
|
||||
"activeLine": 12,
|
||||
"uiLine": 12,
|
||||
"runState": "running",
|
||||
"velocity": 100.0002,
|
||||
"line": 12,
|
||||
"motionProgramLine": 12,
|
||||
"halProgramLine": 12,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 11,
|
||||
"motion": 11,
|
||||
"hal": 11,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 10,
|
||||
"motion": 10,
|
||||
"hal": 10,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"motion": 9,
|
||||
"hal": 9,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"elapsedMs": 4000,
|
||||
"activeLine": 12,
|
||||
"uiLine": 12,
|
||||
"runState": "running",
|
||||
"velocity": 100.0002,
|
||||
"line": 12,
|
||||
"motionProgramLine": 12,
|
||||
"halProgramLine": 12,
|
||||
"activeLineSource": "motion-status",
|
||||
"activeLineHalSynced": true,
|
||||
"history": [
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"line": 12,
|
||||
"motion": 12,
|
||||
"hal": 12,
|
||||
"source": "motion-status",
|
||||
"synced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"final": {
|
||||
"runState": "running",
|
||||
"activeLine": 12,
|
||||
"feedbackHistoryLength": 24
|
||||
},
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)",
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
]
|
||||
}
|
||||
267
qa/web-rtcp-5axis-site-test/output/meter-scene-evidence.json
Normal file
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"generatedAt": "2026-06-22T22:09:41.825Z",
|
||||
"targetUrl": "http://127.0.0.1:45757/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"caseName": "08-meter-scene-desktop-mobile",
|
||||
"summary": "浏览器米尺度 Three.js 证据:desktop/mobile canvas 非空、路径 bounds 为米、预览稳定居中",
|
||||
"status": "PASS",
|
||||
"samples": [
|
||||
{
|
||||
"viewport": {
|
||||
"name": "desktop",
|
||||
"width": 1600,
|
||||
"height": 1200,
|
||||
"deviceScaleFactor": 1
|
||||
},
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/meter-scene-evidence/08-meter-scene-desktop.png",
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 65.18,
|
||||
"nonBlackRatio": 0.8173
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":36.474,\"y\":-22.486,\"z\":-13.749,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "186",
|
||||
"threeFeedPathPoints": "1436",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "1",
|
||||
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
|
||||
},
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=1498"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "scene units are meters",
|
||||
"pass": true,
|
||||
"detail": "sceneUnits=m"
|
||||
},
|
||||
{
|
||||
"name": "linear scale visible",
|
||||
"pass": true,
|
||||
"detail": "scale=0.001"
|
||||
},
|
||||
{
|
||||
"name": "path fit bounds",
|
||||
"pass": true,
|
||||
"detail": "fit=ok"
|
||||
},
|
||||
{
|
||||
"name": "path bounds in meters",
|
||||
"pass": true,
|
||||
"detail": "bounds={\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}"
|
||||
},
|
||||
{
|
||||
"name": "canvas nonblank",
|
||||
"pass": true,
|
||||
"detail": "nonBlack=0.8173"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"viewport": {
|
||||
"name": "mobile",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"deviceScaleFactor": 2
|
||||
},
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/meter-scene-evidence/08-meter-scene-mobile.png",
|
||||
"pixelStats": {
|
||||
"width": 1146,
|
||||
"height": 1036,
|
||||
"averageLuminance": 67.87,
|
||||
"nonBlackRatio": 0.838
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":36.474,\"y\":-22.486,\"z\":-13.749,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "186",
|
||||
"threeFeedPathPoints": "1436",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "1",
|
||||
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
|
||||
},
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=1498"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "scene units are meters",
|
||||
"pass": true,
|
||||
"detail": "sceneUnits=m"
|
||||
},
|
||||
{
|
||||
"name": "linear scale visible",
|
||||
"pass": true,
|
||||
"detail": "scale=0.001"
|
||||
},
|
||||
{
|
||||
"name": "path fit bounds",
|
||||
"pass": true,
|
||||
"detail": "fit=ok"
|
||||
},
|
||||
{
|
||||
"name": "path bounds in meters",
|
||||
"pass": true,
|
||||
"detail": "bounds={\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}"
|
||||
},
|
||||
{
|
||||
"name": "canvas nonblank",
|
||||
"pass": true,
|
||||
"detail": "nonBlack=0.838"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)",
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
]
|
||||
}
|
||||
5962
qa/web-rtcp-5axis-site-test/output/public-https-run-debug.json
Normal file
@@ -0,0 +1,565 @@
|
||||
{
|
||||
"generatedAt": "2026-06-22T22:10:40.219Z",
|
||||
"targetUrl": "http://127.0.0.1:45757/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"caseName": "07-run-preconditions-and-feedback",
|
||||
"summary": "浏览器 RUN 证据:INI/profile/kinematics/task-HAL 同一上下文,taskHalStatusLoop 持续推进,UI 消费 task/HAL/motion feedback",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "INI ready",
|
||||
"pass": true,
|
||||
"detail": "iniReady=true,true,true,true,true"
|
||||
},
|
||||
{
|
||||
"name": "selected LinuxCNC G-code",
|
||||
"pass": true,
|
||||
"detail": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc"
|
||||
},
|
||||
{
|
||||
"name": "task/HAL session opened selected G-code",
|
||||
"pass": true,
|
||||
"detail": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc"
|
||||
},
|
||||
{
|
||||
"name": "taskHalStatusLoop 新 RUN sequence",
|
||||
"pass": true,
|
||||
"detail": "baselineSequence=1, sequences=2,2,2,2,2"
|
||||
},
|
||||
{
|
||||
"name": "taskHalStatusLoop 本次 RUN tickCount",
|
||||
"pass": true,
|
||||
"detail": "ticks=4,4,4,4,4"
|
||||
},
|
||||
{
|
||||
"name": "programRuntimeFeedbackHistory 本次 RUN 采样",
|
||||
"pass": true,
|
||||
"detail": "history=5,5,5,5,5"
|
||||
},
|
||||
{
|
||||
"name": "feedback sourceMode 来自 task/HAL",
|
||||
"pass": true,
|
||||
"detail": "sourceModes=linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm"
|
||||
},
|
||||
{
|
||||
"name": "无 fixture-line-playback feedback",
|
||||
"pass": true,
|
||||
"detail": "sourceModes=linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm"
|
||||
},
|
||||
{
|
||||
"name": "semantic boundary",
|
||||
"pass": true,
|
||||
"detail": "linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime"
|
||||
},
|
||||
{
|
||||
"name": "activeLine 等于 UI 高亮行",
|
||||
"pass": true,
|
||||
"detail": "active=29/29,29/29,29/29,29/29,29/29"
|
||||
},
|
||||
{
|
||||
"name": "DRO 等于 runtime feedback axisPose",
|
||||
"pass": true,
|
||||
"detail": "droMatchesFeedback=true for all samples"
|
||||
},
|
||||
{
|
||||
"name": "RTCP canvas 与 state 一致",
|
||||
"pass": true,
|
||||
"detail": "off/off,off/off,off/off,off/off,off/off"
|
||||
},
|
||||
{
|
||||
"name": "runtime cycle 可见",
|
||||
"pass": true,
|
||||
"detail": "33/330,33/330,33/330,33/330,33/330"
|
||||
},
|
||||
{
|
||||
"name": "canvas 执行轨迹",
|
||||
"pass": true,
|
||||
"detail": "true/1/ok,true/1/ok,true/1/ok,true/1/ok,true/1/ok"
|
||||
}
|
||||
],
|
||||
"baseline": {
|
||||
"elapsedMs": 0,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-0000ms-before-run.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 1,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 12,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:27.362Z",
|
||||
"lastError": null,
|
||||
"stopReason": "running",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "idle",
|
||||
"activeLine": 1,
|
||||
"activeRowLine": 1,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 1,
|
||||
"taskCycle": 3,
|
||||
"servoCycle": 30,
|
||||
"velocity": 0,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 18,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 30
|
||||
}
|
||||
}
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"elapsedMs": 200,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-200ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"elapsedMs": 500,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-500ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"elapsedMs": 1000,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-1000ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"elapsedMs": 2000,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-2000ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"elapsedMs": 5000,
|
||||
"screenshotPath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/07-run-preconditions-and-feedback-5000ms.png",
|
||||
"canvas": {
|
||||
"threeReady": "true",
|
||||
"rtcpState": "off",
|
||||
"executedPathPoints": 1,
|
||||
"currentSegmentHighlight": "ok",
|
||||
"toolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback"
|
||||
},
|
||||
"state": {
|
||||
"profileId": "xyzac-trt",
|
||||
"iniReady": true,
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"selectedGcodeSourceRel": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalSessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc",
|
||||
"taskHalStatusLoop": {
|
||||
"apiName": "web-rtcp-5axis-task-hal-status-loop",
|
||||
"active": false,
|
||||
"sequence": 2,
|
||||
"profileId": "xyzac-trt",
|
||||
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"kinematicsModuleId": "xyzac-trt",
|
||||
"tickCount": 4,
|
||||
"batchSize": 5,
|
||||
"intervalMs": 25,
|
||||
"taskPeriodNs": 10000000,
|
||||
"servoPeriodNs": 1000000,
|
||||
"lastStatusAt": "2026-06-22T22:10:35.152Z",
|
||||
"lastError": null,
|
||||
"stopReason": "complete",
|
||||
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
|
||||
},
|
||||
"runState": "complete",
|
||||
"activeLine": 29,
|
||||
"activeRowLine": 29,
|
||||
"activeLineMatchesUi": true,
|
||||
"droAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"feedbackAxisPose": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0
|
||||
},
|
||||
"droMatchesFeedback": true,
|
||||
"rtcpState": "off",
|
||||
"kinsType": "identity",
|
||||
"feedback": {
|
||||
"sourceMode": "linuxcnc-task-motion-hal-wasm",
|
||||
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
"line": 29,
|
||||
"taskCycle": 33,
|
||||
"servoCycle": 330,
|
||||
"velocity": 3600,
|
||||
"distanceToGo": 0
|
||||
},
|
||||
"feedbackHistoryLength": 5,
|
||||
"feedbackHistorySourceModes": [
|
||||
"linuxcnc-task-motion-hal-wasm"
|
||||
],
|
||||
"taskHalStatus": {
|
||||
"taskState": "on",
|
||||
"taskMode": "auto",
|
||||
"interpState": "idle",
|
||||
"taskCycle": null,
|
||||
"servoCycle": 330
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)",
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"generatedAt": "2026-06-22T13:12:11.146Z",
|
||||
"targetUrl": "http://127.0.0.1:43627/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"generatedAt": "2026-06-22T22:09:28.057Z",
|
||||
"targetUrl": "http://127.0.0.1:45757/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"projectPath": "/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"chromePath": "/usr/bin/google-chrome",
|
||||
"screenshots": {
|
||||
@@ -9,6 +9,7 @@
|
||||
"03-arc-demo-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/03-arc-demo-toolpath.png",
|
||||
"04-clear-preview-reference": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/04-clear-preview-reference.png",
|
||||
"05-vendored-impeller-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/05-vendored-impeller-toolpath.png",
|
||||
"09-g20-g21-mixed-units-preview": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/09-g20-g21-mixed-units-preview.png",
|
||||
"06-running-rtcp-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/06-running-rtcp-toolpath.png"
|
||||
},
|
||||
"cases": [
|
||||
@@ -66,8 +67,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 62.52,
|
||||
"nonBlackRatio": 0.7491
|
||||
"averageLuminance": 68.18,
|
||||
"nonBlackRatio": 0.8681
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -77,7 +78,10 @@
|
||||
"threePathPoints": "2",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -95,6 +99,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":0,\"y\":0,\"z\":0.002},\"size\":{\"x\":0,\"y\":0,\"z\":0.005},\"maxSpan\":0.005}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -207,8 +212,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 59.4,
|
||||
"nonBlackRatio": 0.7092
|
||||
"averageLuminance": 66.03,
|
||||
"nonBlackRatio": 0.854
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -218,7 +223,10 @@
|
||||
"threePathPoints": "6",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -236,6 +244,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":0.005,\"y\":0.005,\"z\":0.002},\"size\":{\"x\":0.01,\"y\":0.01,\"z\":0.005},\"maxSpan\":0.01}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -346,8 +355,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 65.26,
|
||||
"nonBlackRatio": 0.7885
|
||||
"averageLuminance": 67.56,
|
||||
"nonBlackRatio": 0.8687
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -357,7 +366,10 @@
|
||||
"threePathPoints": "2",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":1,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -375,6 +387,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":0.001,\"y\":0.001,\"z\":0},\"size\":{\"x\":0.001,\"y\":0.001,\"z\":0},\"maxSpan\":0.001}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -487,8 +500,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 64.72,
|
||||
"nonBlackRatio": 0.7874
|
||||
"averageLuminance": 67.78,
|
||||
"nonBlackRatio": 0.8701
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -498,7 +511,10 @@
|
||||
"threePathPoints": "0",
|
||||
"threeExecutedPathPoints": "0",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":1,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -516,6 +532,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "null",
|
||||
"threeCurrentSegmentHighlight": "pending",
|
||||
"threeRapidFeedVisualDistinction": "pending",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -643,8 +660,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 35.56,
|
||||
"nonBlackRatio": 0.4577
|
||||
"averageLuminance": 65.18,
|
||||
"nonBlackRatio": 0.8173
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -654,7 +671,10 @@
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":36.474,\"y\":-22.486,\"z\":-13.749,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeRtcpState": "on",
|
||||
@@ -672,6 +692,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -726,6 +747,176 @@
|
||||
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "08-meter-scene-desktop-mobile",
|
||||
"summary": "浏览器米尺度 Three.js 证据:desktop/mobile canvas 非空、路径 bounds 为米、预览稳定居中",
|
||||
"status": "PASS",
|
||||
"screenshotDir": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/meter-scene-evidence",
|
||||
"output": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/meter-scene-evidence.json",
|
||||
"sampleCount": 2
|
||||
},
|
||||
{
|
||||
"name": "09-g20-g21-mixed-units-preview",
|
||||
"summary": "G20/G21 混合单位程序:验证 canonical motion 保留 inch/mm,Three.js 统一按米绘制",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=2"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "G20/G21 motion units",
|
||||
"pass": true,
|
||||
"detail": "units=inch,mm"
|
||||
},
|
||||
{
|
||||
"name": "scene units are meters",
|
||||
"pass": true,
|
||||
"detail": "sceneUnits=m"
|
||||
},
|
||||
{
|
||||
"name": "mixed unit path visible",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=2"
|
||||
},
|
||||
{
|
||||
"name": "1 inch equals 25.4 mm in scene",
|
||||
"pass": true,
|
||||
"detail": "xMeters=0.0254,0.0254"
|
||||
},
|
||||
{
|
||||
"name": "no unit fallback in canonical motion",
|
||||
"pass": true,
|
||||
"detail": "source=linuxcnc-interpreter-wasm"
|
||||
}
|
||||
],
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 64.71,
|
||||
"nonBlackRatio": 0.849
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "2",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":43,\"y\":-32.15,\"z\":-11.306,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":0.013,\"y\":0.013,\"z\":0},\"size\":{\"x\":0.025,\"y\":0.025,\"z\":0},\"maxSpan\":0.0254}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "0",
|
||||
"threeFeedPathPoints": "2",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "1",
|
||||
"threeCurrentSegmentType": "STRAIGHT_FEED"
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "operator-g20-g21-mixed-units.ngc",
|
||||
"activeLine": 2,
|
||||
"programSource": "operator-file",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
"ready": true,
|
||||
"programLineCount": 5,
|
||||
"canonicalEventCount": 22,
|
||||
"motionEventCount": 2,
|
||||
"motionTypes": [
|
||||
"STRAIGHT_FEED"
|
||||
],
|
||||
"finalAxes": {
|
||||
"x": 25.4,
|
||||
"y": 25.4,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"w": 0
|
||||
},
|
||||
"switchkinsEventCount": 0,
|
||||
"switchkinsCodes": [],
|
||||
"switchkinsRemapBoundary": null,
|
||||
"remapRuntimeReady": false,
|
||||
"plannerRuntimeReady": true,
|
||||
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
"machineFileExecutionReady": false,
|
||||
"fullLinuxCncProgramExecutionReady": false
|
||||
},
|
||||
"runState": "idle",
|
||||
"rtcpState": "on",
|
||||
"kinsType": "tcp-xyzac",
|
||||
"programExecutionMotionIndex": 0,
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "06-running-rtcp-toolpath",
|
||||
"summary": "G-code 运行态:验证 task/HAL runtime feedback 驱动执行轨迹、当前段高亮、TCP 球和刀轴线跟随",
|
||||
@@ -790,8 +981,8 @@
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 36.05,
|
||||
"nonBlackRatio": 0.4607
|
||||
"averageLuminance": 65.86,
|
||||
"nonBlackRatio": 0.8173
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
@@ -801,9 +992,12 @@
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0.26,\"y\":-0.458,\"z\":1.485}",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":7.417,\"y\":-13.098,\"z\":28.366,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeToolhead": "{\"x\":-0.023,\"y\":-0.033,\"z\":0.001}",
|
||||
"threeSceneUnits": "m",
|
||||
"threeLinearUnits": "mm",
|
||||
"threeLinearUnitScaleToMeters": "0.001",
|
||||
"threeToolAxis": "{\"x\":0.621,\"y\":0.503,\"z\":0.601}",
|
||||
"threeTcpPose": "{\"x\":-75.066,\"y\":-75.473,\"z\":-49.344,\"a\":-53.043,\"c\":-50.969}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
@@ -819,6 +1013,7 @@
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threePathBoundsMeters": "{\"center\":{\"x\":-0.001,\"y\":0,\"z\":0.019},\"size\":{\"x\":0.088,\"y\":0.089,\"z\":0.043},\"maxSpan\":0.089211}",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
@@ -830,7 +1025,7 @@
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
"activeLine": 5,
|
||||
"activeLine": 65,
|
||||
"programSource": "linuxcnc-vendored-5axis-gcode",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
@@ -872,6 +1067,86 @@
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-task-motion-hal-wasm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "07-run-preconditions-and-feedback",
|
||||
"summary": "浏览器 RUN 证据:INI/profile/kinematics/task-HAL 同一上下文,taskHalStatusLoop 持续推进,UI 消费 task/HAL/motion feedback",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "INI ready",
|
||||
"pass": true,
|
||||
"detail": "iniReady=true,true,true,true,true"
|
||||
},
|
||||
{
|
||||
"name": "selected LinuxCNC G-code",
|
||||
"pass": true,
|
||||
"detail": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc"
|
||||
},
|
||||
{
|
||||
"name": "task/HAL session opened selected G-code",
|
||||
"pass": true,
|
||||
"detail": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt/demos/xyzac_switchkins_test_1.ngc"
|
||||
},
|
||||
{
|
||||
"name": "taskHalStatusLoop 新 RUN sequence",
|
||||
"pass": true,
|
||||
"detail": "baselineSequence=1, sequences=2,2,2,2,2"
|
||||
},
|
||||
{
|
||||
"name": "taskHalStatusLoop 本次 RUN tickCount",
|
||||
"pass": true,
|
||||
"detail": "ticks=4,4,4,4,4"
|
||||
},
|
||||
{
|
||||
"name": "programRuntimeFeedbackHistory 本次 RUN 采样",
|
||||
"pass": true,
|
||||
"detail": "history=5,5,5,5,5"
|
||||
},
|
||||
{
|
||||
"name": "feedback sourceMode 来自 task/HAL",
|
||||
"pass": true,
|
||||
"detail": "sourceModes=linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm"
|
||||
},
|
||||
{
|
||||
"name": "无 fixture-line-playback feedback",
|
||||
"pass": true,
|
||||
"detail": "sourceModes=linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm,linuxcnc-task-motion-hal-wasm"
|
||||
},
|
||||
{
|
||||
"name": "semantic boundary",
|
||||
"pass": true,
|
||||
"detail": "linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime,linuxcnc_task_motion_hal_wasm_simulation_runtime"
|
||||
},
|
||||
{
|
||||
"name": "activeLine 等于 UI 高亮行",
|
||||
"pass": true,
|
||||
"detail": "active=29/29,29/29,29/29,29/29,29/29"
|
||||
},
|
||||
{
|
||||
"name": "DRO 等于 runtime feedback axisPose",
|
||||
"pass": true,
|
||||
"detail": "droMatchesFeedback=true for all samples"
|
||||
},
|
||||
{
|
||||
"name": "RTCP canvas 与 state 一致",
|
||||
"pass": true,
|
||||
"detail": "off/off,off/off,off/off,off/off,off/off"
|
||||
},
|
||||
{
|
||||
"name": "runtime cycle 可见",
|
||||
"pass": true,
|
||||
"detail": "33/330,33/330,33/330,33/330,33/330"
|
||||
},
|
||||
{
|
||||
"name": "canvas 执行轨迹",
|
||||
"pass": true,
|
||||
"detail": "true/1/ok,true/1/ok,true/1/ok,true/1/ok,true/1/ok"
|
||||
}
|
||||
],
|
||||
"screenshotDir": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback",
|
||||
"output": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/run-preconditions-feedback.json",
|
||||
"sampleCount": 5
|
||||
}
|
||||
],
|
||||
"consoleErrors": [
|
||||
|
||||
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -143,3 +143,6 @@ When extending this workspace:
|
||||
2. Keep extraction and patching reproducible.
|
||||
3. Keep adapters narrow and explicit.
|
||||
4. Keep LinuxCNC-derived logic traceable to its upstream file origin.
|
||||
5. After every GPT/Codex execution completes, append the full execution
|
||||
process log to
|
||||
`/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -14,6 +15,17 @@
|
||||
|
||||
namespace {
|
||||
|
||||
struct MotionSegment {
|
||||
int line = 0;
|
||||
std::string type;
|
||||
std::string motion_class;
|
||||
double start_seconds = 0.0;
|
||||
double duration_seconds = 0.0;
|
||||
double velocity_mm_per_min = 0.0;
|
||||
std::map<std::string, double> start_axes;
|
||||
std::map<std::string, double> end_axes;
|
||||
};
|
||||
|
||||
struct TaskRuntime {
|
||||
bool initialized = false;
|
||||
std::string state = "ESTOP";
|
||||
@@ -22,9 +34,16 @@ struct TaskRuntime {
|
||||
std::string exec_state = "DONE";
|
||||
std::string open_program;
|
||||
int opened_line_count = 0;
|
||||
int opened_source_line_count = 0;
|
||||
int executable_line_count = 0;
|
||||
int next_program_line = 0;
|
||||
long long task_cycle = 0;
|
||||
long long servo_cycle = 0;
|
||||
bool motion_plan_loaded = false;
|
||||
int active_segment_index = 0;
|
||||
double run_elapsed_seconds = 0.0;
|
||||
std::string motion_plan_program_path;
|
||||
std::vector<MotionSegment> motion_plan;
|
||||
std::map<std::string, std::string> staged_files;
|
||||
std::vector<std::string> program_lines;
|
||||
std::vector<std::string> events;
|
||||
@@ -141,6 +160,111 @@ int json_int_after(const char *json, const char *key, int fallback)
|
||||
return static_cast<int>(json_number_after(json, key, fallback));
|
||||
}
|
||||
|
||||
std::size_t matching_brace(const std::string &text, std::size_t open)
|
||||
{
|
||||
int depth = 0;
|
||||
bool in_string = false;
|
||||
bool escaped = false;
|
||||
for (std::size_t i = open; i < text.size(); ++i) {
|
||||
const char ch = text[i];
|
||||
if (in_string) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (ch == '\\') {
|
||||
escaped = true;
|
||||
} else if (ch == '"') {
|
||||
in_string = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch == '"') {
|
||||
in_string = true;
|
||||
} else if (ch == '{') {
|
||||
depth += 1;
|
||||
} else if (ch == '}') {
|
||||
depth -= 1;
|
||||
if (depth == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
std::string json_object_after(const std::string &json, const char *key)
|
||||
{
|
||||
const std::size_t key_pos = json.find(key);
|
||||
if (key_pos == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
const std::size_t open = json.find('{', key_pos);
|
||||
if (open == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
const std::size_t close = matching_brace(json, open);
|
||||
if (close == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
return json.substr(open, close - open + 1);
|
||||
}
|
||||
|
||||
std::map<std::string, double> json_axes_object_after(const std::string &json, const char *key)
|
||||
{
|
||||
std::map<std::string, double> axes;
|
||||
const std::string object = json_object_after(json, key);
|
||||
for (const char *axis : {"x", "y", "z", "a", "b", "c", "u", "v", "w"}) {
|
||||
axes[axis] = json_number_after(object.c_str(), (std::string("\"") + axis + "\"").c_str(), 0.0);
|
||||
}
|
||||
return axes;
|
||||
}
|
||||
|
||||
std::vector<std::string> json_segment_objects(const std::string &json)
|
||||
{
|
||||
std::vector<std::string> segments;
|
||||
std::size_t at = json.find("\"segments\"");
|
||||
if (at == std::string::npos) {
|
||||
return segments;
|
||||
}
|
||||
at = json.find('[', at);
|
||||
if (at == std::string::npos) {
|
||||
return segments;
|
||||
}
|
||||
while (at < json.size()) {
|
||||
const std::size_t open = json.find('{', at);
|
||||
const std::size_t close_array = json.find(']', at);
|
||||
if (open == std::string::npos || (close_array != std::string::npos && close_array < open)) {
|
||||
break;
|
||||
}
|
||||
const std::size_t close = matching_brace(json, open);
|
||||
if (close == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
segments.push_back(json.substr(open, close - open + 1));
|
||||
at = close + 1;
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
double clamp_double(double value, double low, double high)
|
||||
{
|
||||
return std::min(std::max(value, low), high);
|
||||
}
|
||||
|
||||
std::string axis_json(const std::map<std::string, double> &start_axes,
|
||||
const std::map<std::string, double> &end_axes,
|
||||
double progress)
|
||||
{
|
||||
std::ostringstream out;
|
||||
for (const char *axis : {"x", "y", "z", "a", "b", "c"}) {
|
||||
const auto start_it = start_axes.find(axis);
|
||||
const auto end_it = end_axes.find(axis);
|
||||
const double start = start_it == start_axes.end() ? 0.0 : start_it->second;
|
||||
const double end = end_it == end_axes.end() ? start : end_it->second;
|
||||
out << ",\"" << axis << "\":" << (start + (end - start) * progress);
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string trim_copy(const std::string &value)
|
||||
{
|
||||
std::size_t begin = 0;
|
||||
@@ -198,12 +322,104 @@ void enqueue_linear_move_from_line(TaskRuntime &state, const std::string &line)
|
||||
line.find('B') == std::string::npos && line.find('C') == std::string::npos) {
|
||||
command << ",\"x\":" << fallback;
|
||||
}
|
||||
command << ",\"velocity\":60}";
|
||||
const std::size_t feed_pos = line.find('F');
|
||||
double velocity_units_per_second = 60.0;
|
||||
if (feed_pos != std::string::npos) {
|
||||
char *end = nullptr;
|
||||
const double feed_units_per_minute = std::strtod(line.c_str() + feed_pos + 1, &end);
|
||||
if (end != line.c_str() + feed_pos + 1 && feed_units_per_minute > 0.0) {
|
||||
velocity_units_per_second = feed_units_per_minute / 60.0;
|
||||
}
|
||||
}
|
||||
command << ",\"velocity\":" << velocity_units_per_second << "}";
|
||||
forward_motion_command(command.str());
|
||||
state.events.push_back("task_queue_motion_line:" + std::to_string(line_number));
|
||||
state.next_program_line += 1;
|
||||
}
|
||||
|
||||
bool load_motion_plan(TaskRuntime &state, const char *plan_json)
|
||||
{
|
||||
if (!plan_json) {
|
||||
return false;
|
||||
}
|
||||
const std::string json(plan_json);
|
||||
std::vector<MotionSegment> segments;
|
||||
for (const std::string &item : json_segment_objects(json)) {
|
||||
MotionSegment segment;
|
||||
segment.line = json_int_after(item.c_str(), "\"line\"", 0);
|
||||
segment.type = json_string_after(item.c_str(), "\"type\"", "");
|
||||
segment.motion_class = json_string_after(item.c_str(), "\"motionClass\"", "");
|
||||
segment.start_seconds = json_number_after(item.c_str(), "\"startSeconds\"", 0.0);
|
||||
segment.duration_seconds = std::max(json_number_after(item.c_str(), "\"durationSeconds\"", 0.0), 0.0);
|
||||
segment.velocity_mm_per_min = std::max(json_number_after(item.c_str(), "\"velocityMmPerMin\"", 0.0), 0.0);
|
||||
segment.start_axes = json_axes_object_after(item, "\"startAxes\"");
|
||||
segment.end_axes = json_axes_object_after(item, "\"endAxes\"");
|
||||
if (segment.line <= 0) {
|
||||
segment.line = static_cast<int>(segments.size()) + 1;
|
||||
}
|
||||
segments.push_back(segment);
|
||||
}
|
||||
if (segments.empty()) {
|
||||
return false;
|
||||
}
|
||||
state.motion_plan = std::move(segments);
|
||||
state.motion_plan_loaded = true;
|
||||
state.active_segment_index = 0;
|
||||
state.run_elapsed_seconds = 0.0;
|
||||
state.motion_plan_program_path = json_string_after(plan_json, "\"programPath\"", state.open_program);
|
||||
if (!state.motion_plan.empty()) {
|
||||
state.opened_source_line_count = std::max(
|
||||
state.opened_source_line_count,
|
||||
state.motion_plan.back().line);
|
||||
}
|
||||
state.events.push_back("task_motion_plan_loaded:" + std::to_string(state.motion_plan.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
void forward_timed_motion_sample(TaskRuntime &state)
|
||||
{
|
||||
if (!state.motion_plan_loaded || state.motion_plan.empty()) {
|
||||
return;
|
||||
}
|
||||
while (state.active_segment_index < static_cast<int>(state.motion_plan.size()) - 1) {
|
||||
const MotionSegment &segment = state.motion_plan[state.active_segment_index];
|
||||
const double end_seconds = segment.start_seconds + segment.duration_seconds;
|
||||
if (state.run_elapsed_seconds < end_seconds) {
|
||||
break;
|
||||
}
|
||||
state.active_segment_index += 1;
|
||||
}
|
||||
|
||||
if (state.active_segment_index >= static_cast<int>(state.motion_plan.size())) {
|
||||
state.interp_state = "IDLE";
|
||||
state.exec_state = "DONE";
|
||||
state.next_program_line = std::max(state.opened_line_count, state.opened_source_line_count);
|
||||
return;
|
||||
}
|
||||
|
||||
const MotionSegment &segment = state.motion_plan[state.active_segment_index];
|
||||
const double duration = std::max(segment.duration_seconds, 0.000001);
|
||||
const double progress = clamp_double((state.run_elapsed_seconds - segment.start_seconds) / duration, 0.0, 1.0);
|
||||
const double velocity_units_per_second = segment.velocity_mm_per_min / 60.0;
|
||||
std::ostringstream command;
|
||||
command << "{\"type\":\"EMC_TRAJ_LINEAR_MOVE\",\"source\":\"feed_timed_motion_plan\"";
|
||||
command << ",\"line\":" << segment.line;
|
||||
command << axis_json(segment.start_axes, segment.end_axes, progress);
|
||||
command << ",\"velocity\":" << velocity_units_per_second;
|
||||
command << ",\"currentVel\":" << velocity_units_per_second;
|
||||
command << ",\"requestedVel\":" << velocity_units_per_second;
|
||||
command << ",\"segmentProgress\":" << progress << "}";
|
||||
forward_motion_command(command.str());
|
||||
const int max_line = std::max(state.opened_line_count, state.opened_source_line_count);
|
||||
state.next_program_line = std::min(std::max(segment.line, 1), std::max(max_line, 1));
|
||||
if (state.active_segment_index == static_cast<int>(state.motion_plan.size()) - 1 && progress >= 1.0) {
|
||||
state.interp_state = "IDLE";
|
||||
state.exec_state = "DONE";
|
||||
state.next_program_line = std::max(state.opened_line_count, state.opened_source_line_count);
|
||||
state.events.push_back("task_motion_plan_complete");
|
||||
}
|
||||
}
|
||||
|
||||
void enqueue_mdi(TaskRuntime &state, const char *json)
|
||||
{
|
||||
const std::string mdi = json_string_after(json, "\"mdi\"");
|
||||
@@ -265,6 +481,8 @@ std::string status_json()
|
||||
out << ",\"cycle\":" << state.task_cycle;
|
||||
out << ",\"openProgram\":\"" << json_escape(state.open_program) << "\"";
|
||||
out << ",\"openedLineCount\":" << state.opened_line_count;
|
||||
out << ",\"openedSourceLineCount\":" << state.opened_source_line_count;
|
||||
out << ",\"executableLineCount\":" << state.executable_line_count;
|
||||
out << ",\"nextProgramLine\":" << state.next_program_line << "}";
|
||||
out << ",\"servoCycle\":" << state.servo_cycle;
|
||||
out << ",\"motionStatus\":" << read_motion_status_json();
|
||||
@@ -341,14 +559,30 @@ int lctask_open_program(const char *path)
|
||||
}
|
||||
state.open_program = path;
|
||||
state.program_lines = split_program_lines(it->second);
|
||||
state.opened_line_count = static_cast<int>(state.program_lines.size());
|
||||
state.executable_line_count = static_cast<int>(state.program_lines.size());
|
||||
state.opened_source_line_count = static_cast<int>(std::count(it->second.begin(), it->second.end(), '\n'));
|
||||
if (!it->second.empty() && it->second.back() != '\n') {
|
||||
state.opened_source_line_count += 1;
|
||||
}
|
||||
state.opened_line_count = state.opened_source_line_count;
|
||||
state.next_program_line = 0;
|
||||
state.active_segment_index = 0;
|
||||
state.run_elapsed_seconds = 0.0;
|
||||
state.interp_state = "IDLE";
|
||||
state.exec_state = "DONE";
|
||||
state.events.push_back(std::string("task_open_program:") + path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int lctask_load_program_motion_plan_json(const char *plan_json)
|
||||
{
|
||||
auto &state = task_runtime();
|
||||
if (!state.initialized || !plan_json) {
|
||||
return -1;
|
||||
}
|
||||
return load_motion_plan(state, plan_json) ? 0 : -1;
|
||||
}
|
||||
|
||||
int lctask_send_command_json(const char *command_json)
|
||||
{
|
||||
auto &state = task_runtime();
|
||||
@@ -373,9 +607,11 @@ int lctask_send_command_json(const char *command_json)
|
||||
if (state.next_program_line < 0) {
|
||||
state.next_program_line = 0;
|
||||
}
|
||||
if (state.next_program_line >= state.opened_line_count) {
|
||||
if (!state.motion_plan_loaded && state.next_program_line >= state.executable_line_count) {
|
||||
state.next_program_line = 0;
|
||||
}
|
||||
state.active_segment_index = 0;
|
||||
state.run_elapsed_seconds = 0.0;
|
||||
state.interp_state = "READING";
|
||||
state.exec_state = "WAITING_FOR_MOTION";
|
||||
state.events.push_back("task_plan_run");
|
||||
@@ -396,6 +632,7 @@ int lctask_send_command_json(const char *command_json)
|
||||
if (contains_token(command_json, "EMC_TASK_ABORT")) {
|
||||
state.interp_state = "IDLE";
|
||||
state.exec_state = "DONE";
|
||||
state.run_elapsed_seconds = 0.0;
|
||||
state.events.push_back("task_abort");
|
||||
return forward_motion_command("{\"type\":\"EMC_TRAJ_ABORT\"}");
|
||||
}
|
||||
@@ -413,6 +650,17 @@ int lctask_send_command_json(const char *command_json)
|
||||
state.events.push_back("task_jog_incr");
|
||||
return forward_motion_command(command_json);
|
||||
}
|
||||
if (contains_token(command_json, "EMC_JOINT_HOME")) {
|
||||
state.mode = "MANUAL";
|
||||
state.interp_state = "IDLE";
|
||||
state.exec_state = "DONE";
|
||||
state.next_program_line = 0;
|
||||
state.active_segment_index = 0;
|
||||
state.run_elapsed_seconds = 0.0;
|
||||
state.events.push_back("task_joint_home");
|
||||
forward_motion_command("{\"type\":\"EMC_TRAJ_LINEAR_MOVE\",\"line\":1,\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"b\":0,\"c\":0,\"velocity\":0}");
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -431,9 +679,18 @@ int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles
|
||||
}
|
||||
for (int i = 0; i < task_cycles; ++i) {
|
||||
state.task_cycle += 1;
|
||||
if (state.interp_state == "READING" && state.next_program_line < state.opened_line_count) {
|
||||
enqueue_linear_move_from_line(state, state.program_lines[state.next_program_line]);
|
||||
if (state.next_program_line >= state.opened_line_count) {
|
||||
const bool has_program_work = state.motion_plan_loaded
|
||||
? state.active_segment_index < static_cast<int>(state.motion_plan.size())
|
||||
: state.next_program_line < state.executable_line_count;
|
||||
if (state.interp_state == "READING" && has_program_work) {
|
||||
if (state.motion_plan_loaded) {
|
||||
const double delta_seconds = task_period_ns > 0 ? static_cast<double>(task_period_ns) / 1000000000.0 : 0.01;
|
||||
state.run_elapsed_seconds += delta_seconds;
|
||||
forward_timed_motion_sample(state);
|
||||
} else {
|
||||
enqueue_linear_move_from_line(state, state.program_lines[state.next_program_line]);
|
||||
}
|
||||
if (!state.motion_plan_loaded && state.next_program_line >= state.executable_line_count) {
|
||||
state.interp_state = "IDLE";
|
||||
state.exec_state = "DONE";
|
||||
state.events.push_back("task_plan_complete");
|
||||
|
||||
@@ -7,6 +7,7 @@ extern "C" {
|
||||
int lctask_init_session(const char *session_json);
|
||||
int lctask_stage_file(const char *path, const char *text);
|
||||
int lctask_open_program(const char *path);
|
||||
int lctask_load_program_motion_plan_json(const char *plan_json);
|
||||
int lctask_send_command_json(const char *command_json);
|
||||
int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles);
|
||||
int lctask_read_status_json(char *out, int out_len);
|
||||
|
||||
@@ -97,6 +97,10 @@ export async function createLinuxCncTaskHalSdk(moduleOptions = {}) {
|
||||
);
|
||||
},
|
||||
|
||||
loadProgramMotionPlan(plan) {
|
||||
return callWithJson(mod, "lctask_load_program_motion_plan_json", plan);
|
||||
},
|
||||
|
||||
sendCommand(command) {
|
||||
return callWithJson(mod, "lctask_send_command_json", command);
|
||||
},
|
||||
|
||||
@@ -52,5 +52,116 @@ const events = sdk.readEvents().events;
|
||||
assert.equal(events.includes("task_plan_run"), true);
|
||||
assert.equal(events.includes("task_mdi_switchkins:M429"), true);
|
||||
|
||||
sdk.resetSession();
|
||||
sdk.initSession({
|
||||
iniPath: "xyzac-trt.ini",
|
||||
iniText: "[TRAJ]\nCOORDINATES = X Y Z A C\n",
|
||||
});
|
||||
assert.equal(sdk.stageFile("programs/feed-plan.ngc", "G1 X120 F60\nG1 X121 F600\n"), 0);
|
||||
assert.equal(sdk.openProgram("programs/feed-plan.ngc"), 0);
|
||||
assert.equal(sdk.loadProgramMotionPlan({
|
||||
programPath: "programs/feed-plan.ngc",
|
||||
segments: [
|
||||
{
|
||||
line: 1,
|
||||
type: "STRAIGHT_FEED",
|
||||
motionClass: "feed",
|
||||
feedMode: "units-per-minute",
|
||||
startSeconds: 0,
|
||||
durationSeconds: 120,
|
||||
velocityMmPerMin: 60,
|
||||
startAxes: { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0 },
|
||||
endAxes: { x: 120, y: 0, z: 0, a: 0, b: 0, c: 0 },
|
||||
},
|
||||
{
|
||||
line: 2,
|
||||
type: "STRAIGHT_FEED",
|
||||
motionClass: "feed",
|
||||
feedMode: "units-per-minute",
|
||||
startSeconds: 120,
|
||||
durationSeconds: 0.1,
|
||||
velocityMmPerMin: 600,
|
||||
startAxes: { x: 120, y: 0, z: 0, a: 0, b: 0, c: 0 },
|
||||
endAxes: { x: 121, y: 0, z: 0, a: 0, b: 0, c: 0 },
|
||||
},
|
||||
],
|
||||
}), 0);
|
||||
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" }), 0);
|
||||
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" }), 0);
|
||||
assert.equal(sdk.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 }), 0);
|
||||
assert.equal(sdk.runCycles({ taskCycles: 1, taskPeriodNs: 1000000000, servoPeriodNs: 1000000 }), 0);
|
||||
status = sdk.readStatus();
|
||||
assert.equal(status.motionStatus.motion.programLine, 1);
|
||||
assert.equal(status.motionStatus.axis.x > 0 && status.motionStatus.axis.x < 2, true);
|
||||
assert.equal(status.motionStatus.motion.currentVel, 1);
|
||||
assert.equal(status.motionStatus.motion.currentVel !== 60, true);
|
||||
assert.equal(sdk.runCycles({ taskCycles: 119, taskPeriodNs: 1000000000, servoPeriodNs: 1000000 }), 0);
|
||||
status = sdk.readStatus();
|
||||
assert.equal(status.motionStatus.motion.programLine, 2);
|
||||
assert.equal(status.motionStatus.motion.currentVel, 10);
|
||||
assert.equal(status.motionStatus.axis.x >= 120, true);
|
||||
|
||||
sdk.resetSession();
|
||||
sdk.initSession({
|
||||
iniPath: "xyzac-trt.ini",
|
||||
iniText: "[TRAJ]\nCOORDINATES = X Y Z A C\n",
|
||||
});
|
||||
assert.equal(sdk.stageFile("programs/comment-lines.ngc", [
|
||||
"(comment before first move)",
|
||||
"",
|
||||
"G1 X1 F60",
|
||||
"; inline comment-only line",
|
||||
"G1 X2 F60",
|
||||
"M2",
|
||||
].join("\n")), 0);
|
||||
assert.equal(sdk.openProgram("programs/comment-lines.ngc"), 0);
|
||||
assert.equal(sdk.loadProgramMotionPlan({
|
||||
programPath: "programs/comment-lines.ngc",
|
||||
segments: [
|
||||
{
|
||||
line: 3,
|
||||
type: "STRAIGHT_FEED",
|
||||
motionClass: "feed",
|
||||
feedMode: "units-per-minute",
|
||||
startSeconds: 0,
|
||||
durationSeconds: 1,
|
||||
velocityMmPerMin: 60,
|
||||
startAxes: { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0 },
|
||||
endAxes: { x: 1, y: 0, z: 0, a: 0, b: 0, c: 0 },
|
||||
},
|
||||
{
|
||||
line: 5,
|
||||
type: "STRAIGHT_FEED",
|
||||
motionClass: "feed",
|
||||
feedMode: "units-per-minute",
|
||||
startSeconds: 1,
|
||||
durationSeconds: 1,
|
||||
velocityMmPerMin: 60,
|
||||
startAxes: { x: 1, y: 0, z: 0, a: 0, b: 0, c: 0 },
|
||||
endAxes: { x: 2, y: 0, z: 0, a: 0, b: 0, c: 0 },
|
||||
},
|
||||
],
|
||||
}), 0);
|
||||
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" }), 0);
|
||||
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" }), 0);
|
||||
assert.equal(sdk.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 }), 0);
|
||||
assert.equal(sdk.runCycles({ taskCycles: 1, taskPeriodNs: 500000000, servoPeriodNs: 1000000 }), 0);
|
||||
status = sdk.readStatus();
|
||||
assert.equal(status.task.openedLineCount, 6);
|
||||
assert.equal(status.task.openedSourceLineCount, 6);
|
||||
assert.equal(status.task.executableLineCount, 3);
|
||||
assert.equal(status.motionStatus.motion.programLine, 3);
|
||||
assert.equal(status.halSnapshot.pins["motion.program-line"].value, 3);
|
||||
assert.equal(sdk.runCycles({ taskCycles: 1, taskPeriodNs: 600000000, servoPeriodNs: 1000000 }), 0);
|
||||
status = sdk.readStatus();
|
||||
assert.equal(status.motionStatus.motion.programLine, 5);
|
||||
assert.equal(status.halSnapshot.pins["motion.program-line"].value, 5);
|
||||
assert.equal(status.task.nextProgramLine, 5);
|
||||
assert.equal(sdk.runCycles({ taskCycles: 1, taskPeriodNs: 1000000000, servoPeriodNs: 1000000 }), 0);
|
||||
status = sdk.readStatus();
|
||||
assert.equal(status.task.nextProgramLine, 6);
|
||||
|
||||
console.log("linuxcnc_task_hal_sdk=ok");
|
||||
console.log("task_hal_sdk_status_snapshot=ok");
|
||||
console.log("task_hal_feed_timed_motion_plan=ok");
|
||||
console.log("task_hal_comment_source_line_numbers=ok");
|
||||
|
||||
@@ -52,7 +52,7 @@ link_wasm_module \
|
||||
-s ENVIRONMENT=web,node \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s NO_EXIT_RUNTIME=1 \
|
||||
-s EXPORTED_FUNCTIONS='["_malloc","_free","_hal_init","_hal_ready","_hal_exit","_hal_malloc","_hal_pin_bit_new","_hal_pin_float_new","_hal_pin_s32_new","_hal_pin_u32_new","_hal_pin_s64_new","_hal_pin_u64_new","_hal_pin_bit_newf","_hal_pin_float_newf","_hal_pin_s32_newf","_hal_pin_u32_newf","_hal_pin_s64_newf","_hal_pin_u64_newf","_hal_param_bit_new","_hal_param_float_new","_hal_param_s32_new","_hal_param_u32_new","_hal_param_s64_new","_hal_param_u64_new","_hal_param_bit_newf","_hal_param_float_newf","_hal_param_s32_newf","_hal_param_u32_newf","_hal_param_s64_newf","_hal_param_u64_newf","_hal_get_pin_value_by_name","_hal_get_signal_value_by_name","_hal_get_param_value_by_name","_hal_link","_hal_unlink","_hal_set_p","_hal_get_p","_hal_create_thread","_hal_add_funct_to_thread","_hal_del_funct_from_thread","_hal_start_threads","_hal_stop_threads","_lchal_init_runtime","_lchal_load_hal_file","_lchal_set_pin_float","_lchal_set_pin_s32","_lchal_set_pin_bit","_lchal_get_pin_json","_lchal_get_snapshot_json","_lchal_step_threads","_lchal_reset_runtime","_lcmot_init_from_ini","_lcmot_write_command_json","_lcmot_step_servo","_lcmot_read_status_json","_lcmot_read_hal_snapshot_json","_lcmot_reset","_lctask_init_session","_lctask_stage_file","_lctask_open_program","_lctask_send_command_json","_lctask_run_cycles","_lctask_read_status_json","_lctask_read_events_json","_lctask_reset_session"]' \
|
||||
-s EXPORTED_FUNCTIONS='["_malloc","_free","_hal_init","_hal_ready","_hal_exit","_hal_malloc","_hal_pin_bit_new","_hal_pin_float_new","_hal_pin_s32_new","_hal_pin_u32_new","_hal_pin_s64_new","_hal_pin_u64_new","_hal_pin_bit_newf","_hal_pin_float_newf","_hal_pin_s32_newf","_hal_pin_u32_newf","_hal_pin_s64_newf","_hal_pin_u64_newf","_hal_param_bit_new","_hal_param_float_new","_hal_param_s32_new","_hal_param_u32_new","_hal_param_s64_new","_hal_param_u64_new","_hal_param_bit_newf","_hal_param_float_newf","_hal_param_s32_newf","_hal_param_u32_newf","_hal_param_s64_newf","_hal_param_u64_newf","_hal_get_pin_value_by_name","_hal_get_signal_value_by_name","_hal_get_param_value_by_name","_hal_link","_hal_unlink","_hal_set_p","_hal_get_p","_hal_create_thread","_hal_add_funct_to_thread","_hal_del_funct_from_thread","_hal_start_threads","_hal_stop_threads","_lchal_init_runtime","_lchal_load_hal_file","_lchal_set_pin_float","_lchal_set_pin_s32","_lchal_set_pin_bit","_lchal_get_pin_json","_lchal_get_snapshot_json","_lchal_step_threads","_lchal_reset_runtime","_lcmot_init_from_ini","_lcmot_write_command_json","_lcmot_step_servo","_lcmot_read_status_json","_lcmot_read_hal_snapshot_json","_lcmot_reset","_lctask_init_session","_lctask_stage_file","_lctask_open_program","_lctask_load_program_motion_plan_json","_lctask_send_command_json","_lctask_run_cycles","_lctask_read_status_json","_lctask_read_events_json","_lctask_reset_session"]' \
|
||||
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString","stringToUTF8","lengthBytesUTF8","getValue","setValue"]'
|
||||
|
||||
echo "linuxcnc_task_hal_wasm_build=ok"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"build": "node scripts/build-static.mjs",
|
||||
"dev": "python3 -m http.server 4173",
|
||||
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_run_preconditions.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_run_preconditions.mjs && node ../tests/node/verify_run_feedback_loop.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs && node ../tests/node/verify_linear_unit_conversion.mjs"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
linearUnitsToMillimetersFactor,
|
||||
linearValueToMillimeters,
|
||||
resolveStateLinearUnits,
|
||||
} from "./linear-units.js";
|
||||
|
||||
const LINEAR_AXES = ["x", "y", "z", "u", "v", "w"];
|
||||
const ANGULAR_AXES = ["a", "b", "c"];
|
||||
const ALL_AXES = [...LINEAR_AXES, ...ANGULAR_AXES];
|
||||
@@ -9,14 +15,17 @@ export function buildProgramExecutionTiming({
|
||||
rapidOverride = 100,
|
||||
defaultFeedRate = 100,
|
||||
} = {}) {
|
||||
const limits = buildVelocityLimits(profile);
|
||||
const linearUnits = resolveStateLinearUnits(profile);
|
||||
const limits = buildVelocityLimits(profile, linearUnits);
|
||||
const segments = [];
|
||||
let previousAxes = null;
|
||||
let previousLinearUnits = linearUnits;
|
||||
let elapsedSeconds = 0;
|
||||
let feedRate = Number(defaultFeedRate) > 0 ? Number(defaultFeedRate) : 100;
|
||||
|
||||
for (let index = 0; index < motion.length; index += 1) {
|
||||
const event = motion[index];
|
||||
const eventLinearUnits = event.linearUnits || linearUnits;
|
||||
const axes = normalizeAxes(event.axes, previousAxes);
|
||||
if (Number.isFinite(event.feedRate) && event.feedRate > 0) {
|
||||
feedRate = event.feedRate;
|
||||
@@ -31,6 +40,8 @@ export function buildProgramExecutionTiming({
|
||||
feedOverride,
|
||||
rapidOverride,
|
||||
elapsedSeconds,
|
||||
linearUnits: eventLinearUnits,
|
||||
previousLinearUnits: previousAxes ? previousLinearUnits : eventLinearUnits,
|
||||
});
|
||||
elapsedSeconds += segment.durationSeconds;
|
||||
segments.push({
|
||||
@@ -38,6 +49,7 @@ export function buildProgramExecutionTiming({
|
||||
elapsedSeconds,
|
||||
});
|
||||
previousAxes = axes;
|
||||
previousLinearUnits = eventLinearUnits;
|
||||
}
|
||||
|
||||
const feedSeconds = segments
|
||||
@@ -58,6 +70,7 @@ export function buildProgramExecutionTiming({
|
||||
motionCount: segments.length,
|
||||
segments,
|
||||
limits,
|
||||
linearUnits,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,14 +95,22 @@ function buildTimingSegment({
|
||||
feedOverride,
|
||||
rapidOverride,
|
||||
elapsedSeconds,
|
||||
linearUnits,
|
||||
previousLinearUnits,
|
||||
}) {
|
||||
const deltas = Object.fromEntries(ALL_AXES.map((axis) => [axis, axes[axis] - previousAxes[axis]]));
|
||||
const linearDistanceMm = vectorLength(LINEAR_AXES.map((axis) => deltas[axis]));
|
||||
const linearDistanceMm = vectorLength(LINEAR_AXES.map((axis) => (
|
||||
linearValueToMillimeters(axes[axis], linearUnits)
|
||||
- linearValueToMillimeters(previousAxes[axis], previousLinearUnits || linearUnits)
|
||||
)));
|
||||
const angularDistanceDeg = vectorLength(ANGULAR_AXES.map((axis) => deltas[axis]));
|
||||
const motionClass = event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed";
|
||||
const feedMode = event.feedMode === "inverse-time" ? "inverse-time" : "units-per-minute";
|
||||
const requestedLinearVelocity = motionClass === "rapid"
|
||||
? limits.maxLinearVelocityMmPerMin * percent(rapidOverride)
|
||||
: Math.max(feedRate, 0) * percent(feedOverride);
|
||||
: feedMode === "inverse-time"
|
||||
? inverseTimeVelocityMmPerMin(linearDistanceMm, angularDistanceDeg, feedRate)
|
||||
: linearValueToMillimeters(Math.max(feedRate, 0), linearUnits) * percent(feedOverride);
|
||||
const cappedLinearVelocity = Math.min(
|
||||
requestedLinearVelocity || limits.defaultLinearVelocityMmPerMin,
|
||||
limits.maxLinearVelocityMmPerMin,
|
||||
@@ -99,17 +120,25 @@ function buildTimingSegment({
|
||||
: 0;
|
||||
const angularVelocityDegPerMin = motionClass === "rapid"
|
||||
? limits.maxAngularVelocityDegPerMin * percent(rapidOverride)
|
||||
: Math.min(Math.max(feedRate, 0) * percent(feedOverride), limits.maxAngularVelocityDegPerMin);
|
||||
: feedMode === "inverse-time"
|
||||
? inverseTimeAngularVelocityDegPerMin(angularDistanceDeg, feedRate)
|
||||
: Math.min(Math.max(feedRate, 0) * percent(feedOverride), limits.maxAngularVelocityDegPerMin);
|
||||
const angularSeconds = angularDistanceDeg > 0
|
||||
? angularDistanceDeg / Math.max(angularVelocityDegPerMin / 60, 0.000001)
|
||||
: 0;
|
||||
const durationSeconds = Math.max(linearSeconds, angularSeconds);
|
||||
const inverseTimeSeconds = motionClass === "feed" && feedMode === "inverse-time" && feedRate > 0
|
||||
? 60 / feedRate
|
||||
: 0;
|
||||
const durationSeconds = inverseTimeSeconds > 0
|
||||
? inverseTimeSeconds
|
||||
: Math.max(linearSeconds, angularSeconds);
|
||||
|
||||
return {
|
||||
index,
|
||||
line: event.line,
|
||||
type: event.type,
|
||||
motionClass,
|
||||
feedMode,
|
||||
linearDistanceMm,
|
||||
angularDistanceDeg,
|
||||
feedRate,
|
||||
@@ -118,20 +147,28 @@ function buildTimingSegment({
|
||||
angularVelocityDegPerMin,
|
||||
durationSeconds,
|
||||
startSeconds: elapsedSeconds,
|
||||
startAxes: previousAxes,
|
||||
endAxes: axes,
|
||||
axes,
|
||||
deltas,
|
||||
linearUnits,
|
||||
};
|
||||
}
|
||||
|
||||
function buildVelocityLimits(profile) {
|
||||
function buildVelocityLimits(profile, linearUnits) {
|
||||
const traj = profile?.traj || {};
|
||||
const axisLimits = profile?.axisLimits || {};
|
||||
const linearVelocityScale = linearUnitsToMillimetersFactor(linearUnits);
|
||||
const maxLinearVelocity = firstFinite(
|
||||
Number(traj.maxLinearVelocity) * 60,
|
||||
...LINEAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * 60),
|
||||
Number(traj.maxLinearVelocity) * linearVelocityScale * 60,
|
||||
...LINEAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * linearVelocityScale * 60),
|
||||
2100,
|
||||
);
|
||||
const defaultLinearVelocity = firstFinite(Number(traj.defaultLinearVelocity) * 60, maxLinearVelocity, 1200);
|
||||
const defaultLinearVelocity = firstFinite(
|
||||
Number(traj.defaultLinearVelocity) * linearVelocityScale * 60,
|
||||
maxLinearVelocity,
|
||||
1200,
|
||||
);
|
||||
const maxAngularVelocity = firstFinite(
|
||||
...ANGULAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * 60),
|
||||
maxLinearVelocity,
|
||||
@@ -161,6 +198,24 @@ function percent(value) {
|
||||
return Number.isFinite(number) ? Math.max(number, 0) / 100 : 1;
|
||||
}
|
||||
|
||||
function inverseTimeVelocityMmPerMin(linearDistanceMm, angularDistanceDeg, feedRate) {
|
||||
const durationSeconds = feedRate > 0 ? 60 / feedRate : 0;
|
||||
if (linearDistanceMm > 0 && durationSeconds > 0) {
|
||||
return (linearDistanceMm / durationSeconds) * 60;
|
||||
}
|
||||
if (angularDistanceDeg > 0 && durationSeconds > 0) {
|
||||
return angularDistanceDeg / durationSeconds * 60;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function inverseTimeAngularVelocityDegPerMin(angularDistanceDeg, feedRate) {
|
||||
const durationSeconds = feedRate > 0 ? 60 / feedRate : 0;
|
||||
return angularDistanceDeg > 0 && durationSeconds > 0
|
||||
? angularDistanceDeg / durationSeconds * 60
|
||||
: 0;
|
||||
}
|
||||
|
||||
function firstFinite(...values) {
|
||||
return values.find((value) => Number.isFinite(value) && value > 0) || 1;
|
||||
}
|
||||
|
||||
60
web-rtcp-5axis-sim-plan/app/src/runtime/linear-units.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const UNIT_ALIASES = new Map([
|
||||
["mm", "mm"],
|
||||
["millimeter", "mm"],
|
||||
["millimeters", "mm"],
|
||||
["millimetre", "mm"],
|
||||
["millimetres", "mm"],
|
||||
["metric", "mm"],
|
||||
["inch", "inch"],
|
||||
["inches", "inch"],
|
||||
["in", "inch"],
|
||||
["imperial", "inch"],
|
||||
["m", "m"],
|
||||
["meter", "m"],
|
||||
["meters", "m"],
|
||||
["metre", "m"],
|
||||
["metres", "m"],
|
||||
]);
|
||||
|
||||
const METERS_PER_UNIT = {
|
||||
mm: 0.001,
|
||||
inch: 0.0254,
|
||||
m: 1,
|
||||
};
|
||||
|
||||
export function normalizeLinearUnits(units, fallback = "mm") {
|
||||
const key = String(units || "").trim().toLowerCase();
|
||||
return UNIT_ALIASES.get(key) || UNIT_ALIASES.get(String(fallback || "mm").trim().toLowerCase()) || "mm";
|
||||
}
|
||||
|
||||
export function linearUnitsToMetersFactor(units) {
|
||||
return METERS_PER_UNIT[normalizeLinearUnits(units)] || METERS_PER_UNIT.mm;
|
||||
}
|
||||
|
||||
export function linearUnitsToMillimetersFactor(units) {
|
||||
return linearUnitsToMetersFactor(units) * 1000;
|
||||
}
|
||||
|
||||
export function linearValueToMeters(value, units) {
|
||||
const number = Number(value) || 0;
|
||||
return number * linearUnitsToMetersFactor(units);
|
||||
}
|
||||
|
||||
export function linearValueToMillimeters(value, units) {
|
||||
const number = Number(value) || 0;
|
||||
return number * linearUnitsToMillimetersFactor(units);
|
||||
}
|
||||
|
||||
export function linearUnitsLabel(units) {
|
||||
return normalizeLinearUnits(units);
|
||||
}
|
||||
|
||||
export function resolveStateLinearUnits(stateOrProfile, fallback = "mm") {
|
||||
return normalizeLinearUnits(
|
||||
stateOrProfile?.programExecution?.summary?.linearUnits
|
||||
|| stateOrProfile?.linuxCncIniConfig?.traj?.linearUnits
|
||||
|| stateOrProfile?.profile?.traj?.linearUnits
|
||||
|| stateOrProfile?.traj?.linearUnits,
|
||||
fallback,
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,17 @@ export function parseLinuxCncIni(text, { path = "inline.ini", profileId = "unkno
|
||||
const remaps = parseRemaps(sections);
|
||||
const hal = parseHal(sections);
|
||||
const display = parseDisplay(sections);
|
||||
const emcmot = {
|
||||
module: getFirstValue(sections, "EMCMOT", "EMCMOT") || null,
|
||||
servoPeriodNs: numberOrNull(getFirstValue(sections, "EMCMOT", "SERVO_PERIOD")),
|
||||
};
|
||||
const task = {
|
||||
module: getFirstValue(sections, "TASK", "TASK") || null,
|
||||
cycleTimeSeconds: numberOrNull(getFirstValue(sections, "TASK", "CYCLE_TIME")),
|
||||
};
|
||||
const emcio = {
|
||||
toolTable: getFirstValue(sections, "EMCIO", "TOOL_TABLE") || null,
|
||||
};
|
||||
const halui = {
|
||||
mdiCommands: getValues(sections, "HALUI", "MDI_COMMAND"),
|
||||
};
|
||||
@@ -20,6 +31,7 @@ export function parseLinuxCncIni(text, { path = "inline.ini", profileId = "unkno
|
||||
apiName: "web-rtcp-5axis-linuxcnc-ini-config",
|
||||
profileId,
|
||||
path,
|
||||
sourceText: text,
|
||||
machineName: getFirstValue(sections, "EMC", "MACHINE") || null,
|
||||
kinematics: parseKinematics(kinsText),
|
||||
kinematicsModuleId,
|
||||
@@ -48,10 +60,27 @@ export function parseLinuxCncIni(text, { path = "inline.ini", profileId = "unkno
|
||||
halui,
|
||||
axisLimits,
|
||||
jointConfig,
|
||||
emcio: {
|
||||
toolTable: getFirstValue(sections, "EMCIO", "TOOL_TABLE") || null,
|
||||
},
|
||||
validation: validateIniConfig({ coordinates, jointCount, axisLimits, jointConfig, kinsText }),
|
||||
emcmot,
|
||||
task,
|
||||
emcio,
|
||||
validation: validateIniConfig({
|
||||
sections,
|
||||
coordinates,
|
||||
jointCount,
|
||||
axisLimits,
|
||||
jointConfig,
|
||||
kinsText,
|
||||
remaps,
|
||||
hal,
|
||||
halui,
|
||||
rs274ngc: {
|
||||
halPinVars: boolFromIni(getFirstValue(sections, "RS274NGC", "HAL_PIN_VARS")),
|
||||
parameterFile: getFirstValue(sections, "RS274NGC", "PARAMETER_FILE") || null,
|
||||
},
|
||||
emcmot,
|
||||
task,
|
||||
emcio,
|
||||
}),
|
||||
semanticBoundary: "linuxcnc_ini_file_browser_parser",
|
||||
};
|
||||
}
|
||||
@@ -130,6 +159,18 @@ export function applyIniConfigToProfile(profile, iniConfig) {
|
||||
},
|
||||
},
|
||||
halui: iniConfig.halui.mdiCommands.length > 0 ? iniConfig.halui : profile.halui,
|
||||
emcmot: {
|
||||
...profile.emcmot,
|
||||
...dropNullish(iniConfig.emcmot || {}),
|
||||
},
|
||||
task: {
|
||||
...profile.task,
|
||||
...dropNullish(iniConfig.task || {}),
|
||||
},
|
||||
emcio: {
|
||||
...profile.emcio,
|
||||
...dropNullish(iniConfig.emcio || {}),
|
||||
},
|
||||
traj: {
|
||||
...profile.traj,
|
||||
...dropNullish(iniConfig.traj),
|
||||
@@ -297,17 +338,73 @@ function inferSwitchkinsTypes({ coordinates, halui, kinematicsModuleId }) {
|
||||
}));
|
||||
}
|
||||
|
||||
function validateIniConfig({ coordinates, jointCount, axisLimits, jointConfig, kinsText }) {
|
||||
function validateIniConfig({
|
||||
sections,
|
||||
coordinates,
|
||||
jointCount,
|
||||
axisLimits,
|
||||
jointConfig,
|
||||
kinsText,
|
||||
remaps,
|
||||
hal,
|
||||
halui,
|
||||
rs274ngc,
|
||||
emcmot,
|
||||
task,
|
||||
emcio,
|
||||
}) {
|
||||
const missing = [];
|
||||
const requiredSections = [
|
||||
"EMC",
|
||||
"DISPLAY",
|
||||
"RS274NGC",
|
||||
"KINS",
|
||||
"HAL",
|
||||
"HALUI",
|
||||
"TRAJ",
|
||||
"EMCMOT",
|
||||
"TASK",
|
||||
"EMCIO",
|
||||
];
|
||||
for (const section of requiredSections) {
|
||||
if (!sections.has(section)) missing.push(`[${section}]`);
|
||||
}
|
||||
if (!["XYZAC", "XYZBC"].includes(coordinates)) missing.push("TRAJ.COORDINATES XYZAC/XYZBC");
|
||||
if (!coordinates) missing.push("TRAJ.COORDINATES");
|
||||
if (!jointCount) missing.push("KINS.JOINTS");
|
||||
if (jointCount !== 5) missing.push("KINS.JOINTS=5");
|
||||
if (!kinsText) missing.push("KINS.KINEMATICS");
|
||||
if (!String(kinsText || "").includes("sparm=identityfirst")) {
|
||||
missing.push("KINS.KINEMATICS sparm=identityfirst");
|
||||
}
|
||||
for (const axis of String(coordinates || "").split("")) {
|
||||
if (!axisLimits[axis]) missing.push(`AXIS_${axis}`);
|
||||
}
|
||||
if (jointCount && jointConfig.length !== jointCount) {
|
||||
missing.push(`JOINT_ count ${jointConfig.length}/${jointCount}`);
|
||||
}
|
||||
for (let joint = 0; joint < 5; joint += 1) {
|
||||
if (!sections.has(`JOINT_${joint}`)) missing.push(`JOINT_${joint}`);
|
||||
}
|
||||
for (const code of ["M428", "M429", "M430"]) {
|
||||
if (!remaps.some((remap) => remap.code === code && remap.ngc)) {
|
||||
missing.push(`RS274NGC.REMAP ${code}`);
|
||||
}
|
||||
}
|
||||
if (rs274ngc.halPinVars !== true) missing.push("RS274NGC.HAL_PIN_VARS=1");
|
||||
if (!rs274ngc.parameterFile) missing.push("RS274NGC.PARAMETER_FILE");
|
||||
if (!hal.halui) missing.push("HAL.HALUI");
|
||||
if (!hal.halFiles.length) missing.push("HAL.HALFILE");
|
||||
if (!hal.postguiHalFiles.length) missing.push("HAL.POSTGUI_HALFILE");
|
||||
if (!hal.halcmd.some((line) => line.includes("motion.analog-out-03") && line.includes("motion.switchkins-type"))) {
|
||||
missing.push("HAL.HALCMD motion.analog-out-03=>motion.switchkins-type");
|
||||
}
|
||||
if (halui.mdiCommands.length < 3) missing.push("HALUI.MDI_COMMAND M428/M429/M430");
|
||||
if (!emcmot.module) missing.push("EMCMOT.EMCMOT");
|
||||
if (!emcmot.servoPeriodNs) missing.push("EMCMOT.SERVO_PERIOD");
|
||||
if (!task.module) missing.push("TASK.TASK");
|
||||
if (!task.cycleTimeSeconds) missing.push("TASK.CYCLE_TIME");
|
||||
if (!emcio.toolTable) missing.push("EMCIO.TOOL_TABLE");
|
||||
return {
|
||||
ready: missing.length === 0,
|
||||
missing,
|
||||
|
||||
@@ -234,11 +234,15 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
|
||||
const axes = Object.fromEntries(AXES.map((axis) => [axis, 0]));
|
||||
const sourceLines = programLineMap(programText);
|
||||
const feedRatesByLine = feedRatesBySourceLine(programText);
|
||||
const feedModesByLine = feedModesBySourceLine(programText);
|
||||
const linearUnitsByLine = linearUnitsBySourceLine(programText);
|
||||
const switchkinsByLine = switchkinsEventsByLine(switchkinsEvents);
|
||||
const motion = [];
|
||||
let activePlane = 170;
|
||||
let activeSwitchkinsEvent = null;
|
||||
let activeFeedRate = null;
|
||||
let activeFeedMode = "units-per-minute";
|
||||
let activeLinearUnits = "mm";
|
||||
|
||||
for (const line of String(resultText).split("\n")) {
|
||||
const feedRate = readCanonicalNumber(line, "feed_rate");
|
||||
@@ -289,6 +293,8 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
|
||||
if (Number.isFinite(sourceFeedRate) && sourceFeedRate > 0) {
|
||||
activeFeedRate = sourceFeedRate;
|
||||
}
|
||||
activeFeedMode = latestFeedModeAtOrBeforeLine(feedModesByLine, sourceLine) || activeFeedMode;
|
||||
activeLinearUnits = latestLinearUnitsAtOrBeforeLine(linearUnitsByLine, sourceLine) || activeLinearUnits;
|
||||
}
|
||||
motion.push({
|
||||
type: event[1],
|
||||
@@ -300,6 +306,8 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
|
||||
switchkinsCode: activeSwitchkinsEvent?.code || null,
|
||||
switchkinsRemapBoundary: activeSwitchkinsEvent ? SWITCHKINS_REMAP_BOUNDARY : null,
|
||||
feedRate: activeFeedRate,
|
||||
feedMode: activeFeedMode,
|
||||
linearUnits: activeLinearUnits,
|
||||
raw: line,
|
||||
});
|
||||
}
|
||||
@@ -307,6 +315,56 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
|
||||
return motion;
|
||||
}
|
||||
|
||||
function feedModesBySourceLine(programText) {
|
||||
const modes = [{ line: 0, feedMode: "units-per-minute" }];
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
const codeOnly = stripComments(line);
|
||||
let activeMode = null;
|
||||
for (const match of codeOnly.matchAll(/\bG\s*([0-9]+(?:\.[0-9]+)?)\b/gi)) {
|
||||
const value = Number(match[1]);
|
||||
if (value === 93) activeMode = "inverse-time";
|
||||
if (value === 94) activeMode = "units-per-minute";
|
||||
}
|
||||
if (activeMode) {
|
||||
modes.push({ line: index + 1, feedMode: activeMode });
|
||||
}
|
||||
});
|
||||
return modes;
|
||||
}
|
||||
|
||||
function latestFeedModeAtOrBeforeLine(modes, sourceLine) {
|
||||
let feedMode = "units-per-minute";
|
||||
for (const entry of modes) {
|
||||
if (entry.line <= sourceLine) feedMode = entry.feedMode;
|
||||
}
|
||||
return feedMode;
|
||||
}
|
||||
|
||||
function linearUnitsBySourceLine(programText) {
|
||||
const units = [{ line: 0, linearUnits: "mm" }];
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
const codeOnly = stripComments(line);
|
||||
let activeUnits = null;
|
||||
for (const match of codeOnly.matchAll(/\bG\s*([0-9]+(?:\.[0-9]+)?)\b/gi)) {
|
||||
const value = Number(match[1]);
|
||||
if (value === 20) activeUnits = "inch";
|
||||
if (value === 21) activeUnits = "mm";
|
||||
}
|
||||
if (activeUnits) {
|
||||
units.push({ line: index + 1, linearUnits: activeUnits });
|
||||
}
|
||||
});
|
||||
return units;
|
||||
}
|
||||
|
||||
function latestLinearUnitsAtOrBeforeLine(units, sourceLine) {
|
||||
let linearUnits = "mm";
|
||||
for (const entry of units) {
|
||||
if (entry.line <= sourceLine) linearUnits = entry.linearUnits;
|
||||
}
|
||||
return linearUnits;
|
||||
}
|
||||
|
||||
function feedRatesBySourceLine(programText) {
|
||||
const rates = [];
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
|
||||
@@ -93,6 +93,17 @@ export function wrapTaskHalSdk(sdk, {
|
||||
return rc;
|
||||
},
|
||||
|
||||
loadProgramMotionPlan(plan = {}) {
|
||||
if (typeof sdk.loadProgramMotionPlan !== "function") {
|
||||
throw new Error("task/HAL SDK missing loadProgramMotionPlan; rebuild wasm-port/tools/build_task_hal_wasm.sh");
|
||||
}
|
||||
const rc = sdk.loadProgramMotionPlan(plan);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_load_program_motion_plan_json failed rc=${rc}`);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
|
||||
sendCommand(command) {
|
||||
const rc = sdk.sendCommand(command);
|
||||
if (rc !== 0) {
|
||||
@@ -152,10 +163,65 @@ export function buildTaskHalSessionFromMachineFiles({ profile, plan, save, selec
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTaskHalProgramMotionPlan({
|
||||
programPath = null,
|
||||
motion = [],
|
||||
timing = null,
|
||||
linearUnits = "mm",
|
||||
programLines = [],
|
||||
} = {}) {
|
||||
const segments = Array.isArray(timing?.segments) ? timing.segments : [];
|
||||
let planSegments = segments.map((segment, index) => {
|
||||
const event = motion[index] || {};
|
||||
const startAxes = normalizePlanAxes(segment.startAxes || motion[index - 1]?.axes || {});
|
||||
const endAxes = normalizePlanAxes(segment.endAxes || segment.axes || event.axes || startAxes);
|
||||
return {
|
||||
line: Number(segment.line ?? event.line ?? index + 1),
|
||||
type: segment.type || event.type || "STRAIGHT_FEED",
|
||||
motionClass: segment.motionClass || (event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed"),
|
||||
feedMode: segment.feedMode || event.feedMode || "units-per-minute",
|
||||
startSeconds: Number(segment.startSeconds || 0),
|
||||
durationSeconds: Math.max(Number(segment.durationSeconds || 0), 0),
|
||||
elapsedSeconds: Number(segment.elapsedSeconds || 0),
|
||||
feedRate: Number(segment.feedRate || event.feedRate || 0),
|
||||
linearUnits: segment.linearUnits || event.linearUnits || linearUnits,
|
||||
velocityMmPerMin: Math.max(Number(segment.velocityMmPerMin || 0), 0),
|
||||
requestedVelocityMmPerMin: Math.max(Number(segment.requestedVelocityMmPerMin || segment.velocityMmPerMin || 0), 0),
|
||||
startAxes,
|
||||
endAxes,
|
||||
};
|
||||
}).filter((segment) => segment.durationSeconds > 0 || segment.line > 0);
|
||||
const lineSegments = buildSourceLineMotionSegments({
|
||||
programLines,
|
||||
seedSegments: planSegments,
|
||||
linearUnits,
|
||||
});
|
||||
if (shouldUseSourceLineSegments(planSegments, lineSegments)) {
|
||||
planSegments = lineSegments;
|
||||
}
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-program-motion-plan",
|
||||
semanticBoundary: "linuxcnc_canonical_motion_feed_timed_task_hal_plan",
|
||||
programPath,
|
||||
linearUnits,
|
||||
totalSeconds: Number(timing?.totalSeconds || 0),
|
||||
segmentCount: planSegments.length,
|
||||
segments: planSegments,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeTaskHalStatus(status = {}) {
|
||||
const motion = status.motionStatus?.motion || {};
|
||||
const axis = status.motionStatus?.axis || {};
|
||||
const halPins = status.halSnapshot?.pins || {};
|
||||
const motionProgramLine = Number(motion.programLine || 0);
|
||||
const halProgramLine = Number(halPins["motion.program-line"]?.value || 0);
|
||||
const activeLine = motionProgramLine > 0
|
||||
? motionProgramLine
|
||||
: halProgramLine > 0
|
||||
? halProgramLine
|
||||
: 1;
|
||||
return {
|
||||
...status,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
@@ -183,7 +249,13 @@ export function normalizeTaskHalStatus(status = {}) {
|
||||
halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins)
|
||||
? status.halSnapshot.changedPins.length
|
||||
: Number(status.halSnapshot?.changedPinCount || 0),
|
||||
activeLine: Number(motion.programLine || halPins["motion.program-line"]?.value || 1),
|
||||
activeLine,
|
||||
motionProgramLine,
|
||||
halProgramLine,
|
||||
activeLineSource: motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback",
|
||||
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0
|
||||
? motionProgramLine === halProgramLine
|
||||
: false,
|
||||
switchkinsType: Number(motion.switchkinsType ?? halPins["motion.switchkins-type"]?.value ?? 0),
|
||||
axisPose: {
|
||||
x: Number(axis.x ?? halPins["axis.0.pos-cmd"]?.value ?? 0),
|
||||
@@ -199,6 +271,133 @@ export function normalizeTaskHalStatus(status = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function shouldUseSourceLineSegments(planSegments, lineSegments) {
|
||||
if (lineSegments.length < 3) return false;
|
||||
const plannedLines = new Set(planSegments.map((segment) => Number(segment.line)).filter(Number.isFinite));
|
||||
const lineCount = lineSegments.length;
|
||||
return plannedLines.size <= 2 && lineCount > plannedLines.size;
|
||||
}
|
||||
|
||||
function buildSourceLineMotionSegments({
|
||||
programLines = [],
|
||||
seedSegments = [],
|
||||
linearUnits = "mm",
|
||||
} = {}) {
|
||||
if (!Array.isArray(programLines) || programLines.length === 0) return [];
|
||||
const seedByLine = new Map(seedSegments.map((segment) => [Number(segment.line), segment]));
|
||||
const axes = normalizePlanAxes(seedSegments[0]?.startAxes || {});
|
||||
const segments = [];
|
||||
let elapsedSeconds = 0;
|
||||
let feedRate = firstPositive(seedSegments.map((segment) => segment.feedRate), 100);
|
||||
let rapidVelocity = firstPositive(
|
||||
seedSegments.filter((segment) => segment.motionClass === "rapid").map((segment) => segment.velocityMmPerMin),
|
||||
2100,
|
||||
);
|
||||
|
||||
for (let index = 0; index < programLines.length; index += 1) {
|
||||
const line = index + 1;
|
||||
const code = stripSourceLineComments(programLines[index]);
|
||||
if (!isExecutableGcodeLine(code)) continue;
|
||||
|
||||
const seed = seedByLine.get(line) || null;
|
||||
const startAxes = normalizePlanAxes(seed?.startAxes || axes);
|
||||
const parsed = parseGcodeLineMotion(code, startAxes, feedRate);
|
||||
if (parsed.feedRate > 0) feedRate = parsed.feedRate;
|
||||
const motionClass = seed?.motionClass || parsed.motionClass;
|
||||
const endAxes = normalizePlanAxes(seed?.endAxes || parsed.endAxes);
|
||||
const velocityMmPerMin = Number(seed?.velocityMmPerMin) > 0
|
||||
? Number(seed.velocityMmPerMin)
|
||||
: motionClass === "rapid"
|
||||
? rapidVelocity
|
||||
: Math.max(feedRate, 1);
|
||||
if (motionClass === "rapid" && velocityMmPerMin > 0) {
|
||||
rapidVelocity = velocityMmPerMin;
|
||||
}
|
||||
const durationSeconds = Math.max(
|
||||
Number(seed?.durationSeconds || 0),
|
||||
estimateLineDurationSeconds(startAxes, endAxes, velocityMmPerMin),
|
||||
0.05,
|
||||
);
|
||||
const segment = {
|
||||
line,
|
||||
type: seed?.type || parsed.type,
|
||||
motionClass,
|
||||
feedMode: seed?.feedMode || parsed.feedMode,
|
||||
startSeconds: elapsedSeconds,
|
||||
durationSeconds,
|
||||
elapsedSeconds: elapsedSeconds + durationSeconds,
|
||||
feedRate,
|
||||
linearUnits: seed?.linearUnits || linearUnits,
|
||||
velocityMmPerMin,
|
||||
requestedVelocityMmPerMin: Number(seed?.requestedVelocityMmPerMin || velocityMmPerMin),
|
||||
startAxes,
|
||||
endAxes,
|
||||
};
|
||||
segments.push(segment);
|
||||
Object.assign(axes, endAxes);
|
||||
elapsedSeconds += durationSeconds;
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
function parseGcodeLineMotion(code, startAxes, currentFeedRate) {
|
||||
const numberPattern = "[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)";
|
||||
const gCodes = [...code.matchAll(new RegExp(`\\bG\\s*(${numberPattern})\\b`, "gi"))].map((match) => Number(match[1]));
|
||||
const feedMode = gCodes.includes(93) ? "inverse-time" : "units-per-minute";
|
||||
const rapid = gCodes.includes(0);
|
||||
const feed = gCodes.some((value) => value === 1 || value === 2 || value === 3);
|
||||
const endAxes = { ...startAxes };
|
||||
for (const axis of ["x", "y", "z", "a", "b", "c", "u", "v", "w"]) {
|
||||
const match = code.match(new RegExp(`\\b${axis}\\s*(${numberPattern})`, "i"));
|
||||
if (match) endAxes[axis] = Number(match[1]);
|
||||
}
|
||||
const feedMatch = code.match(new RegExp(`\\bF\\s*(${numberPattern})`, "i"));
|
||||
const feedRate = feedMatch && Number(feedMatch[1]) > 0 ? Number(feedMatch[1]) : Number(currentFeedRate || 0);
|
||||
return {
|
||||
type: rapid ? "STRAIGHT_TRAVERSE" : "STRAIGHT_FEED",
|
||||
motionClass: rapid && !feed ? "rapid" : "feed",
|
||||
feedMode,
|
||||
feedRate,
|
||||
endAxes,
|
||||
};
|
||||
}
|
||||
|
||||
function stripSourceLineComments(line) {
|
||||
return String(line || "")
|
||||
.replace(/\([^)]*\)/g, " ")
|
||||
.replace(/;.*$/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isExecutableGcodeLine(code) {
|
||||
if (!code || code === "%") return false;
|
||||
return /\b[GMTXYZABCUVWF]\s*[-+]?\d/i.test(code);
|
||||
}
|
||||
|
||||
function estimateLineDurationSeconds(startAxes, endAxes, velocityMmPerMin) {
|
||||
const distance = Math.sqrt(["x", "y", "z"].reduce((total, axis) => {
|
||||
const delta = Number(endAxes[axis] || 0) - Number(startAxes[axis] || 0);
|
||||
return total + delta * delta;
|
||||
}, 0));
|
||||
if (distance <= 0 || velocityMmPerMin <= 0) return 0;
|
||||
return distance / Math.max(velocityMmPerMin / 60, 0.000001);
|
||||
}
|
||||
|
||||
function firstPositive(values, fallback) {
|
||||
for (const value of values) {
|
||||
const number = Number(value);
|
||||
if (Number.isFinite(number) && number > 0) return number;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizePlanAxes(axes = {}) {
|
||||
return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => [
|
||||
axis,
|
||||
Number.isFinite(Number(axes[axis])) ? Number(axes[axis]) : 0,
|
||||
]));
|
||||
}
|
||||
|
||||
function isJogMotion(motion = {}) {
|
||||
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export async function createLinuxCncTaskHalWorkerRuntime({
|
||||
initSession: (session) => client.call("initSession", { session }),
|
||||
stageFiles: (files) => client.call("stageFiles", { files }),
|
||||
openProgram: (path) => client.call("openProgram", { path }),
|
||||
loadProgramMotionPlan: (plan) => client.call("loadProgramMotionPlan", { plan }),
|
||||
sendCommand: (command) => client.call("command", { command }),
|
||||
runCycles: (options) => client.call("runCycles", { options }),
|
||||
readStatus: () => client.call("readStatus"),
|
||||
|
||||
@@ -33,6 +33,9 @@ async function handleMessage(type, payload) {
|
||||
case "openProgram":
|
||||
assertRuntime();
|
||||
return runtime.openProgram(payload.path);
|
||||
case "loadProgramMotionPlan":
|
||||
assertRuntime();
|
||||
return runtime.loadProgramMotionPlan(payload.plan || {});
|
||||
case "command":
|
||||
assertRuntime();
|
||||
return runtime.sendCommand(payload.command);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
stageProfileMachineFiles,
|
||||
} from "../runtime/linuxcnc-machine-file-staging.js";
|
||||
import {
|
||||
buildTaskHalProgramMotionPlan,
|
||||
buildTaskHalSessionFromMachineFiles,
|
||||
} from "../runtime/linuxcnc-task-hal-runtime.js";
|
||||
import {
|
||||
@@ -42,6 +43,40 @@ const initialAxisPose = {
|
||||
c: 0.0,
|
||||
};
|
||||
|
||||
function createTaskHalStatusLoopState({
|
||||
active = false,
|
||||
sequence = 0,
|
||||
profileId = null,
|
||||
iniPath = null,
|
||||
kinematicsModuleId = null,
|
||||
tickCount = 0,
|
||||
batchSize = 5,
|
||||
intervalMs = 25,
|
||||
taskPeriodNs = 10000000,
|
||||
servoPeriodNs = 1000000,
|
||||
lastStatusAt = null,
|
||||
lastError = null,
|
||||
stopReason = null,
|
||||
} = {}) {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-status-loop",
|
||||
active,
|
||||
sequence,
|
||||
profileId,
|
||||
iniPath,
|
||||
kinematicsModuleId,
|
||||
tickCount,
|
||||
batchSize,
|
||||
intervalMs,
|
||||
taskPeriodNs,
|
||||
servoPeriodNs,
|
||||
lastStatusAt,
|
||||
lastError,
|
||||
stopReason,
|
||||
semanticBoundary: "js_status_polling_loop_for_linuxcnc_task_hal_motion_status",
|
||||
};
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
machineProfile: "xyzac-trt",
|
||||
availableProfiles: fiveAxisProfiles.map(({ id, title, traj, kinematicsModuleId, kinematics }) => ({
|
||||
@@ -136,12 +171,14 @@ const initialState = {
|
||||
programExecutionMotionIndex: 0,
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: null,
|
||||
programRuntimeFeedbackHistory: [],
|
||||
taskHalRuntime: null,
|
||||
taskHalRuntimeReadiness: null,
|
||||
taskHalStatus: null,
|
||||
taskHalSession: null,
|
||||
taskHalExecutionPending: false,
|
||||
taskHalExecutionSequence: 0,
|
||||
taskHalStatusLoop: createTaskHalStatusLoopState(),
|
||||
taskHalFallbackReason: null,
|
||||
pendingJogCommand: null,
|
||||
interpreterExecutionPending: false,
|
||||
@@ -249,6 +286,7 @@ export function createSimulationStore(seed = {}) {
|
||||
};
|
||||
state.fullExecutionBoundary = createFullLinuxCncExecutionBoundary(state);
|
||||
const listeners = new Set();
|
||||
let taskHalStatusLoopTimer = null;
|
||||
|
||||
const notify = () => {
|
||||
for (const listener of listeners) {
|
||||
@@ -772,7 +810,7 @@ export function createSimulationStore(seed = {}) {
|
||||
},
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "auto",
|
||||
mode: state.machine.mode,
|
||||
},
|
||||
axisPose: initialAxisPose,
|
||||
runState: "idle",
|
||||
@@ -799,12 +837,51 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
break;
|
||||
case "TASK_HAL_STATUS_APPLIED":
|
||||
setState(applyTaskHalStatusPatch(state, action.status, action.operatorMessage));
|
||||
setState(applyTaskHalStatusPatch(state, action.status, action.operatorMessage, {
|
||||
loopSequence: action.loopSequence,
|
||||
preserveMachine: action.preserveMachine,
|
||||
}));
|
||||
break;
|
||||
case "TASK_HAL_STATUS_LOOP_STARTED":
|
||||
setState({
|
||||
taskHalStatusLoop: {
|
||||
...createTaskHalStatusLoopState({
|
||||
active: true,
|
||||
sequence: action.sequence,
|
||||
profileId: action.profileId,
|
||||
iniPath: action.iniPath,
|
||||
kinematicsModuleId: action.kinematicsModuleId,
|
||||
batchSize: action.batchSize,
|
||||
intervalMs: action.intervalMs,
|
||||
taskPeriodNs: action.taskPeriodNs,
|
||||
servoPeriodNs: action.servoPeriodNs,
|
||||
}),
|
||||
},
|
||||
programRuntimeFeedbackHistory: [],
|
||||
operatorMessage: action.operatorMessage || "task/HAL status loop running",
|
||||
});
|
||||
break;
|
||||
case "TASK_HAL_STATUS_LOOP_STOPPED":
|
||||
setState({
|
||||
taskHalStatusLoop: {
|
||||
...state.taskHalStatusLoop,
|
||||
active: false,
|
||||
stopReason: action.reason || "stopped",
|
||||
lastError: action.error || null,
|
||||
},
|
||||
operatorMessage: action.operatorMessage || state.operatorMessage,
|
||||
});
|
||||
break;
|
||||
case "TASK_HAL_COMMAND_FAILED":
|
||||
setState({
|
||||
taskHalFallbackReason: action.error,
|
||||
taskHalExecutionPending: false,
|
||||
taskHalStatusLoop: {
|
||||
...state.taskHalStatusLoop,
|
||||
active: false,
|
||||
lastError: action.error,
|
||||
stopReason: "error",
|
||||
},
|
||||
pendingJogCommand: null,
|
||||
operatorMessage: `task/HAL fallback: ${action.error}`,
|
||||
});
|
||||
@@ -850,13 +927,29 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
const turningOff = state.machine.taskState === "on" || state.machine.powerOn;
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: !turningOff,
|
||||
estopActive: false,
|
||||
taskState: turningOff ? "estop-reset" : "on",
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: turningOff ? "powered-off" : "idle",
|
||||
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
|
||||
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
|
||||
spindle: turningOff ? { ...state.spindle, enabled: false } : state.spindle,
|
||||
operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on",
|
||||
});
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_STATE", state: state.machine.powerOn ? "ESTOP_RESET" : "ON" },
|
||||
], { operatorMessage: state.machine.powerOn ? "task/HAL machine power off" : "task/HAL machine power on" }).catch(() => {});
|
||||
{ type: "EMC_TASK_SET_STATE", state: turningOff ? "ESTOP_RESET" : "ON" },
|
||||
], { operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on" }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const turningOff = state.machine.taskState === "on" || state.machine.powerOn;
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -1108,6 +1201,9 @@ export function createSimulationStore(seed = {}) {
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
stopTaskHalStatusLoop(action.type === "ABORT" ? "aborted" : "stopped", {
|
||||
operatorMessage: action.type === "ABORT" ? "task/HAL abort requested" : "task/HAL stop requested",
|
||||
});
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_ABORT" },
|
||||
], { operatorMessage: action.type === "ABORT" ? "task/HAL abort complete" : "task/HAL program stopped" }).catch(() => {});
|
||||
@@ -1137,6 +1233,7 @@ export function createSimulationStore(seed = {}) {
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
stopTaskHalStatusLoop("paused", { operatorMessage: "task/HAL pause requested" });
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_PLAN_PAUSE" },
|
||||
], { operatorMessage: "task/HAL program paused" }).catch(() => {});
|
||||
@@ -1166,7 +1263,15 @@ export function createSimulationStore(seed = {}) {
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_PLAN_RESUME" },
|
||||
], { operatorMessage: "task/HAL program resumed" }).catch(() => {});
|
||||
], {
|
||||
operatorMessage: "task/HAL program resumed",
|
||||
}).then(() => {
|
||||
if (state.runState === "running" || state.machine.interpState === "reading") {
|
||||
startTaskHalStatusLoop({
|
||||
operatorMessage: "task/HAL status loop resumed",
|
||||
});
|
||||
}
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const resumeState = state.machine.interpResumeState === "idle"
|
||||
@@ -1191,6 +1296,14 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
stopTaskHalStatusLoop("step", { operatorMessage: "task/HAL step requested" });
|
||||
runTaskHalCommandSequence([], {
|
||||
taskCycles: 1,
|
||||
operatorMessage: "task/HAL stepped one cycle",
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const playback = nextProgramRuntimeSamplePlayback(state, 1);
|
||||
setState({
|
||||
machine: {
|
||||
@@ -1259,6 +1372,11 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_JOINT_HOME", joint: -1 },
|
||||
], { operatorMessage: "task/HAL machine homed" }).catch(() => {});
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -1541,6 +1659,13 @@ export function createSimulationStore(seed = {}) {
|
||||
if (!state.taskHalRuntime?.loaded || !state.machineFileStaging?.save?.files?.length) {
|
||||
return null;
|
||||
}
|
||||
const preserveMachine = {
|
||||
powerOn: state.machine.powerOn,
|
||||
estopActive: state.machine.estopActive,
|
||||
taskState: state.machine.taskState,
|
||||
mode: state.machine.mode,
|
||||
allHomed: state.machine.allHomed,
|
||||
};
|
||||
const selectedPlan = selectMachineFileProgramForState(state);
|
||||
const session = buildTaskHalSessionFromMachineFiles({
|
||||
profile: state.profile,
|
||||
@@ -1559,12 +1684,28 @@ export function createSimulationStore(seed = {}) {
|
||||
await state.taskHalRuntime.stageFiles(session.files);
|
||||
if (openProgram && session.programPath) {
|
||||
await state.taskHalRuntime.openProgram(session.programPath);
|
||||
await loadTaskHalMotionPlanForSession(session);
|
||||
}
|
||||
if (preserveMachine.powerOn) {
|
||||
await state.taskHalRuntime.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" });
|
||||
}
|
||||
if (preserveMachine.allHomed) {
|
||||
await state.taskHalRuntime.sendCommand({ type: "EMC_JOINT_HOME", joint: -1 });
|
||||
}
|
||||
await state.taskHalRuntime.sendCommand({
|
||||
type: "EMC_TASK_SET_MODE",
|
||||
mode: normalizeLinuxCncTaskMode(preserveMachine.mode).toUpperCase(),
|
||||
});
|
||||
await state.taskHalRuntime.runCycles({
|
||||
...deriveTaskHalCyclePeriods(state),
|
||||
taskCycles: 1,
|
||||
});
|
||||
dispatch({ type: "TASK_HAL_SESSION_READY", session });
|
||||
const status = await state.taskHalRuntime.readStatus();
|
||||
dispatch({
|
||||
type: "TASK_HAL_STATUS_APPLIED",
|
||||
status,
|
||||
preserveMachine,
|
||||
operatorMessage: `LinuxCNC task/HAL session ready ${session.programPath || "-"}`,
|
||||
});
|
||||
return session;
|
||||
@@ -1581,6 +1722,11 @@ export function createSimulationStore(seed = {}) {
|
||||
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
|
||||
await initializeTaskHalSession({ openProgram: true });
|
||||
}
|
||||
const loadedMotionPlan = await loadTaskHalMotionPlanForSession(state.taskHalSession);
|
||||
if (!loadedMotionPlan) {
|
||||
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
|
||||
return null;
|
||||
}
|
||||
|
||||
const ready = validateRunPreconditions(state, { requireTaskHalSession: true });
|
||||
if (!ready.ok) {
|
||||
@@ -1588,21 +1734,151 @@ export function createSimulationStore(seed = {}) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return runTaskHalCommandSequence([
|
||||
stopTaskHalStatusLoop("restarted", {
|
||||
operatorMessage: "task/HAL status loop restarting",
|
||||
});
|
||||
|
||||
const status = await runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_STATE", state: "ON" },
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
|
||||
{ type: "EMC_TASK_PLAN_RUN", line: Math.max(Number(state.activeLine || 1) - Number(state.programStartLine || 1), 0) },
|
||||
{ type: "EMC_TASK_PLAN_RUN", line: 0 },
|
||||
], {
|
||||
taskCycles: 5,
|
||||
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
|
||||
allowFixtureSession: false,
|
||||
});
|
||||
if (shouldContinueTaskHalStatusLoop(state, status)) {
|
||||
startTaskHalStatusLoop({
|
||||
profileId: ready.profileId,
|
||||
iniPath: ready.iniPath,
|
||||
kinematicsModuleId: ready.kinematicsModuleId,
|
||||
operatorMessage: `task/HAL status loop running ${ready.profileId} ${ready.kinematicsModuleId}`,
|
||||
});
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
const loadTaskHalMotionPlanForSession = async (session = state.taskHalSession) => {
|
||||
if (!state.taskHalRuntime?.loaded || typeof state.taskHalRuntime.loadProgramMotionPlan !== "function") {
|
||||
return null;
|
||||
}
|
||||
const motion = state.programExecution?.motion || [];
|
||||
const timing = state.programExecutionTiming || buildTimingForState(state, state.programExecution);
|
||||
if (!session?.programPath || !Array.isArray(motion) || motion.length === 0 || !Array.isArray(timing?.segments) || timing.segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const plan = buildTaskHalProgramMotionPlan({
|
||||
programPath: session.programPath,
|
||||
motion,
|
||||
timing,
|
||||
programLines: state.programLines,
|
||||
linearUnits: timing.linearUnits || state.profile?.traj?.linearUnits || "mm",
|
||||
});
|
||||
if (plan.segmentCount <= 0) {
|
||||
return null;
|
||||
}
|
||||
await state.taskHalRuntime.loadProgramMotionPlan(plan);
|
||||
return plan;
|
||||
};
|
||||
|
||||
const startTaskHalStatusLoop = ({
|
||||
profileId = state.machineProfile,
|
||||
iniPath = state.profile?.iniPath || null,
|
||||
kinematicsModuleId = state.profile?.kinematicsModuleId || state.machineProfile,
|
||||
batchSize = 5,
|
||||
intervalMs = 25,
|
||||
taskPeriodNs = deriveTaskHalCyclePeriods(state).taskPeriodNs,
|
||||
servoPeriodNs = deriveTaskHalCyclePeriods(state).servoPeriodNs,
|
||||
operatorMessage = "task/HAL status loop running",
|
||||
} = {}) => {
|
||||
if (!state.taskHalRuntime?.loaded) return null;
|
||||
stopTaskHalStatusLoop("restarted", { notify: false });
|
||||
const sequence = Number(state.taskHalStatusLoop?.sequence || 0) + 1;
|
||||
dispatch({
|
||||
type: "TASK_HAL_STATUS_LOOP_STARTED",
|
||||
sequence,
|
||||
profileId,
|
||||
iniPath,
|
||||
kinematicsModuleId,
|
||||
batchSize,
|
||||
intervalMs,
|
||||
taskPeriodNs,
|
||||
servoPeriodNs,
|
||||
operatorMessage,
|
||||
});
|
||||
const tick = () => runTaskHalStatusLoopTick(sequence).catch((error) => {
|
||||
stopTaskHalStatusLoop("error", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
operatorMessage: `task/HAL status loop failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
});
|
||||
taskHalStatusLoopTimer = setTimeout(tick, intervalMs);
|
||||
return sequence;
|
||||
};
|
||||
|
||||
const runTaskHalStatusLoopTick = async (sequence) => {
|
||||
const loop = state.taskHalStatusLoop || {};
|
||||
if (!loop.active || loop.sequence !== sequence || !state.taskHalRuntime?.loaded) {
|
||||
return null;
|
||||
}
|
||||
await state.taskHalRuntime.runCycles({
|
||||
taskPeriodNs: loop.taskPeriodNs,
|
||||
servoPeriodNs: loop.servoPeriodNs,
|
||||
taskCycles: loop.batchSize,
|
||||
});
|
||||
const status = await state.taskHalRuntime.readStatus();
|
||||
if (state.taskHalStatusLoop?.sequence !== sequence) {
|
||||
return status;
|
||||
}
|
||||
dispatch({
|
||||
type: "TASK_HAL_STATUS_APPLIED",
|
||||
status,
|
||||
loopSequence: sequence,
|
||||
operatorMessage: `task/HAL status tick ${Number(state.taskHalStatusLoop?.tickCount || 0) + 1}`,
|
||||
});
|
||||
if (shouldContinueTaskHalStatusLoop(state, status)) {
|
||||
taskHalStatusLoopTimer = setTimeout(
|
||||
() => runTaskHalStatusLoopTick(sequence).catch((error) => {
|
||||
stopTaskHalStatusLoop("error", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
operatorMessage: `task/HAL status loop failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}),
|
||||
Number(state.taskHalStatusLoop?.intervalMs || loop.intervalMs || 25),
|
||||
);
|
||||
} else {
|
||||
stopTaskHalStatusLoop(state.runState === "complete" ? "complete" : state.runState, {
|
||||
operatorMessage: state.runState === "complete"
|
||||
? "task/HAL program complete"
|
||||
: `task/HAL status loop ${state.runState}`,
|
||||
});
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
const stopTaskHalStatusLoop = (reason = "stopped", {
|
||||
error = null,
|
||||
operatorMessage = null,
|
||||
notify = true,
|
||||
} = {}) => {
|
||||
if (taskHalStatusLoopTimer) {
|
||||
clearTimeout(taskHalStatusLoopTimer);
|
||||
taskHalStatusLoopTimer = null;
|
||||
}
|
||||
if (notify && (state.taskHalStatusLoop?.active || state.taskHalStatusLoop?.stopReason !== reason || error)) {
|
||||
dispatch({
|
||||
type: "TASK_HAL_STATUS_LOOP_STOPPED",
|
||||
reason,
|
||||
error,
|
||||
operatorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const runTaskHalCommandSequence = async (commands, {
|
||||
taskCycles = 1,
|
||||
taskPeriodNs = 10000000,
|
||||
servoPeriodNs = 1000000,
|
||||
taskPeriodNs = deriveTaskHalCyclePeriods(state).taskPeriodNs,
|
||||
servoPeriodNs = deriveTaskHalCyclePeriods(state).servoPeriodNs,
|
||||
operatorMessage = "task/HAL command complete",
|
||||
pendingJogCommand = null,
|
||||
allowFixtureSession = true,
|
||||
@@ -1939,16 +2215,37 @@ function expectedTaskHalProgramPathForState(state = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function deriveTaskHalCyclePeriods(state = {}) {
|
||||
const taskCycleTimeSeconds = Number(state.linuxCncIniConfig?.task?.cycleTimeSeconds);
|
||||
const iniTaskPeriodNs = Number.isFinite(taskCycleTimeSeconds) && taskCycleTimeSeconds > 0
|
||||
? Math.round(taskCycleTimeSeconds * 1_000_000_000)
|
||||
: null;
|
||||
const iniServoPeriodNs = Number(state.linuxCncIniConfig?.emcmot?.servoPeriodNs);
|
||||
|
||||
return {
|
||||
taskPeriodNs: iniTaskPeriodNs || 10000000,
|
||||
servoPeriodNs: Number.isFinite(iniServoPeriodNs) && iniServoPeriodNs > 0
|
||||
? Math.round(iniServoPeriodNs)
|
||||
: 1000000,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCoordinates(value) {
|
||||
return String(value || "").replace(/[^A-Za-z]/g, "").toUpperCase();
|
||||
}
|
||||
|
||||
function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
function applyTaskHalStatusPatch(state, status, operatorMessage, {
|
||||
loopSequence = null,
|
||||
preserveMachine = null,
|
||||
} = {}) {
|
||||
const ui = status?.ui || {};
|
||||
const task = status?.task || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
const taskState = normalizeTaskHalTaskState(ui.taskState || task.state);
|
||||
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine.mode);
|
||||
const rawTaskState = normalizeTaskHalTaskState(ui.taskState || task.state);
|
||||
const taskState = preserveMachine?.powerOn && rawTaskState === "estop-reset"
|
||||
? "on"
|
||||
: rawTaskState;
|
||||
const taskMode = normalizeLinuxCncTaskMode(preserveMachine?.mode || ui.taskMode || task.mode || state.machine.mode);
|
||||
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
|
||||
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
|
||||
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
|
||||
@@ -1958,7 +2255,8 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
: state.feed.currentVelocity;
|
||||
const paused = interpState === "paused" || motion.paused === true;
|
||||
const aborted = motion.aborted === true;
|
||||
const programComplete = interpState === "idle" && Number(task.nextProgramLine || 0) >= Number(task.openedLineCount || 1);
|
||||
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
|
||||
const programComplete = interpState === "idle" && Number(task.nextProgramLine || 0) >= openedProgramLineCount;
|
||||
const runState = aborted
|
||||
? "stopped"
|
||||
: paused
|
||||
@@ -1972,6 +2270,13 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
: state.runState === "jogging"
|
||||
? "jogging"
|
||||
: "idle";
|
||||
const runtimeFeedback = createTaskHalRuntimeFeedback(state, status, axisPose, activeLine);
|
||||
const loopActive = state.taskHalStatusLoop?.active === true
|
||||
&& loopSequence !== null
|
||||
&& Number(state.taskHalStatusLoop.sequence) === Number(loopSequence)
|
||||
&& (runState === "running" || runState === "mdi");
|
||||
const nextTickCount = loopActive ? Number(state.taskHalStatusLoop.tickCount || 0) + 1 : Number(state.taskHalStatusLoop?.tickCount || 0);
|
||||
const feedbackHistory = [runtimeFeedback, ...(state.programRuntimeFeedbackHistory || [])].slice(0, 100);
|
||||
|
||||
return {
|
||||
taskHalStatus: status,
|
||||
@@ -1982,9 +2287,7 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
axisPose,
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
programExecutionSourceMode: state.programExecution
|
||||
? state.programExecutionSourceMode
|
||||
: "linuxcnc-task-motion-hal-wasm",
|
||||
programExecutionSourceMode: "linuxcnc-task-motion-hal-wasm",
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: taskState === "on",
|
||||
@@ -1994,17 +2297,42 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
interpState,
|
||||
interpResumeState: paused ? state.machine.interpResumeState || "reading" : interpState,
|
||||
taskPaused: paused,
|
||||
allHomed: Boolean(preserveMachine?.allHomed ?? state.machine.allHomed),
|
||||
},
|
||||
runState,
|
||||
taskHalStatusLoop: loopSequence === null
|
||||
? state.taskHalStatusLoop
|
||||
: {
|
||||
...state.taskHalStatusLoop,
|
||||
active: loopActive,
|
||||
tickCount: nextTickCount,
|
||||
lastStatusAt: new Date().toISOString(),
|
||||
stopReason: loopActive ? null : runState,
|
||||
},
|
||||
feed: {
|
||||
...state.feed,
|
||||
currentVelocity,
|
||||
},
|
||||
programRuntimeFeedback: createTaskHalRuntimeFeedback(state, status, axisPose, activeLine),
|
||||
programRuntimeFeedback: runtimeFeedback,
|
||||
programRuntimeFeedbackHistory: feedbackHistory,
|
||||
operatorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
|
||||
const ui = status?.ui || {};
|
||||
const task = status?.task || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
|
||||
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine?.mode);
|
||||
const aborted = motion.aborted === true;
|
||||
const paused = interpState === "paused" || motion.paused === true;
|
||||
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
|
||||
const complete = interpState === "idle"
|
||||
&& Number(task.nextProgramLine || 0) >= openedProgramLineCount;
|
||||
return !aborted && !paused && !complete && (interpState === "reading" || taskMode === "mdi");
|
||||
}
|
||||
|
||||
function resolveTaskHalKinsType(state, status, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const numeric = Number(ui.switchkinsType);
|
||||
@@ -2095,6 +2423,8 @@ function wouldResetNonZeroPoseToLocalZero(currentPose = {}, nextPose = {}) {
|
||||
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
const halProgramLine = Number(status?.halSnapshot?.pins?.["motion.program-line"]?.value || 0);
|
||||
const motionProgramLine = Number(motion.programLine || 0);
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-program-runtime-feedback",
|
||||
sourceMode: "linuxcnc-task-motion-hal-wasm",
|
||||
@@ -2102,6 +2432,10 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
sampleIndex: Number(ui.servoCycle || 0),
|
||||
motionIndex: Math.max(Number(ui.activeLine || 1) - 1, 0),
|
||||
line: activeLine,
|
||||
motionProgramLine,
|
||||
halProgramLine,
|
||||
activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback"),
|
||||
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0 && motionProgramLine === halProgramLine,
|
||||
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
|
||||
timeSeconds: Number(ui.taskCycle || 0) * 0.01,
|
||||
axisPose,
|
||||
@@ -2452,6 +2786,10 @@ function nextProgramRuntimeSamplePlayback(state, step) {
|
||||
const sample = samples[sampleIndex];
|
||||
const motionIndex = clampMotionIndex(state, sample.motionIndex);
|
||||
const motion = state.programExecution?.motion?.[motionIndex] || null;
|
||||
const sampleWithUnits = {
|
||||
...sample,
|
||||
linearUnits: sample.linearUnits || motion?.linearUnits || state.profile.traj?.linearUnits,
|
||||
};
|
||||
const segment = timing?.segments?.[motionIndex] || null;
|
||||
const kinsType = kinsTypeFromProgramMotion(state, motion) || state.kinsType;
|
||||
const elapsedSeconds = Number(sample.timeSeconds) || Number(segment?.elapsedSeconds) || 0;
|
||||
@@ -2461,7 +2799,7 @@ function nextProgramRuntimeSamplePlayback(state, step) {
|
||||
|| 0;
|
||||
const runtimeFeedback = createProgramRuntimeFeedbackFromSample({
|
||||
state,
|
||||
sample,
|
||||
sample: sampleWithUnits,
|
||||
sampleIndex,
|
||||
motion,
|
||||
motionIndex,
|
||||
@@ -2473,7 +2811,7 @@ function nextProgramRuntimeSamplePlayback(state, step) {
|
||||
motionIndex,
|
||||
sampleIndex,
|
||||
activeLine: sample.line || motion?.line || state.activeLine,
|
||||
axisPose: axisPoseFromRuntimeSample(sample, motion, state.axisPose),
|
||||
axisPose: axisPoseFromRuntimeSample(sampleWithUnits, motion, state.axisPose),
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
timing: {
|
||||
@@ -2507,9 +2845,13 @@ function nextProgramRuntimeSamplePlayback(state, step) {
|
||||
function createInitialProgramRuntimeFeedback({ state, timing, motion, timingSnapshot }) {
|
||||
const firstSample = timing?.samples?.[0] || null;
|
||||
if (firstSample) {
|
||||
const sampleWithUnits = {
|
||||
...firstSample,
|
||||
linearUnits: firstSample.linearUnits || motion?.linearUnits || state.profile.traj?.linearUnits,
|
||||
};
|
||||
return createProgramRuntimeFeedbackFromSample({
|
||||
state,
|
||||
sample: firstSample,
|
||||
sample: sampleWithUnits,
|
||||
sampleIndex: 0,
|
||||
motion,
|
||||
motionIndex: clampMotionIndex(state, firstSample.motionIndex),
|
||||
@@ -2537,11 +2879,13 @@ function clampMotionIndex(state, motionIndex) {
|
||||
}
|
||||
|
||||
function buildTimingForState(state, execution) {
|
||||
if (execution?.plannerTiming?.plannerRuntimeReady === true) {
|
||||
const motion = execution?.motion || [];
|
||||
const requiresFeedModeTiming = motion.some((event) => event?.feedMode === "inverse-time");
|
||||
if (!requiresFeedModeTiming && execution?.plannerTiming?.plannerRuntimeReady === true) {
|
||||
return execution.plannerTiming;
|
||||
}
|
||||
return buildProgramExecutionTiming({
|
||||
motion: execution?.motion || [],
|
||||
motion,
|
||||
profile: state.profile,
|
||||
feedOverride: state.feed.feedOverride,
|
||||
rapidOverride: state.feed.rapidOverride,
|
||||
@@ -2713,6 +3057,7 @@ function createProgramRuntimeFeedbackFromSample({
|
||||
motionIndex,
|
||||
line: sample?.line || motion?.line || null,
|
||||
type: sample?.type || motion?.type || null,
|
||||
linearUnits: sample?.linearUnits || motion?.linearUnits || state.profile.traj?.linearUnits || "mm",
|
||||
timeSeconds: elapsedSeconds,
|
||||
axisPose,
|
||||
currentVelocityMmPerMin: currentVelocity,
|
||||
@@ -2750,6 +3095,7 @@ function createProgramRuntimeFeedbackFromMotion({
|
||||
motionIndex,
|
||||
line: motion?.line || null,
|
||||
type: motion?.type || null,
|
||||
linearUnits: motion?.linearUnits || state.profile.traj?.linearUnits || "mm",
|
||||
timeSeconds: Number(timing?.elapsedSeconds) || 0,
|
||||
axisPose,
|
||||
currentVelocityMmPerMin: Number(timing?.currentVelocity) || 0,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { renderFiveAxisScene } from "../visualization/five-axis-scene.js";
|
||||
import { gateLinuxCncTaskAction } from "../state/linuxcnc-task-policy.js";
|
||||
|
||||
const REGIONS = [
|
||||
"titlebar",
|
||||
@@ -623,7 +624,12 @@ function renderBottomControls(element, state, dispatch) {
|
||||
element.innerHTML = `
|
||||
<input type="file" class="program-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
|
||||
${controls
|
||||
.map(([label, action]) => `<button type="button" data-action="${action}">${label}</button>`)
|
||||
.map(([label, action]) => {
|
||||
const gate = bottomControlGate(state, action);
|
||||
const disabled = gate.allowed ? "" : " disabled";
|
||||
const title = gate.allowed ? "" : ` title="${escapeHtml(gate.operatorMessage || "blocked")}"`;
|
||||
return `<button type="button" data-action="${action}"${disabled}${title}>${label}</button>`;
|
||||
})
|
||||
.join("")}
|
||||
`;
|
||||
|
||||
@@ -650,6 +656,22 @@ function renderBottomControls(element, state, dispatch) {
|
||||
}
|
||||
}
|
||||
|
||||
function bottomControlGate(state, action) {
|
||||
const actionMap = {
|
||||
RUN: { type: "RUN" },
|
||||
STEP: { type: "STEP" },
|
||||
PAUSE: { type: "PAUSE" },
|
||||
RESUME: { type: "RESUME" },
|
||||
HOME: { type: "HOME" },
|
||||
MDI_RUN: { type: "RUN_MDI" },
|
||||
JOG_X_NEG: { type: "JOG" },
|
||||
JOG_X_POS: { type: "JOG" },
|
||||
JOG_Y_NEG: { type: "JOG" },
|
||||
JOG_Y_POS: { type: "JOG" },
|
||||
};
|
||||
return gateLinuxCncTaskAction(state, actionMap[action] || { type: "UI_CONTROL" });
|
||||
}
|
||||
|
||||
function formatNumber(value, digits = 3) {
|
||||
return Number(value).toFixed(digits);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import * as THREE from "../vendor/three/three.module.js";
|
||||
import {
|
||||
linearUnitsLabel,
|
||||
linearUnitsToMetersFactor,
|
||||
linearValueToMeters,
|
||||
resolveStateLinearUnits,
|
||||
} from "../runtime/linear-units.js";
|
||||
|
||||
const scenes = new WeakMap();
|
||||
const CAMERA_PRESETS = {
|
||||
iso: { theta: -0.96, phi: 1.02, radius: 7.0, target: new THREE.Vector3(0, 0, 0) },
|
||||
x: { theta: 0, phi: Math.PI / 2, radius: 6.2, target: new THREE.Vector3(0, 0, 0) },
|
||||
y: { theta: -Math.PI / 2, phi: Math.PI / 2, radius: 6.2, target: new THREE.Vector3(0, 0, 0) },
|
||||
z: { theta: 0, phi: 0.001, radius: 6.6, target: new THREE.Vector3(0, 0, 0) },
|
||||
iso: { theta: -0.96, phi: 1.02, radius: 0.72, target: new THREE.Vector3(0, 0, 0) },
|
||||
x: { theta: 0, phi: Math.PI / 2, radius: 0.64, target: new THREE.Vector3(0, 0, 0) },
|
||||
y: { theta: -Math.PI / 2, phi: Math.PI / 2, radius: 0.64, target: new THREE.Vector3(0, 0, 0) },
|
||||
z: { theta: 0, phi: 0.001, radius: 0.68, target: new THREE.Vector3(0, 0, 0) },
|
||||
};
|
||||
const MAX_TOOLPATH_POINTS = 1600;
|
||||
const MAX_TOOLPATH_POINTS = Number.POSITIVE_INFINITY;
|
||||
const EMPTY_GEOMETRY = new THREE.BufferGeometry().setFromPoints([]);
|
||||
|
||||
export function renderFiveAxisScene(canvas, state) {
|
||||
@@ -51,6 +57,11 @@ export function renderFiveAxisScene(canvas, state) {
|
||||
toolExecutionMarker: preview.toolMarker.visible,
|
||||
toolAxisMarker: preview.toolAxis.visible,
|
||||
pathFitBounds: preview.pathFitBoundsReady,
|
||||
pathBounds: computePointBoundsFromGeometryGroups([
|
||||
preview.previewPath.geometry,
|
||||
preview.executedPath.geometry,
|
||||
preview.currentSegmentPath.geometry,
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,7 +83,7 @@ function createScene(canvas) {
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
||||
const camera = new THREE.PerspectiveCamera(42, 1, 0.001, 10);
|
||||
|
||||
const machineModel = createMachineReferenceModel();
|
||||
scene.add(machineModel.root);
|
||||
@@ -84,7 +95,7 @@ function createScene(canvas) {
|
||||
const arcPath = createLine(0xd7ff62, 0.95);
|
||||
const currentSegmentPath = createLine(0xff4fd8, 1);
|
||||
const toolMarker = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.065, 18, 12),
|
||||
new THREE.SphereGeometry(0.0065, 18, 12),
|
||||
new THREE.MeshBasicMaterial({ color: 0x1ffff4 }),
|
||||
);
|
||||
const toolAxis = new THREE.Line(
|
||||
@@ -165,6 +176,7 @@ function renderFallbackPreview(preview, state) {
|
||||
arcPointCount: arcPoints.length,
|
||||
currentSegmentPointCount: currentSegmentPoints.length,
|
||||
pathFitBounds: computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints)) !== null,
|
||||
pathBounds: summarizeBounds(computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints))),
|
||||
});
|
||||
canvas.dataset.threeFallbackReason = preview.errorMessage;
|
||||
|
||||
@@ -177,7 +189,7 @@ function renderFallbackPreview(preview, state) {
|
||||
|
||||
const cx = width * 0.5;
|
||||
const cy = height * 0.53;
|
||||
const scale = Math.min(width / 7.2, height / 4.8);
|
||||
const scale = Math.min(width / 0.72, height / 0.48);
|
||||
|
||||
drawFallbackMachineReference(ctx, cx, cy, scale, state);
|
||||
|
||||
@@ -225,6 +237,9 @@ function exposePreviewDataset(canvas, state, preview) {
|
||||
canvas.dataset.threeExecutedPathPoints = String(preview.executedPointCount);
|
||||
canvas.dataset.threeSceneObjects = String(preview.sceneObjectCount);
|
||||
canvas.dataset.threeToolhead = JSON.stringify(toRoundedVector(preview.toolhead));
|
||||
canvas.dataset.threeSceneUnits = "m";
|
||||
canvas.dataset.threeLinearUnits = linearUnitsLabel(resolveSceneLinearUnits(state));
|
||||
canvas.dataset.threeLinearUnitScaleToMeters = String(linearUnitsToMetersFactor(resolveSceneLinearUnits(state)));
|
||||
canvas.dataset.threeToolAxis = JSON.stringify(toRoundedVector(state.toolAxisVector));
|
||||
canvas.dataset.threeTcpPose = JSON.stringify(toRoundedPose(state.tcpPose));
|
||||
canvas.dataset.threeRtcpState = state.rtcpState;
|
||||
@@ -244,6 +259,7 @@ function exposePreviewDataset(canvas, state, preview) {
|
||||
canvas.dataset.threeToolpathPreviewSource = toolpathPreviewSource(state);
|
||||
canvas.dataset.threeToolExecutionTraceSource = toolExecutionTraceSource(state);
|
||||
canvas.dataset.threePathFitBounds = preview.pathFitBounds ? "ok" : "pending";
|
||||
canvas.dataset.threePathBoundsMeters = JSON.stringify(preview.pathBounds || null);
|
||||
canvas.dataset.threeCurrentSegmentHighlight = preview.currentSegmentPointCount > 0 ? "ok" : "pending";
|
||||
canvas.dataset.threeRapidFeedVisualDistinction = preview.rapidPointCount > 0 || preview.feedPointCount > 0 || preview.arcPointCount > 0 ? "ok" : "pending";
|
||||
canvas.dataset.threeNoGcodeSemanticsGeneration = "ok";
|
||||
@@ -270,47 +286,47 @@ function createMachineReferenceModel() {
|
||||
root.name = "five-axis-machine-reference";
|
||||
|
||||
const base = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(4.8, 3.2, 0.08),
|
||||
new THREE.BoxGeometry(0.48, 0.32, 0.008),
|
||||
new THREE.MeshBasicMaterial({ color: 0x222930 }),
|
||||
);
|
||||
base.position.z = -0.16;
|
||||
base.position.z = -0.016;
|
||||
|
||||
const table = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(3.7, 2.35, 0.05),
|
||||
new THREE.BoxGeometry(0.37, 0.235, 0.005),
|
||||
new THREE.MeshBasicMaterial({ color: 0x3a444d, transparent: true, opacity: 0.78 }),
|
||||
);
|
||||
table.position.z = -0.08;
|
||||
table.position.z = -0.008;
|
||||
|
||||
const xAxis = createStaticLine([new THREE.Vector3(-2.2, 0, 0), new THREE.Vector3(2.25, 0, 0)], 0xff4d4d);
|
||||
const yAxis = createStaticLine([new THREE.Vector3(0, -1.55, 0), new THREE.Vector3(0, 1.6, 0)], 0x70df7d);
|
||||
const zAxis = createStaticLine([new THREE.Vector3(0, 0, -0.08), new THREE.Vector3(0, 0, 1.75)], 0x5aa7ff);
|
||||
const xAxis = createStaticLine([new THREE.Vector3(-0.22, 0, 0), new THREE.Vector3(0.225, 0, 0)], 0xff4d4d);
|
||||
const yAxis = createStaticLine([new THREE.Vector3(0, -0.155, 0), new THREE.Vector3(0, 0.16, 0)], 0x70df7d);
|
||||
const zAxis = createStaticLine([new THREE.Vector3(0, 0, -0.008), new THREE.Vector3(0, 0, 0.175)], 0x5aa7ff);
|
||||
|
||||
const rotaryA = new THREE.Mesh(
|
||||
new THREE.TorusGeometry(0.88, 0.018, 8, 72),
|
||||
new THREE.TorusGeometry(0.088, 0.0018, 8, 72),
|
||||
new THREE.MeshBasicMaterial({ color: 0x1ffff4, transparent: true, opacity: 0.92 }),
|
||||
);
|
||||
rotaryA.rotation.y = Math.PI / 2;
|
||||
|
||||
const rotaryC = new THREE.Mesh(
|
||||
new THREE.TorusGeometry(1.1, 0.016, 8, 72),
|
||||
new THREE.TorusGeometry(0.11, 0.0016, 8, 72),
|
||||
new THREE.MeshBasicMaterial({ color: 0xffd166, transparent: true, opacity: 0.9 }),
|
||||
);
|
||||
rotaryC.rotation.x = Math.PI / 2;
|
||||
rotaryC.position.z = 0.04;
|
||||
rotaryC.position.z = 0.004;
|
||||
|
||||
const toolHolder = new THREE.Group();
|
||||
const holderBody = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.08, 0.08, 0.42, 18),
|
||||
new THREE.CylinderGeometry(0.008, 0.008, 0.042, 18),
|
||||
new THREE.MeshBasicMaterial({ color: 0xf1f5f9 }),
|
||||
);
|
||||
holderBody.rotation.x = Math.PI / 2;
|
||||
holderBody.position.z = 0.32;
|
||||
holderBody.position.z = 0.032;
|
||||
const cutter = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(0.06, 0.25, 18),
|
||||
new THREE.ConeGeometry(0.006, 0.025, 18),
|
||||
new THREE.MeshBasicMaterial({ color: 0xfff176 }),
|
||||
);
|
||||
cutter.rotation.x = Math.PI;
|
||||
cutter.position.z = 0.08;
|
||||
cutter.position.z = 0.008;
|
||||
toolHolder.add(holderBody, cutter);
|
||||
|
||||
root.add(base, table, xAxis, yAxis, zAxis, rotaryA, rotaryC, toolHolder);
|
||||
@@ -387,7 +403,7 @@ function updateToolExecutionMarker(preview, state, toolPosition) {
|
||||
preview.toolAxis.visible = true;
|
||||
updateLineGeometry(preview.toolAxis, [
|
||||
toolPosition,
|
||||
toolPosition.clone().add(vector.multiplyScalar(0.7)),
|
||||
toolPosition.clone().add(vector.multiplyScalar(0.07)),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -401,7 +417,7 @@ function updateMachineReferenceModel(preview, state, toolPosition) {
|
||||
model.rotaryA.rotation.y = Math.PI / 2 + b;
|
||||
model.rotaryC.rotation.z = c;
|
||||
|
||||
const tcpPosition = toolPosition || toPreviewVector(state.tcpPose || state.axisPose);
|
||||
const tcpPosition = toolPosition || toPreviewVector(state.tcpPose || state.axisPose, state);
|
||||
model.toolHolder.position.copy(tcpPosition);
|
||||
const toolVector = toToolVector(state.toolAxisVector);
|
||||
model.toolHolder.lookAt(tcpPosition.clone().add(toolVector));
|
||||
@@ -422,12 +438,12 @@ function geometryPointCount(geometry) {
|
||||
function buildProgramPreviewPoints(state) {
|
||||
const motion = state.programExecution?.motion;
|
||||
if (Array.isArray(motion) && motion.length > 0 && state.preview.pathPoints !== 0) {
|
||||
return limitPoints(motion.map((event) => vectorFromAxes(event.axes)));
|
||||
return limitPoints(motion.map((event) => vectorFromAxes(event.axes, state, event.linearUnits)));
|
||||
}
|
||||
|
||||
const pointCount = normalizePathPointCount(state.preview.pathPoints);
|
||||
if (pointCount === 0) return [];
|
||||
return buildFixturePreviewPoints(pointCount, toPreviewVector(state.tcpPose));
|
||||
return buildFixturePreviewPoints(pointCount, toPreviewVector(state.tcpPose, state));
|
||||
}
|
||||
|
||||
function buildExecutedProgramPoints(state, previewPoints) {
|
||||
@@ -436,7 +452,7 @@ function buildExecutedProgramPoints(state, previewPoints) {
|
||||
const sampleIndex = Number(state.programExecutionSampleIndex || 0);
|
||||
if (Array.isArray(samples) && samples.length > 0) {
|
||||
const end = clamp(Math.round(sampleIndex), 0, samples.length - 1);
|
||||
return limitPoints(samples.slice(0, end + 1).map((sample) => vectorFromAxes(sample)));
|
||||
return limitPoints(samples.slice(0, end + 1).map((sample) => vectorFromAxes(sample, state, sample.linearUnits)));
|
||||
}
|
||||
|
||||
const motionIndex = clamp(Math.round(Number(state.programExecutionMotionIndex || 0)), 0, previewPoints.length - 1);
|
||||
@@ -453,7 +469,7 @@ function buildTypedPreviewPoints(state, type) {
|
||||
return limitPoints(
|
||||
motion
|
||||
.filter((event) => event.type === type)
|
||||
.map((event) => vectorFromAxes(event.axes)),
|
||||
.map((event) => vectorFromAxes(event.axes, state, event.linearUnits)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -465,8 +481,10 @@ function buildCurrentSegmentPoints(state) {
|
||||
const current = motion[motionIndex];
|
||||
const previous = motion[Math.max(motionIndex - 1, 0)];
|
||||
if (!current) return [];
|
||||
const start = motionIndex === 0 ? vectorFromAxes(previous?.axes || current.axes) : vectorFromAxes(previous.axes);
|
||||
const end = vectorFromAxes(current.axes);
|
||||
const start = motionIndex === 0
|
||||
? vectorFromAxes(previous?.axes || current.axes, state, previous?.linearUnits || current.linearUnits)
|
||||
: vectorFromAxes(previous.axes, state, previous.linearUnits);
|
||||
const end = vectorFromAxes(current.axes, state, current.linearUnits);
|
||||
return start.distanceTo(end) > 0 ? [start, end] : [end];
|
||||
}
|
||||
|
||||
@@ -474,9 +492,9 @@ function buildFixturePreviewPoints(pointCount, tcpPosition) {
|
||||
const points = [];
|
||||
for (let index = 0; index < pointCount; index += 1) {
|
||||
const t = pointCount === 1 ? 0 : index / (pointCount - 1);
|
||||
const x = -2.45 + t * 4.9;
|
||||
const y = Math.sin(t * Math.PI * 13) * 0.36;
|
||||
const z = -0.68 + Math.sin(t * Math.PI * 2) * 0.42;
|
||||
const x = -0.085 + t * 0.17;
|
||||
const y = Math.sin(t * Math.PI * 13) * 0.012;
|
||||
const z = 0.012 + Math.sin(t * Math.PI * 2) * 0.018;
|
||||
points.push(new THREE.Vector3(x, y, z));
|
||||
}
|
||||
if (points.length > 0 && tcpPosition) {
|
||||
@@ -487,33 +505,42 @@ function buildFixturePreviewPoints(pointCount, tcpPosition) {
|
||||
|
||||
function executionToolPosition(state, previewPoints) {
|
||||
const feedbackAxes = state.programRuntimeFeedback?.axisPose || state.programRuntimeFeedback;
|
||||
if (feedbackAxes && hasLinearAxes(feedbackAxes)) return vectorFromAxes(feedbackAxes);
|
||||
if (hasLinearAxes(state.axisPose)) return vectorFromAxes(state.axisPose);
|
||||
if (feedbackAxes && hasLinearAxes(feedbackAxes)) {
|
||||
return vectorFromAxes(feedbackAxes, state, state.programRuntimeFeedback?.linearUnits);
|
||||
}
|
||||
if (hasLinearAxes(state.axisPose)) return vectorFromAxes(state.axisPose, state);
|
||||
return previewPoints.at(-1) || null;
|
||||
}
|
||||
|
||||
function vectorFromAxes(axes = {}) {
|
||||
export function axesToSceneMeters(axes = {}, state = {}, linearUnits = null) {
|
||||
const units = linearUnits || axes.linearUnits || resolveSceneLinearUnits(state);
|
||||
return {
|
||||
x: linearValueToMeters(axes.x, units),
|
||||
y: linearValueToMeters(axes.y, units),
|
||||
z: linearValueToMeters(axes.z, units),
|
||||
};
|
||||
}
|
||||
|
||||
function vectorFromAxes(axes = {}, state = {}, linearUnits = null) {
|
||||
const point = axesToSceneMeters(axes, state, linearUnits);
|
||||
return new THREE.Vector3(
|
||||
scaleLinearAxis(axes.x),
|
||||
scaleLinearAxis(axes.y),
|
||||
scaleZAxis(axes.z),
|
||||
point.x,
|
||||
point.y,
|
||||
point.z,
|
||||
);
|
||||
}
|
||||
|
||||
function toPreviewVector(pose = {}) {
|
||||
function toPreviewVector(pose = {}, state = {}) {
|
||||
const point = axesToSceneMeters(pose, state);
|
||||
return new THREE.Vector3(
|
||||
scaleLinearAxis(pose.x),
|
||||
scaleLinearAxis(pose.y),
|
||||
scaleZAxis(pose.z),
|
||||
point.x,
|
||||
point.y,
|
||||
point.z,
|
||||
);
|
||||
}
|
||||
|
||||
function scaleLinearAxis(value) {
|
||||
return clamp((Number(value) || 0) * 0.035, -2.7, 2.7);
|
||||
}
|
||||
|
||||
function scaleZAxis(value) {
|
||||
return clamp((Number(value) || 0) * 0.04 + 0.35, -1.1, 1.9);
|
||||
function resolveSceneLinearUnits(state) {
|
||||
return resolveStateLinearUnits(state);
|
||||
}
|
||||
|
||||
function toToolVector(vector = {}) {
|
||||
@@ -596,6 +623,35 @@ function computePointBounds(points) {
|
||||
return box;
|
||||
}
|
||||
|
||||
function computePointBoundsFromGeometryGroups(geometries) {
|
||||
const points = [];
|
||||
for (const geometry of geometries) {
|
||||
const position = geometry?.getAttribute("position");
|
||||
if (!position) continue;
|
||||
for (let index = 0; index < position.count; index += 1) {
|
||||
points.push(new THREE.Vector3(
|
||||
position.getX(index),
|
||||
position.getY(index),
|
||||
position.getZ(index),
|
||||
));
|
||||
}
|
||||
}
|
||||
return summarizeBounds(computePointBounds(points));
|
||||
}
|
||||
|
||||
function summarizeBounds(bounds) {
|
||||
if (!bounds) return null;
|
||||
const center = new THREE.Vector3();
|
||||
const size = new THREE.Vector3();
|
||||
bounds.getCenter(center);
|
||||
bounds.getSize(size);
|
||||
return {
|
||||
center: toRoundedVector(center),
|
||||
size: toRoundedVector(size),
|
||||
maxSpan: Number(Math.max(size.x, size.y, size.z).toFixed(6)),
|
||||
};
|
||||
}
|
||||
|
||||
function drawFallbackPolyline(ctx, points, cx, cy, scale) {
|
||||
for (let index = 0; index < points.length; index += 1) {
|
||||
const point = points[index];
|
||||
@@ -607,27 +663,27 @@ function drawFallbackPolyline(ctx, points, cx, cy, scale) {
|
||||
}
|
||||
|
||||
function drawFallbackMachineReference(ctx, cx, cy, scale, state) {
|
||||
const tableWidth = 4.8 * scale;
|
||||
const tableHeight = 3.2 * scale;
|
||||
const tableWidth = 0.48 * scale;
|
||||
const tableHeight = 0.32 * scale;
|
||||
ctx.fillStyle = "#20272e";
|
||||
ctx.strokeStyle = "#56616b";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillRect(cx - tableWidth / 2, cy - tableHeight / 2, tableWidth, tableHeight);
|
||||
ctx.strokeRect(cx - tableWidth / 2, cy - tableHeight / 2, tableWidth, tableHeight);
|
||||
|
||||
drawFallbackAxis(ctx, cx - 2.25 * scale, cy, cx + 2.25 * scale, cy, "#ff4d4d");
|
||||
drawFallbackAxis(ctx, cx, cy + 1.55 * scale, cx, cy - 1.6 * scale, "#70df7d");
|
||||
drawFallbackAxis(ctx, cx, cy + 0.2 * scale, cx, cy - 1.15 * scale, "#5aa7ff");
|
||||
drawFallbackAxis(ctx, cx - 0.225 * scale, cy, cx + 0.225 * scale, cy, "#ff4d4d");
|
||||
drawFallbackAxis(ctx, cx, cy + 0.155 * scale, cx, cy - 0.16 * scale, "#70df7d");
|
||||
drawFallbackAxis(ctx, cx, cy + 0.02 * scale, cx, cy - 0.115 * scale, "#5aa7ff");
|
||||
|
||||
ctx.strokeStyle = "#1ffff4";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, 0.92 * scale, 0.42 * scale, degreesToRadians(state.axisPose?.a), 0, Math.PI * 2);
|
||||
ctx.ellipse(cx, cy, 0.092 * scale, 0.042 * scale, degreesToRadians(state.axisPose?.a), 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.strokeStyle = "#ffd166";
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, 0.7 * scale, 0, Math.PI * 2);
|
||||
ctx.arc(cx, cy, 0.07 * scale, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
const tcp = executionToolPosition(state, []);
|
||||
@@ -637,12 +693,12 @@ function drawFallbackMachineReference(ctx, cx, cy, scale, state) {
|
||||
ctx.strokeStyle = "#f1f5f9";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(toolX, toolY - 0.38 * scale);
|
||||
ctx.lineTo(toolX, toolY - 0.08 * scale);
|
||||
ctx.moveTo(toolX, toolY - 0.038 * scale);
|
||||
ctx.lineTo(toolX, toolY - 0.008 * scale);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#1ffff4";
|
||||
ctx.beginPath();
|
||||
ctx.arc(toolX, toolY, 0.07 * scale, 0, Math.PI * 2);
|
||||
ctx.arc(toolX, toolY, 0.007 * scale, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
@@ -676,7 +732,7 @@ function createToolpathCameraControls(canvas, camera, renderFrame) {
|
||||
canvas.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
const scale = Math.exp(Math.sign(event.deltaY) * 0.12);
|
||||
controls.radius = clamp(controls.radius * scale, 1.2, 28);
|
||||
controls.radius = clamp(controls.radius * scale, 0.06, 4);
|
||||
applyCameraControls(controls);
|
||||
controls.renderFrame();
|
||||
}, { passive: false });
|
||||
@@ -700,7 +756,7 @@ function createToolpathCameraControls(canvas, camera, renderFrame) {
|
||||
const distance = getPointerDistance(controls.pointers);
|
||||
const center = getPointerCenter(controls.pointers);
|
||||
if (distance > 0 && controls.lastPinchDistance > 0) {
|
||||
controls.radius = clamp(controls.radius * (controls.lastPinchDistance / distance), 1.2, 28);
|
||||
controls.radius = clamp(controls.radius * (controls.lastPinchDistance / distance), 0.06, 4);
|
||||
if (controls.lastPinchCenter) {
|
||||
panCamera(
|
||||
controls,
|
||||
@@ -810,9 +866,9 @@ function applyFitBounds(controls, selectedView, fitPoints) {
|
||||
bounds.getCenter(center);
|
||||
bounds.getSize(size);
|
||||
controls.target.copy(center);
|
||||
const maxSpan = Math.max(size.x, size.y, size.z, 0.8);
|
||||
const fitRadius = clamp(maxSpan * 1.8, 2.2, 28);
|
||||
controls.radius = selectedView === "z" ? Math.max(fitRadius, 4.2) : fitRadius;
|
||||
const maxSpan = Math.max(size.x, size.y, size.z, 0.08);
|
||||
const fitRadius = clamp(maxSpan * 1.8, 0.22, 4);
|
||||
controls.radius = selectedView === "z" ? Math.max(fitRadius, 0.42) : fitRadius;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
726
web-rtcp-5axis-sim-plan/feed-rate-run-fix/README.md
Normal file
@@ -0,0 +1,726 @@
|
||||
# G-code F 进给速度驱动 RUN 执行修复文档
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 问题结论
|
||||
|
||||
当前 `RUN` 链路已经能完成:
|
||||
|
||||
```text
|
||||
1. 打开 LinuxCNC G-code 程序。
|
||||
2. 通过 task/HAL runtime 推进状态。
|
||||
3. 更新当前行、高亮行、DRO、axisPose、执行轨迹和 task/HAL feedback。
|
||||
```
|
||||
|
||||
但当前 `RUN` **还没有按 G-code 的真实进给速度执行**。
|
||||
|
||||
当前关键问题:
|
||||
|
||||
```text
|
||||
G-code interpreter 能解析 F。
|
||||
execution-timing.js 能基于 F / 距离 / INI 限速做 feed-based timing estimate。
|
||||
但是 task/HAL RUN 执行链路没有把 F 用作真实运行速度。
|
||||
C++ wrapper 层固定写入 velocity=60。
|
||||
activeLine 按 task cycle 推进一行,而不是按 段距离 / F / elapsed time 推进。
|
||||
```
|
||||
|
||||
因此现有浏览器 RUN 证据只能证明:
|
||||
|
||||
```text
|
||||
task/HAL feedback 正在产生;
|
||||
UI 当前行与 task/HAL active line 同步;
|
||||
DRO 与 runtime axisPose 同步;
|
||||
执行轨迹可见;
|
||||
```
|
||||
|
||||
不能证明:
|
||||
|
||||
```text
|
||||
程序按 G-code F 进给速度、G93/G94 模态、rapid/feed 区分、override、段距离真实定时执行。
|
||||
```
|
||||
|
||||
## 2. 当前源码证据
|
||||
|
||||
### 2.1 固定 velocity=60
|
||||
|
||||
文件:
|
||||
|
||||
```text
|
||||
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp
|
||||
```
|
||||
|
||||
函数:
|
||||
|
||||
```text
|
||||
enqueue_linear_move_from_line(TaskRuntime &state, const std::string &line)
|
||||
```
|
||||
|
||||
当前逻辑:
|
||||
|
||||
```cpp
|
||||
command << ",\"velocity\":60}";
|
||||
```
|
||||
|
||||
影响:
|
||||
|
||||
```text
|
||||
1. 每条 G-code 运动行传给 motion runtime 的速度都是 60 units/s。
|
||||
2. JS 状态层把 currentVel * 60 转成 mm/min。
|
||||
3. 在 mm 单位下 UI 看到的速度固定为 3600 mm/min。
|
||||
4. G-code 行中的 F159 / F318 / F636 等不会影响 RUN 实际速度。
|
||||
```
|
||||
|
||||
### 2.2 按 task cycle 推进一行
|
||||
|
||||
文件:
|
||||
|
||||
```text
|
||||
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp
|
||||
```
|
||||
|
||||
函数:
|
||||
|
||||
```text
|
||||
lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles)
|
||||
```
|
||||
|
||||
当前逻辑:
|
||||
|
||||
```cpp
|
||||
for (int i = 0; i < task_cycles; ++i) {
|
||||
state.task_cycle += 1;
|
||||
if (state.interp_state == "READING" && state.next_program_line < state.opened_line_count) {
|
||||
enqueue_linear_move_from_line(state, state.program_lines[state.next_program_line]);
|
||||
...
|
||||
}
|
||||
lcmot_step_servo(...);
|
||||
}
|
||||
```
|
||||
|
||||
影响:
|
||||
|
||||
```text
|
||||
1. 每个 task cycle 至多 enqueue 一条程序行。
|
||||
2. active line 由 cycle 数推进。
|
||||
3. 长段、短段、F 快、F 慢不会改变行推进节奏。
|
||||
4. 这不是真实 LinuxCNC planner/task/motion 的时间语义。
|
||||
```
|
||||
|
||||
### 2.3 interpreter 已有 F 信息
|
||||
|
||||
文件:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-interpreter-runtime.js
|
||||
```
|
||||
|
||||
已有能力:
|
||||
|
||||
```text
|
||||
1. feedRatesBySourceLine(programText) 从 G-code 中提取 F。
|
||||
2. parseLinuxCncCanonicalMotion(...) 把 activeFeedRate 写入 motion event 的 feedRate。
|
||||
3. linearUnitsBySourceLine(...) 跟踪 G20/G21。
|
||||
```
|
||||
|
||||
输出 motion event 已包含:
|
||||
|
||||
```js
|
||||
{
|
||||
type,
|
||||
line,
|
||||
axes,
|
||||
feedRate,
|
||||
linearUnits,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 timing estimate 已有 F 计算
|
||||
|
||||
文件:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/app/src/runtime/execution-timing.js
|
||||
```
|
||||
|
||||
已有能力:
|
||||
|
||||
```text
|
||||
1. 按 motion event feedRate 计算 requestedLinearVelocity。
|
||||
2. 按 profile/INI max velocity 限速。
|
||||
3. 按 linear distance / angular distance 计算 segment durationSeconds。
|
||||
4. 处理 feedOverride / rapidOverride。
|
||||
```
|
||||
|
||||
但当前边界是:
|
||||
|
||||
```text
|
||||
semanticBoundary = linuxcnc_canonical_motion_timing_estimate_not_planner_queue
|
||||
```
|
||||
|
||||
说明它是估算,不是 task/HAL RUN 的权威执行节奏。
|
||||
|
||||
## 3. 修复目标
|
||||
|
||||
### 3.1 必须达成
|
||||
|
||||
修复后 `RUN` 必须满足:
|
||||
|
||||
```text
|
||||
1. G-code 中 F 值进入 task/HAL RUN 执行链路。
|
||||
2. G94 units/min 模式下,feed move 的速度来自当前 modal F。
|
||||
3. G0/STRAIGHT_TRAVERSE 使用 rapid velocity / rapid override / INI max velocity。
|
||||
4. activeLine 按 elapsed execution time 与当前段完成度推进。
|
||||
5. 长距离低 F 段明显运行更久。
|
||||
6. 短距离高 F 段明显更快完成。
|
||||
7. UI currentVelocity 不再固定 3600 mm/min。
|
||||
8. DRO/axisPose 可以在段内插值,而不是只在行边界跳变。
|
||||
9. 现有 RUN gate、task/HAL session、status loop、STOP/ABORT/STEP 不回退。
|
||||
```
|
||||
|
||||
### 3.2 第一阶段不强求
|
||||
|
||||
以下可以作为第二阶段:
|
||||
|
||||
```text
|
||||
1. 完整 LinuxCNC trajectory planner queue 动力学一致性。
|
||||
2. 加速度/jerk/圆弧真实插补完全对齐 LinuxCNC。
|
||||
3. G93 inverse-time feed 的完整五轴角度/线性混合真实语义。
|
||||
4. 硬件 realtime HAL 驱动。
|
||||
```
|
||||
|
||||
但第一阶段至少不能继续固定 `velocity=60`。
|
||||
|
||||
## 4. 推荐实现方案
|
||||
|
||||
### 4.1 不要继续在 C++ wrapper 中逐行粗解析 G-code
|
||||
|
||||
当前 C++ wrapper 的 `enqueue_linear_move_from_line()` 是字符串扫描:
|
||||
|
||||
```text
|
||||
查找 XYZABC 字母;
|
||||
strtod 读取数值;
|
||||
没有模态;
|
||||
没有 F;
|
||||
没有 G90/G91;
|
||||
没有 G20/G21;
|
||||
没有 G93/G94;
|
||||
没有 G0/G1/G2/G3 区分;
|
||||
```
|
||||
|
||||
这个方向继续扩展会很脆弱。
|
||||
|
||||
推荐改为:
|
||||
|
||||
```text
|
||||
JS interpreter runtime 继续负责生成 canonical motion events。
|
||||
JS 侧把 canonical motion + timing segments 传入 task/HAL runtime session。
|
||||
C++ task/HAL wrapper 不再直接解析 G-code 行来生成 velocity。
|
||||
C++ task/HAL wrapper 按已解析 motion segment 执行。
|
||||
```
|
||||
|
||||
### 4.2 新增 task/HAL program motion plan
|
||||
|
||||
在 `wasm-port/runtime/sdk/src/linuxcnc-task-hal.js` 增加可选 API:
|
||||
|
||||
```js
|
||||
loadProgramMotionPlan({
|
||||
programPath,
|
||||
motion,
|
||||
timing,
|
||||
linearUnits,
|
||||
})
|
||||
```
|
||||
|
||||
在 C++ wrapper 增加导出:
|
||||
|
||||
```cpp
|
||||
int lctask_load_program_motion_plan_json(const char *plan_json);
|
||||
```
|
||||
|
||||
motion plan 中至少包含:
|
||||
|
||||
```json
|
||||
{
|
||||
"programPath": "...",
|
||||
"segments": [
|
||||
{
|
||||
"line": 8,
|
||||
"type": "STRAIGHT_FEED",
|
||||
"motionClass": "feed",
|
||||
"axes": { "x": 6.302, "y": -11.560, "z": 27.743, "a": -71.841, "c": -35.930 },
|
||||
"startAxes": { "...": 0 },
|
||||
"feedRate": 318,
|
||||
"linearUnits": "mm",
|
||||
"velocityMmPerMin": 318,
|
||||
"durationSeconds": 0.42,
|
||||
"startSeconds": 1.25,
|
||||
"elapsedSeconds": 1.67
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 C++ task runtime 状态新增字段
|
||||
|
||||
`TaskRuntime` 增加:
|
||||
|
||||
```cpp
|
||||
struct MotionSegment {
|
||||
int line = 0;
|
||||
std::string type;
|
||||
std::string motion_class;
|
||||
double start_seconds = 0.0;
|
||||
double duration_seconds = 0.0;
|
||||
double velocity_mm_per_min = 0.0;
|
||||
std::map<std::string, double> start_axes;
|
||||
std::map<std::string, double> end_axes;
|
||||
};
|
||||
|
||||
std::vector<MotionSegment> motion_plan;
|
||||
int active_segment_index = 0;
|
||||
double run_elapsed_seconds = 0.0;
|
||||
double run_start_seconds = 0.0;
|
||||
```
|
||||
|
||||
### 4.4 RUN 周期推进改为按 elapsed time
|
||||
|
||||
当前:
|
||||
|
||||
```text
|
||||
每 task cycle enqueue 一行。
|
||||
```
|
||||
|
||||
修复后:
|
||||
|
||||
```text
|
||||
每次 lctask_run_cycles 根据 task_period_ns * task_cycles 增加 run_elapsed_seconds。
|
||||
根据 run_elapsed_seconds 找到 active segment。
|
||||
按 segment progress 插值 axisPose。
|
||||
把 currentVel/requestedVel 设置为 segment.velocityMmPerMin / 60。
|
||||
activeLine = segment.line。
|
||||
segment 完成后才进入下一 segment。
|
||||
```
|
||||
|
||||
伪代码:
|
||||
|
||||
```cpp
|
||||
int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles)
|
||||
{
|
||||
const double delta_seconds = (task_period_ns / 1e9) * task_cycles;
|
||||
state.run_elapsed_seconds += delta_seconds;
|
||||
|
||||
const MotionSegment *segment = find_segment_at_time(state.motion_plan, state.run_elapsed_seconds);
|
||||
if (!segment) {
|
||||
state.interp_state = "IDLE";
|
||||
state.exec_state = "DONE";
|
||||
return step_servo(...);
|
||||
}
|
||||
|
||||
const double local = state.run_elapsed_seconds - segment->start_seconds;
|
||||
const double progress = clamp(local / segment->duration_seconds, 0.0, 1.0);
|
||||
AxisPose pose = interpolate(segment->start_axes, segment->end_axes, progress);
|
||||
|
||||
forward_motion_sample({
|
||||
line: segment->line,
|
||||
axes: pose,
|
||||
currentVel: segment->velocity_mm_per_min / 60.0,
|
||||
requestedVel: segment->velocity_mm_per_min / 60.0,
|
||||
inPosition: progress >= 1.0
|
||||
});
|
||||
|
||||
step_servo(...);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 motion runtime command schema
|
||||
|
||||
当前 motion command 是:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "EMC_TRAJ_LINEAR_MOVE",
|
||||
"line": 12,
|
||||
"x": 1,
|
||||
"velocity": 60
|
||||
}
|
||||
```
|
||||
|
||||
建议新增或扩展为:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "EMC_TRAJ_LINEAR_SAMPLE",
|
||||
"line": 12,
|
||||
"x": 1.2,
|
||||
"y": 3.4,
|
||||
"z": 5.6,
|
||||
"a": -70,
|
||||
"c": 20,
|
||||
"currentVel": 5.3,
|
||||
"requestedVel": 5.3,
|
||||
"segmentProgress": 0.35,
|
||||
"source": "feed_timed_motion_plan"
|
||||
}
|
||||
```
|
||||
|
||||
如果不想新增 command type,也可以继续使用 `EMC_TRAJ_LINEAR_MOVE`,但必须:
|
||||
|
||||
```text
|
||||
1. velocity 来自 segment.velocityMmPerMin / 60。
|
||||
2. axes 是当前插值位置,而不是只用 segment end。
|
||||
3. status 能保留 line/progress/currentVel。
|
||||
```
|
||||
|
||||
## 5. G94 速度规则
|
||||
|
||||
### 5.1 G94 units per minute
|
||||
|
||||
在 `TRAJ.LINEAR_UNITS=mm` 时:
|
||||
|
||||
```text
|
||||
F318 => 318 mm/min
|
||||
```
|
||||
|
||||
在 `TRAJ.LINEAR_UNITS=inch` 或 G20 active 时:
|
||||
|
||||
```text
|
||||
F10 => 10 inch/min => 254 mm/min
|
||||
```
|
||||
|
||||
计算:
|
||||
|
||||
```text
|
||||
velocity_mm_per_min = feedRate * linearUnitScaleToMm * feedOverride
|
||||
duration_seconds = linearDistanceMm / (velocity_mm_per_min / 60)
|
||||
```
|
||||
|
||||
### 5.2 Rapid
|
||||
|
||||
G0 / `STRAIGHT_TRAVERSE`:
|
||||
|
||||
```text
|
||||
velocity_mm_per_min = min(INI max velocity, profile max velocity) * rapidOverride
|
||||
duration_seconds = distance / velocity
|
||||
```
|
||||
|
||||
### 5.3 Angular axes
|
||||
|
||||
第一阶段可沿用 `execution-timing.js` 现有策略:
|
||||
|
||||
```text
|
||||
linearSeconds = linearDistanceMm / linearVelocity
|
||||
angularSeconds = angularDistanceDeg / angularVelocity
|
||||
durationSeconds = max(linearSeconds, angularSeconds)
|
||||
```
|
||||
|
||||
这至少能避免旋转轴运动被零时长吞掉。
|
||||
|
||||
### 5.4 G93 inverse time
|
||||
|
||||
LinuxCNC impeller 程序包含:
|
||||
|
||||
```text
|
||||
G93
|
||||
...
|
||||
G1 ... F159
|
||||
```
|
||||
|
||||
G93 的 F 不是 units/min,而是 inverse time。第一阶段有两种策略:
|
||||
|
||||
```text
|
||||
方案 A:先检测 G93,明确标注 unsupported,RUN gate 阻止真实 feed mode 运行。
|
||||
方案 B:实现基础 inverse-time:duration_minutes = 1 / F,duration_seconds = 60 / F。
|
||||
```
|
||||
|
||||
推荐:
|
||||
|
||||
```text
|
||||
第一阶段实现方案 B。
|
||||
```
|
||||
|
||||
原因:
|
||||
|
||||
```text
|
||||
1. test_linuxcnc_source/impeller-7bl-xyzac.ngc 使用 G93。
|
||||
2. 如果不支持 G93,就无法验证用户当前指定测试文件的真实进给语义。
|
||||
3. 对 inverse-time,F 直接定义该运动块完成时间,适合第一阶段验证。
|
||||
```
|
||||
|
||||
需要在 interpreter motion event 中增加:
|
||||
|
||||
```js
|
||||
feedMode: "inverse-time" | "units-per-minute"
|
||||
```
|
||||
|
||||
或至少在 timing 阶段通过 source line 扫描维护 G93/G94 模态。
|
||||
|
||||
## 6. 代码修改落点
|
||||
|
||||
### 6.1 `linuxcnc-interpreter-runtime.js`
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
feedModeBySourceLine(programText)
|
||||
```
|
||||
|
||||
识别:
|
||||
|
||||
```text
|
||||
G93 => inverse-time
|
||||
G94 => units-per-minute
|
||||
```
|
||||
|
||||
motion event 增加:
|
||||
|
||||
```js
|
||||
feedMode: activeFeedMode
|
||||
```
|
||||
|
||||
### 6.2 `execution-timing.js`
|
||||
|
||||
修改 `buildTimingSegment(...)`:
|
||||
|
||||
```text
|
||||
if event.feedMode === "inverse-time":
|
||||
durationSeconds = 60 / feedRate
|
||||
velocityMmPerMin = linearDistanceMm / durationSeconds * 60
|
||||
else:
|
||||
保持 G94 units/min 逻辑
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
```text
|
||||
1. G93 中 F 必须大于 0。
|
||||
2. G93 中如果缺 F,应保留上一个 modal F 或报错,按 LinuxCNC 语义确认。
|
||||
3. velocity 仍要可被 INI max velocity 限制还是忠实 inverse-time,需要明确。第一阶段建议记录 requested 与 capped 两个值。
|
||||
```
|
||||
|
||||
### 6.3 `store.js`
|
||||
|
||||
在 `initializeTaskHalSession` 后或 `RUN_INTERPRETER_PROGRAM` 完成后,把当前 `programExecution.motion` 与 `programExecutionTiming.segments` 传给 task/HAL runtime:
|
||||
|
||||
```js
|
||||
await state.taskHalRuntime.loadProgramMotionPlan({
|
||||
programPath: session.programPath,
|
||||
motion: state.programExecution.motion,
|
||||
timing: state.programExecutionTiming,
|
||||
linearUnits: state.profile.traj.linearUnits,
|
||||
});
|
||||
```
|
||||
|
||||
需要保证:
|
||||
|
||||
```text
|
||||
1. LOAD_LINUXCNC_GCODE_SOURCE 后 interpreter 已完成。
|
||||
2. task/HAL openProgram 的 programPath 与 motion plan programPath 一致。
|
||||
3. 如果 motion plan 不存在,RUN gate 应阻止“真实进给速度 RUN”,不能悄悄回退 velocity=60。
|
||||
```
|
||||
|
||||
### 6.4 `linuxcnc-task-hal.js`
|
||||
|
||||
新增 SDK 方法:
|
||||
|
||||
```js
|
||||
loadProgramMotionPlan(plan) {
|
||||
const rc = callWithJson(mod, "lctask_load_program_motion_plan_json", plan);
|
||||
if (rc !== 0) throw new Error(...);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 `linuxcnc_task_hal_wasm.hh`
|
||||
|
||||
新增声明:
|
||||
|
||||
```cpp
|
||||
int lctask_load_program_motion_plan_json(const char *plan_json);
|
||||
```
|
||||
|
||||
### 6.6 `linuxcnc_task_hal_wasm.cpp`
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
1. MotionSegment 数据结构。
|
||||
2. 简单 JSON plan parser,或复用现有轻量 json_number_after 风格解析数组。
|
||||
3. lctask_load_program_motion_plan_json。
|
||||
4. lctask_run_cycles 按 elapsed time 找 segment。
|
||||
5. 删除或隔离 fixed velocity=60 的 fallback。
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
```text
|
||||
fixed velocity=60 只能作为 explicit fixture fallback。
|
||||
真实 RUN 路径不允许使用它。
|
||||
```
|
||||
|
||||
## 7. 测试计划
|
||||
|
||||
### 7.1 Node 单元测试:F 值改变 duration
|
||||
|
||||
新增测试:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/tests/node/verify_feed_rate_execution_timing.mjs
|
||||
```
|
||||
|
||||
用例:
|
||||
|
||||
```gcode
|
||||
G90 G94
|
||||
G1 X10 F60
|
||||
G1 X20 F600
|
||||
M2
|
||||
```
|
||||
|
||||
期望:
|
||||
|
||||
```text
|
||||
第一段 10mm @ 60mm/min => 10s。
|
||||
第二段 10mm @ 600mm/min => 1s。
|
||||
duration ratio ≈ 10:1。
|
||||
```
|
||||
|
||||
### 7.2 Node 单元测试:G93 inverse time
|
||||
|
||||
用例:
|
||||
|
||||
```gcode
|
||||
G90 G93
|
||||
G1 X10 F2
|
||||
G1 X20 F10
|
||||
M2
|
||||
```
|
||||
|
||||
期望:
|
||||
|
||||
```text
|
||||
F2 => 60/2 = 30s。
|
||||
F10 => 60/10 = 6s。
|
||||
```
|
||||
|
||||
### 7.3 task/HAL runtime 测试:速度不固定
|
||||
|
||||
新增或扩展:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
|
||||
```
|
||||
|
||||
断言:
|
||||
|
||||
```text
|
||||
1. RUN feedback currentVelocityMmPerMin 不等于固定 3600。
|
||||
2. 不同 F 段采样到不同 requested/current velocity。
|
||||
3. activeLine 不再每 task cycle 固定递增一行。
|
||||
4. 慢速段停留采样数 > 快速段。
|
||||
```
|
||||
|
||||
### 7.4 浏览器证据测试
|
||||
|
||||
扩展:
|
||||
|
||||
```text
|
||||
qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs
|
||||
```
|
||||
|
||||
新增报告字段:
|
||||
|
||||
```text
|
||||
feedMode
|
||||
feedRate
|
||||
segmentDurationSeconds
|
||||
segmentProgress
|
||||
requestedVelocityMmPerMin
|
||||
currentVelocityMmPerMin
|
||||
```
|
||||
|
||||
报告验收:
|
||||
|
||||
```text
|
||||
1. impeller G93 行显示 inverse-time。
|
||||
2. 采样 velocity 随当前 segment 变化。
|
||||
3. 当前行停留时间与 F 值/段时长一致。
|
||||
4. Word 报告中列出 F、feedMode、duration、velocity 的采样表。
|
||||
```
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
修复完成必须通过:
|
||||
|
||||
```bash
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_feed_rate_execution_timing.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linear_unit_conversion.mjs
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
node qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs
|
||||
```
|
||||
|
||||
并满足:
|
||||
|
||||
```text
|
||||
1. 不再出现所有 RUN 样本 velocity=3600 的固定速度现象。
|
||||
2. C++ task/HAL RUN 主路径不再写死 velocity=60。
|
||||
3. G94 F60/F600 用例体现 10:1 段时长差异。
|
||||
4. G93 F2/F10 用例体现 5:1 段时长差异。
|
||||
5. test_linuxcnc_source/impeller-7bl-xyzac.ngc 的 RUN 报告包含 feedMode/feedRate/duration 证据。
|
||||
6. STOP/ABORT/STEP 现有测试不回退。
|
||||
```
|
||||
|
||||
## 9. 风险与注意事项
|
||||
|
||||
### 9.1 不要伪造 LinuxCNC 语义
|
||||
|
||||
如果还没有完整 planner,就必须在状态中明确:
|
||||
|
||||
```text
|
||||
semanticBoundary = feed_timed_canonical_motion_runtime
|
||||
```
|
||||
|
||||
不要标称为真实 LinuxCNC planner。
|
||||
|
||||
### 9.2 不要把 estimate 当作硬件实时
|
||||
|
||||
第一阶段可以做到:
|
||||
|
||||
```text
|
||||
canonical motion + feed mode + timing driven browser simulation
|
||||
```
|
||||
|
||||
不能声称:
|
||||
|
||||
```text
|
||||
hardware realtime LinuxCNC execution
|
||||
```
|
||||
|
||||
### 9.3 G93 是当前 impeller 文件的关键
|
||||
|
||||
`test_linuxcnc_source/impeller-7bl-xyzac.ngc` 开头有:
|
||||
|
||||
```gcode
|
||||
M428 ;TCP:xyzac
|
||||
G93
|
||||
S600 M3
|
||||
```
|
||||
|
||||
因此如果不处理 G93,针对该文件的“真实 F 速度”验证仍然不完整。
|
||||
|
||||
## 10. 建议实施顺序
|
||||
|
||||
```text
|
||||
1. 给 interpreter motion event 增加 feedMode。
|
||||
2. 给 execution-timing.js 增加 G93 duration。
|
||||
3. 写 verify_feed_rate_execution_timing.mjs,先证明 timing 正确。
|
||||
4. 给 task/HAL SDK/WASM 增加 loadProgramMotionPlan。
|
||||
5. 改 lctask_run_cycles,按 elapsed time 和 segment progress 推进。
|
||||
6. 改 store,把 motion plan 注入 task/HAL session。
|
||||
7. 扩展 verify_run_feedback_loop,证明 velocity 不固定、慢段停留更久。
|
||||
8. 扩展浏览器 RUN 报告,输出 feedMode/feedRate/duration/progress。
|
||||
9. 重跑 build、smoke、浏览器报告。
|
||||
10. 更新 working_run 和 gptlog-process。
|
||||
```
|
||||
|
||||
1173
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
Normal file
@@ -0,0 +1,117 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { buildProgramExecutionTiming } from "../../app/src/runtime/execution-timing.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import {
|
||||
buildTaskHalProgramMotionPlan,
|
||||
wrapTaskHalSdk,
|
||||
} from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = resolve(__dirname, "../../..");
|
||||
const sourceDir = resolve(rootDir, "web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source");
|
||||
const iniPath = resolve(sourceDir, "xyzac-trt.ini");
|
||||
const gcodePath = resolve(sourceDir, "impeller-7bl-xyzac.ngc");
|
||||
const wasmPath = resolve(rootDir, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm");
|
||||
|
||||
const iniText = readFileSync(iniPath, "utf8");
|
||||
const gcodeText = readFileSync(gcodePath, "utf8");
|
||||
const profile = {
|
||||
...getFiveAxisProfile("xyzac-trt"),
|
||||
linuxCncIniConfig: parseLinuxCncIni(iniText, {
|
||||
path: "working_run/test_linuxcnc_source/xyzac-trt.ini",
|
||||
profileId: "xyzac-trt",
|
||||
}),
|
||||
};
|
||||
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
|
||||
const execution = interpreterRuntime.runProgram(gcodeText);
|
||||
const timing = buildProgramExecutionTiming({
|
||||
motion: execution.motion,
|
||||
profile,
|
||||
feedOverride: 100,
|
||||
rapidOverride: 100,
|
||||
defaultFeedRate: 100,
|
||||
});
|
||||
const feedSegments = timing.segments.filter((segment) => segment.motionClass === "feed");
|
||||
const f159 = feedSegments.find((segment) => segment.feedRate === 159);
|
||||
const f636 = feedSegments.find((segment) => segment.feedRate === 636);
|
||||
|
||||
assert.equal(execution.summary.ready, true);
|
||||
assert.equal(execution.motion.length > 1000, true);
|
||||
assert.equal(execution.motion.some((event) => event.feedMode === "inverse-time"), true);
|
||||
assert.equal(feedSegments.length > 1000, true);
|
||||
assert.equal(f159?.feedMode, "inverse-time");
|
||||
assert.equal(f636?.feedMode, "inverse-time");
|
||||
assertNear(f159.durationSeconds, 60 / 159, "F159 inverse-time duration");
|
||||
assertNear(f636.durationSeconds, 60 / 636, "F636 inverse-time duration");
|
||||
assert.equal(f636.durationSeconds < f159.durationSeconds, true);
|
||||
assert.equal(f159.velocityMmPerMin > 0, true);
|
||||
assert.equal(f636.velocityMmPerMin > 0, true);
|
||||
|
||||
const taskHal = wrapTaskHalSdk(await createLinuxCncTaskHalSdk({
|
||||
wasmBinary: readFileSync(wasmPath),
|
||||
print() {},
|
||||
printErr() {},
|
||||
}));
|
||||
const programPath = "/work/sim/xyzac-trt/impeller-7bl-xyzac.ngc";
|
||||
taskHal.initSession({
|
||||
profileId: "xyzac-trt",
|
||||
iniPath: "/work/sim/xyzac-trt/xyzac-trt.ini",
|
||||
iniText,
|
||||
programPath,
|
||||
});
|
||||
taskHal.stageFiles([{ path: programPath, text: gcodeText }]);
|
||||
taskHal.openProgram(programPath);
|
||||
taskHal.loadProgramMotionPlan(buildTaskHalProgramMotionPlan({
|
||||
programPath,
|
||||
motion: execution.motion,
|
||||
timing,
|
||||
linearUnits: timing.linearUnits,
|
||||
}));
|
||||
taskHal.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" });
|
||||
taskHal.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" });
|
||||
taskHal.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 });
|
||||
|
||||
const samples = [];
|
||||
for (let index = 0; index < 200; index += 1) {
|
||||
taskHal.runCycles({ taskPeriodNs: 100000000, servoPeriodNs: 1000000, taskCycles: 1 });
|
||||
const status = taskHal.readStatus();
|
||||
samples.push({
|
||||
index,
|
||||
activeLine: status.ui.activeLine,
|
||||
axisPose: status.ui.axisPose,
|
||||
currentVelocity: status.ui.currentVelocity,
|
||||
taskCycle: status.ui.taskCycle,
|
||||
});
|
||||
}
|
||||
|
||||
const movedSamples = samples.filter((sample) => sample.currentVelocity > 0);
|
||||
const distinctVelocities = [...new Set(movedSamples.map((sample) => Math.round(sample.currentVelocity * 1000) / 1000))];
|
||||
|
||||
assert.equal(movedSamples.length > 20, true);
|
||||
assert.equal(distinctVelocities.length > 3, true);
|
||||
assert.equal(distinctVelocities.includes(3600), false);
|
||||
assert.equal(samples.at(0).activeLine >= 7, true);
|
||||
assert.equal(samples.at(-1).activeLine > samples.at(0).activeLine, true);
|
||||
assert.equal(samples.some((sample) => sample.activeLine === f159.line), true);
|
||||
assert.equal(samples.some((sample) => sample.activeLine === f636.line), true);
|
||||
assert.equal(samples.some((sample) => Math.abs(sample.currentVelocity - f159.velocityMmPerMin) < 0.001), true);
|
||||
assert.equal(samples.some((sample) => Math.abs(sample.currentVelocity - f636.velocityMmPerMin) < 0.001), true);
|
||||
|
||||
console.log(`impeller_motion_count=${execution.motion.length}`);
|
||||
console.log(`impeller_feed_segments=${feedSegments.length}`);
|
||||
console.log(`impeller_f159_duration_seconds=${f159.durationSeconds}`);
|
||||
console.log(`impeller_f636_duration_seconds=${f636.durationSeconds}`);
|
||||
console.log(`impeller_task_hal_distinct_velocities=${distinctVelocities.slice(0, 10).join(",")}`);
|
||||
console.log("impeller_feed_task_hal_run=ok");
|
||||
|
||||
function assertNear(actual, expected, label, tolerance = 1e-9) {
|
||||
assert.equal(Math.abs(Number(actual) - Number(expected)) <= tolerance, true, `${label}: ${actual} != ${expected}`);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
linearUnitsToMetersFactor,
|
||||
linearUnitsToMillimetersFactor,
|
||||
linearValueToMeters,
|
||||
linearValueToMillimeters,
|
||||
normalizeLinearUnits,
|
||||
resolveStateLinearUnits,
|
||||
} from "../../app/src/runtime/linear-units.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { buildProgramExecutionTiming } from "../../app/src/runtime/execution-timing.js";
|
||||
import { axesToSceneMeters } from "../../app/src/visualization/five-axis-scene.js";
|
||||
|
||||
function assertNear(actual, expected, label) {
|
||||
assert.equal(Math.abs(actual - expected) < 1e-9, true, `${label}: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
|
||||
assert.equal(normalizeLinearUnits("MM"), "mm");
|
||||
assert.equal(normalizeLinearUnits("inch"), "inch");
|
||||
assert.equal(normalizeLinearUnits("meters"), "m");
|
||||
assertNear(linearUnitsToMetersFactor("mm"), 0.001, "mm to m factor");
|
||||
assertNear(linearUnitsToMetersFactor("inch"), 0.0254, "inch to m factor");
|
||||
assertNear(linearUnitsToMetersFactor("m"), 1, "m to m factor");
|
||||
assertNear(linearUnitsToMillimetersFactor("inch"), 25.4, "inch to mm factor");
|
||||
assertNear(linearValueToMeters(25.4, "mm"), 0.0254, "25.4 mm to meters");
|
||||
assertNear(linearValueToMeters(1, "inch"), 0.0254, "1 inch to meters");
|
||||
assertNear(linearValueToMillimeters(0.5, "m"), 500, "0.5 m to millimeters");
|
||||
|
||||
const mmState = { profile: { traj: { linearUnits: "mm" } } };
|
||||
const inchState = { profile: { traj: { linearUnits: "inch" } } };
|
||||
const meterState = { profile: { traj: { linearUnits: "m" } } };
|
||||
const iniState = {
|
||||
linuxCncIniConfig: { traj: { linearUnits: "inch" } },
|
||||
profile: { traj: { linearUnits: "mm" } },
|
||||
};
|
||||
|
||||
assert.equal(resolveStateLinearUnits(iniState), "inch");
|
||||
assert.deepEqual(axesToSceneMeters({ x: 25.4, y: -10, z: 2000 }, mmState), {
|
||||
x: 0.0254,
|
||||
y: -0.01,
|
||||
z: 2,
|
||||
});
|
||||
assert.deepEqual(axesToSceneMeters({ x: 1, y: -0.5, z: 2 }, inchState), {
|
||||
x: 0.0254,
|
||||
y: -0.0127,
|
||||
z: 0.0508,
|
||||
});
|
||||
assert.deepEqual(axesToSceneMeters({ x: 0.1, y: -0.2, z: 0.3 }, meterState), {
|
||||
x: 0.1,
|
||||
y: -0.2,
|
||||
z: 0.3,
|
||||
});
|
||||
|
||||
const inchTiming = buildProgramExecutionTiming({
|
||||
profile: {
|
||||
traj: {
|
||||
linearUnits: "inch",
|
||||
maxLinearVelocity: 2,
|
||||
defaultLinearVelocity: 1,
|
||||
},
|
||||
},
|
||||
defaultFeedRate: 1,
|
||||
motion: [
|
||||
{ type: "STRAIGHT_FEED", line: 1, axes: { x: 0, y: 0, z: 0 } },
|
||||
{ type: "STRAIGHT_FEED", line: 2, axes: { x: 1, y: 0, z: 0 } },
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(inchTiming.linearUnits, "inch");
|
||||
assertNear(inchTiming.segments[1].linearDistanceMm, 25.4, "inch segment distance in mm");
|
||||
assertNear(inchTiming.segments[1].velocityMmPerMin, 25.4, "inch feed velocity in mm/min");
|
||||
|
||||
const mixedUnitsTiming = buildProgramExecutionTiming({
|
||||
profile: {
|
||||
traj: {
|
||||
linearUnits: "mm",
|
||||
maxLinearVelocity: 100,
|
||||
defaultLinearVelocity: 10,
|
||||
},
|
||||
},
|
||||
motion: [
|
||||
{ type: "STRAIGHT_FEED", line: 1, axes: { x: 0, y: 0, z: 0 }, linearUnits: "inch", feedRate: 10 },
|
||||
{ type: "STRAIGHT_FEED", line: 2, axes: { x: 1, y: 0, z: 0 }, linearUnits: "inch", feedRate: 10 },
|
||||
{ type: "STRAIGHT_FEED", line: 3, axes: { x: 25.4, y: 0, z: 0 }, linearUnits: "mm", feedRate: 100 },
|
||||
],
|
||||
});
|
||||
assertNear(mixedUnitsTiming.segments[1].linearDistanceMm, 25.4, "mixed units inch segment distance");
|
||||
assertNear(mixedUnitsTiming.segments[2].linearDistanceMm, 0, "mixed units unchanged position distance");
|
||||
|
||||
const inverseTimeTiming = buildProgramExecutionTiming({
|
||||
profile: {
|
||||
traj: {
|
||||
linearUnits: "mm",
|
||||
maxLinearVelocity: 100,
|
||||
defaultLinearVelocity: 10,
|
||||
},
|
||||
},
|
||||
motion: [
|
||||
{ type: "STRAIGHT_FEED", line: 1, axes: { x: 0, y: 0, z: 0 }, linearUnits: "mm", feedMode: "inverse-time", feedRate: 120 },
|
||||
{ type: "STRAIGHT_FEED", line: 2, axes: { x: 10, y: 0, z: 0 }, linearUnits: "mm", feedMode: "inverse-time", feedRate: 120 },
|
||||
],
|
||||
});
|
||||
assertNear(inverseTimeTiming.segments[1].durationSeconds, 0.5, "G93 inverse-time F120 duration");
|
||||
assertNear(inverseTimeTiming.segments[1].velocityMmPerMin, 1200, "G93 inverse-time velocity");
|
||||
|
||||
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
|
||||
const inchExecution = interpreterRuntime.runProgram("G20 G90\nG1 X1 F10\nM2");
|
||||
const metricExecution = interpreterRuntime.runProgram("G21 G90\nG1 X25.4 F100\nM2");
|
||||
const inverseExecution = interpreterRuntime.runProgram("G21 G90 G93\nG1 X1 F120\nM2");
|
||||
|
||||
assert.equal(inchExecution.motion[0].linearUnits, "inch");
|
||||
assert.equal(metricExecution.motion[0].linearUnits, "mm");
|
||||
assert.equal(inverseExecution.motion[0].feedMode, "inverse-time");
|
||||
assertNear(
|
||||
axesToSceneMeters(inchExecution.motion[0].axes, mmState, inchExecution.motion[0].linearUnits).x,
|
||||
0.0254,
|
||||
"G20 interpreter motion uses inch scene conversion",
|
||||
);
|
||||
assertNear(
|
||||
axesToSceneMeters(metricExecution.motion[0].axes, mmState, metricExecution.motion[0].linearUnits).x,
|
||||
0.0254,
|
||||
"G21 interpreter motion uses millimeter scene conversion",
|
||||
);
|
||||
|
||||
console.log("linear_unit_conversion_smoke=ok");
|
||||
@@ -28,6 +28,16 @@ assert.equal(xyzacIni.kinematics.name, "xyzac-trt-kins");
|
||||
assert.equal(xyzacIni.kinematicsParameters.sparm, "identityfirst");
|
||||
assert.equal(xyzacIni.kinematicsParameters.joints, 5);
|
||||
assert.equal(xyzacIni.traj.coordinates, "XYZAC");
|
||||
assert.equal(xyzacIni.rs274ngc.halPinVars, true);
|
||||
assert.equal(xyzacIni.rs274ngc.parameterFile, "xyzac.var");
|
||||
assert.equal(xyzacIni.hal.halFiles.includes("LIB:basic_sim.tcl"), true);
|
||||
assert.equal(xyzacIni.hal.postguiHalFiles.includes("switchkins_postgui.hal"), true);
|
||||
assert.equal(xyzacIni.hal.halcmd.some((line) => line.includes("motion.analog-out-03") && line.includes("motion.switchkins-type")), true);
|
||||
assert.equal(xyzacIni.emcmot.module, "motmod");
|
||||
assert.equal(xyzacIni.emcmot.servoPeriodNs, 1000000);
|
||||
assert.equal(xyzacIni.task.module, "milltask");
|
||||
assert.equal(xyzacIni.task.cycleTimeSeconds, 0.01);
|
||||
assert.equal(xyzacIni.emcio.toolTable, "xyzac-trt.tbl");
|
||||
assert.equal(xyzacIni.axisLimits.A.max, 50);
|
||||
assert.equal(xyzacIni.jointConfig[3].axis, "A");
|
||||
assert.equal(xyzacIni.jointConfig[4].max, 36000);
|
||||
@@ -50,8 +60,25 @@ assert.equal(xyzbcIni.validation.ready, true);
|
||||
assert.equal(xyzbcIni.kinematics.name, "xyzbc-trt-kins");
|
||||
assert.equal(xyzbcIni.kinematicsModuleId, "xyzbc-trt");
|
||||
assert.equal(xyzbcIni.traj.coordinates, "XYZBC");
|
||||
assert.equal(xyzbcIni.rs274ngc.parameterFile, "xyzbc.var");
|
||||
assert.equal(xyzbcIni.emcio.toolTable, "xyzbc-trt.tbl");
|
||||
assert.equal(xyzbcIni.axisLimits.B.max, 36000);
|
||||
assert.equal(xyzbcIni.jointConfig[3].axis, "B");
|
||||
assert.equal(xyzbcIni.kinematicsParameters.switchkinsTypes[1].webKinsType, "tcp-xyzbc");
|
||||
|
||||
const missingHalPinVarsIni = parseLinuxCncIni(xyzacIniText.replace(/^\s*HAL_PIN_VARS\s*=\s*1$/m, ""), {
|
||||
path: xyzacTrtProfile.iniPath,
|
||||
profileId: xyzacTrtProfile.id,
|
||||
});
|
||||
assert.equal(missingHalPinVarsIni.validation.ready, false);
|
||||
assert.equal(missingHalPinVarsIni.validation.missing.includes("RS274NGC.HAL_PIN_VARS=1"), true);
|
||||
|
||||
const missingTaskIni = parseLinuxCncIni(xyzacIniText.replace(/\n\[TASK\][\s\S]*?(?=\n\[EMCIO\])/m, "\n"), {
|
||||
path: xyzacTrtProfile.iniPath,
|
||||
profileId: xyzacTrtProfile.id,
|
||||
});
|
||||
assert.equal(missingTaskIni.validation.ready, false);
|
||||
assert.equal(missingTaskIni.validation.missing.includes("[TASK]"), true);
|
||||
assert.equal(missingTaskIni.validation.missing.includes("TASK.CYCLE_TIME"), true);
|
||||
|
||||
console.log("linuxcnc_ini_runtime_smoke=ok");
|
||||
|
||||
@@ -71,8 +71,10 @@ assert.equal(state.fullExecutionBoundary.nativeTaskReady, true);
|
||||
assert.equal(state.fullExecutionBoundary.nativeHalSyncReady, true);
|
||||
|
||||
await store.initializeTaskHalSession({ openProgram: true });
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
await waitForTaskHal(store);
|
||||
if (!store.getState().machine.powerOn) {
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
await waitForTaskHal(store);
|
||||
}
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "mdi" });
|
||||
await waitForTaskHal(store);
|
||||
|
||||
171
web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
Normal file
@@ -0,0 +1,171 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import { wrapTaskHalSdk } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = resolve(__dirname, "../../..");
|
||||
const wasmPath = resolve(rootDir, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm");
|
||||
|
||||
await verifyRunFeedbackLoop({
|
||||
profileId: "xyzac-trt",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
stopAction: "STOP",
|
||||
});
|
||||
|
||||
await verifyRunFeedbackLoop({
|
||||
profileId: "xyzac-trt",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
stopAction: "ABORT",
|
||||
});
|
||||
|
||||
await verifyRunFeedbackLoop({
|
||||
profileId: "xyzbc-trt",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
|
||||
stopAction: "STOP",
|
||||
});
|
||||
|
||||
console.log("run_feedback_status_loop_smoke=ok");
|
||||
|
||||
async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) {
|
||||
const sdk = await createLinuxCncTaskHalSdk({
|
||||
wasmBinary: readFileSync(wasmPath),
|
||||
print() {},
|
||||
printErr(message) {
|
||||
console.error(message);
|
||||
},
|
||||
});
|
||||
const profile = getFiveAxisProfile(profileId);
|
||||
const iniText = readFileSync(resolve(rootDir, "wasm-port/vendor/linuxcnc", profile.iniPath), "utf8");
|
||||
const iniConfig = parseLinuxCncIni(iniText, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
const kinematicsModuleId = iniConfig.kinematicsModuleId;
|
||||
const store = createSimulationStore();
|
||||
|
||||
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
store.dispatch({
|
||||
type: "ATTACH_KINEMATICS_RUNTIME",
|
||||
runtime: await createLinuxCncKinematicsRuntime({ moduleId: kinematicsModuleId }),
|
||||
});
|
||||
store.dispatch({
|
||||
type: "ATTACH_INTERPRETER_RUNTIME",
|
||||
runtime: await createLinuxCncInterpreterRuntime(),
|
||||
});
|
||||
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime: wrapTaskHalSdk(sdk) });
|
||||
await store.stageMachineFiles();
|
||||
store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
|
||||
await waitForState(store, (state) => (
|
||||
state.taskHalSession?.programPath?.endsWith(sourceRel.split("/").at(-1)) &&
|
||||
state.rtcpFrame?.sourceMode === "source-derived-kinematics-wasm"
|
||||
));
|
||||
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
await waitForTaskHalCommand(store);
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
await waitForTaskHalCommand(store);
|
||||
|
||||
store.dispatch({ type: "RUN" });
|
||||
await waitForTaskHalCommand(store);
|
||||
await waitForState(store, (state) => distinctActiveLines(state.programRuntimeFeedbackHistory).length >= 3);
|
||||
|
||||
let state = store.getState();
|
||||
const history = state.programRuntimeFeedbackHistory;
|
||||
if (state.taskHalStatusLoop.profileId) {
|
||||
assert.equal(state.taskHalStatusLoop.profileId, profileId);
|
||||
}
|
||||
if (state.taskHalStatusLoop.kinematicsModuleId) {
|
||||
assert.equal(state.taskHalStatusLoop.kinematicsModuleId, kinematicsModuleId);
|
||||
}
|
||||
assert.equal(state.taskHalStatusLoop.taskPeriodNs, 10000000);
|
||||
assert.equal(state.taskHalStatusLoop.servoPeriodNs, 1000000);
|
||||
assert.equal(history.length >= 3, true);
|
||||
assert.equal(history.every((entry) => entry.sourceMode === "linuxcnc-task-motion-hal-wasm"), true);
|
||||
assert.equal(history.every((entry) => entry.semanticBoundary === "linuxcnc_task_motion_hal_wasm_simulation_runtime"), true);
|
||||
assert.equal(history.some((entry) => entry.sourceMode === "fixture-line-playback"), false);
|
||||
assert.equal(history.every((entry) => entry.activeLineSource === "motion-status"), true);
|
||||
assert.equal(history.every((entry) => entry.activeLineHalSynced === true), true);
|
||||
assert.equal(history.every((entry) => entry.line === entry.motionProgramLine), true);
|
||||
assert.equal(history.every((entry) => entry.line === entry.halProgramLine), true);
|
||||
assert.equal(history.some((entry) => entry.currentVelocityMmPerMin > 0), true);
|
||||
assert.equal(history.some((entry) => entry.currentVelocityMmPerMin !== 3600), true);
|
||||
assert.equal(isMonotonic(history.map((entry) => entry.taskCycle).reverse()), true);
|
||||
assert.equal(isMonotonic(history.map((entry) => entry.cycle).reverse()), true);
|
||||
const activeLines = distinctActiveLines(history);
|
||||
assert.equal(activeLines.length >= 3, true, `${sourceRel} activeLines=${activeLines.join(",")}`);
|
||||
assert.equal(isMonotonic(activeLines), true, `${sourceRel} activeLines=${activeLines.join(",")}`);
|
||||
assert.equal(
|
||||
sourceRel.endsWith("boat-xyzbc.ngc") ? activeLines.join(",") !== "1,10" : true,
|
||||
true,
|
||||
`${sourceRel} activeLines=${activeLines.join(",")}`,
|
||||
);
|
||||
assert.equal(
|
||||
sourceRel.endsWith("boat-xyzbc.ngc") ? activeLines.some((line) => line > activeLines.indexOf(line) + 1) : true,
|
||||
true,
|
||||
`${sourceRel} activeLines=${activeLines.join(",")}`,
|
||||
);
|
||||
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
assert.equal(state.taskHalStatus.summary.taskRuntimeReady, true);
|
||||
assert.equal(state.taskHalStatus.summary.halSyncReady, true);
|
||||
|
||||
const cycleAfterRun = state.taskHalStatus.ui.taskCycle;
|
||||
store.dispatch({ type: "STEP" });
|
||||
await waitForTaskHalCommand(store);
|
||||
state = store.getState();
|
||||
assert.equal(state.programRuntimeFeedback.sourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
|
||||
store.dispatch({ type: stopAction });
|
||||
await waitForTaskHalCommand(store);
|
||||
state = store.getState();
|
||||
assert.equal(state.taskHalStatusLoop.active, false);
|
||||
assert.equal(["stopped", "complete", "idle"].includes(state.runState), true);
|
||||
assert.equal(
|
||||
stopAction !== "ABORT" || ["aborted", "stopped", "complete", "idle"].includes(state.taskHalStatusLoop.stopReason),
|
||||
true,
|
||||
);
|
||||
assert.equal(state.taskHalStatus.ui.taskCycle >= cycleAfterRun, true);
|
||||
}
|
||||
|
||||
async function waitForTaskHalCommand(store) {
|
||||
for (let attempt = 0; attempt < 80; attempt += 1) {
|
||||
if (!store.getState().taskHalExecutionPending) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
if (!store.getState().taskHalExecutionPending) return store.getState();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
throw new Error("task/HAL command did not settle");
|
||||
}
|
||||
|
||||
async function waitForState(store, predicate) {
|
||||
for (let attempt = 0; attempt < 160; attempt += 1) {
|
||||
const state = store.getState();
|
||||
if (predicate(state)) return state;
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
throw new Error("store state condition did not settle");
|
||||
}
|
||||
|
||||
function isMonotonic(values) {
|
||||
for (let index = 1; index < values.length; index += 1) {
|
||||
if (Number(values[index]) < Number(values[index - 1])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function distinctActiveLines(history = []) {
|
||||
return [...new Set(history.map((entry) => Number(entry.line)).reverse())]
|
||||
.filter((line) => Number.isFinite(line));
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
createSimulationStore,
|
||||
validateRunPreconditions,
|
||||
} from "../../app/src/state/store.js";
|
||||
import { gateLinuxCncTaskAction } from "../../app/src/state/linuxcnc-task-policy.js";
|
||||
|
||||
const cases = [
|
||||
{
|
||||
@@ -124,6 +125,35 @@ check = validateRunPreconditions(store.getState(), {
|
||||
assert.equal(check.ok, false);
|
||||
assert.equal(check.operatorMessage, "run blocked: task/HAL runtime not ready");
|
||||
|
||||
let gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: machine must be on");
|
||||
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: home machine first");
|
||||
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
|
||||
|
||||
const unopenedStore = createSimulationStore();
|
||||
unopenedStore.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
unopenedStore.dispatch({
|
||||
type: "ATTACH_KINEMATICS_RUNTIME",
|
||||
runtime: await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }),
|
||||
});
|
||||
await unopenedStore.stageMachineFiles();
|
||||
check = validateRunPreconditions(unopenedStore.getState(), {
|
||||
requireTaskHalRuntime: false,
|
||||
requireTaskHalSession: false,
|
||||
});
|
||||
assert.equal(check.ok, false);
|
||||
assert.equal(check.operatorMessage, "run blocked: no machine-file G-code opened for task/HAL session");
|
||||
|
||||
console.log("run_preconditions_ini_profile_smoke=ok");
|
||||
console.log("run_preconditions_kinematics_smoke=ok");
|
||||
console.log("run_preconditions_machine_file_smoke=ok");
|
||||
|
||||
@@ -93,7 +93,385 @@ M430 -> switchkins type 2 -> userk
|
||||
HAL link: motion.analog-out-03 => motion.switchkins-type
|
||||
```
|
||||
|
||||
## 3. RUN 前置状态检查
|
||||
## 3. INI definition contract
|
||||
|
||||
`RUN` 只能基于一个完整、可验证的 LinuxCNC INI 上下文启动。INI 不是展示信息,而是机床契约来源。`parseLinuxCncIni()` 和 `applyIniConfigToProfile()` 必须把下面字段转成结构化状态,RUN gate 只读取结构化状态,不在 RUN action 中重新猜测字符串。
|
||||
|
||||
### 3.1 必须存在的 INI sections
|
||||
|
||||
当前五轴 `xyzac-trt` / `xyzbc-trt` profile 至少要求这些 section:
|
||||
|
||||
| section | 是否必填 | RUN gate 用途 |
|
||||
| --- | --- | --- |
|
||||
| `[EMC]` | 必填 | 识别 machine name,确认是 switchkins 五轴 profile |
|
||||
| `[DISPLAY]` | 必填 | 提供默认打开程序、程序目录、UI 坐标提示;不能作为运动学真值 |
|
||||
| `[RS274NGC]` | 必填 | 提供 remap、subroutine path、HAL pin vars、parameter file |
|
||||
| `[KINS]` | 必填 | 提供 kinematics module 和 joint 数,是 kinematics/runtime 匹配依据 |
|
||||
| `[HAL]` | 必填 | 提供 HAL 文件、HALUI、POSTGUI_HALFILE、switchkins HALCMD |
|
||||
| `[HALUI]` | 必填 | 提供 M428/M429/M430 operator command 顺序和语义提示 |
|
||||
| `[TRAJ]` | 必填 | 提供 COORDINATES、单位、速度/加速度上限 |
|
||||
| `[EMCMOT]` | 必填 | 提供 motmod、SERVO_PERIOD、COMM_TIMEOUT |
|
||||
| `[TASK]` | 必填 | 提供 task module 和 CYCLE_TIME |
|
||||
| `[EMCIO]` | 必填 | 提供 tool table |
|
||||
| `[AXIS_*]` | 必填 | 提供各坐标轴 limit/velocity/acceleration |
|
||||
| `[JOINT_*]` | 必填 | 提供 joint 类型、home、limit、home sequence |
|
||||
|
||||
不满足这些 section 时,INI readiness 必须是 not ready,RUN 必须阻断。
|
||||
|
||||
### 3.2 必填字段与 gate 用途
|
||||
|
||||
`[EMC]`:
|
||||
|
||||
```text
|
||||
VERSION
|
||||
MACHINE
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
```text
|
||||
xyzac-trt: MACHINE = sim-xyzac-trt-kins (switchkins)
|
||||
xyzbc-trt: MACHINE = sim-xyzbc-trt-kins (switchkins)
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. 推导 profileId。
|
||||
2. 确认当前 profile 是 switchkins 五轴仿真机床。
|
||||
3. 作为 task/HAL session metadata,写入 feedback 证据。
|
||||
```
|
||||
|
||||
`[DISPLAY]`:
|
||||
|
||||
```text
|
||||
GEOMETRY
|
||||
OPEN_FILE
|
||||
JOG_AXES
|
||||
DISPLAY
|
||||
PROGRAM_PREFIX
|
||||
POSITION_OFFSET
|
||||
POSITION_FEEDBACK
|
||||
MAX_LINEAR_VELOCITY
|
||||
MAX_ANGULAR_VELOCITY
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. OPEN_FILE 只能作为默认程序候选,RUN 实际程序必须来自 staged selected G-code。
|
||||
2. PROGRAM_PREFIX 用于解析 machine-file staging 的相对路径。
|
||||
3. GEOMETRY/JOG_AXES 用于 UI 呈现,不得覆盖 [TRAJ] COORDINATES。
|
||||
```
|
||||
|
||||
`[RS274NGC]`:
|
||||
|
||||
```text
|
||||
SUBROUTINE_PATH
|
||||
HAL_PIN_VARS = 1
|
||||
REMAP = M428 modalgroup=10 ngc=428remap
|
||||
REMAP = M429 modalgroup=10 ngc=429remap
|
||||
REMAP = M430 modalgroup=10 ngc=430remap
|
||||
PARAMETER_FILE
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. HAL_PIN_VARS=1 是 remap 子程序读取 motion.switchkins-type 的前提。
|
||||
2. M428/M429/M430 remap 文件必须被 stage 到同一 machine context。
|
||||
3. PARAMETER_FILE 必须随 profile 区分,不能 xyzac/xyzbc 混用。
|
||||
```
|
||||
|
||||
`[KINS]`:
|
||||
|
||||
```text
|
||||
KINEMATICS
|
||||
JOINTS
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
```text
|
||||
xyzac-trt: KINEMATICS = xyzac-trt-kins sparm=identityfirst
|
||||
xyzbc-trt: KINEMATICS = xyzbc-trt-kins sparm=identityfirst
|
||||
JOINTS = 5
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. 从 KINEMATICS 推导 kinematicsModuleId。
|
||||
2. 校验 kinematics WASM moduleId。
|
||||
3. 校验 joint count 与 [JOINT_0]...[JOINT_4] 完整性。
|
||||
4. sparm=identityfirst 决定 switchkins type 0 是 identity,不能按默认 kins 猜测。
|
||||
```
|
||||
|
||||
`[HAL]`:
|
||||
|
||||
```text
|
||||
HALUI = halui
|
||||
HALFILE = LIB:basic_sim.tcl
|
||||
POSTGUI_HALFILE = switchkins_postgui.hal
|
||||
HALCMD = net :kinstype-select <= motion.analog-out-03 => motion.switchkins-type
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. HALUI 存在时,UI command 与 LinuxCNC halui 行为对齐。
|
||||
2. HALFILE/POSTGUI_HALFILE 必须 stage,task/HAL runtime 才能构造同一机床上下文。
|
||||
3. switchkins HALCMD 是 M428/M429/M430 影响 motion.switchkins-type 的证据。
|
||||
```
|
||||
|
||||
`[HALUI]`:
|
||||
|
||||
```text
|
||||
MDI_COMMAND = M429
|
||||
MDI_COMMAND = M428
|
||||
MDI_COMMAND = M430
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. 记录 operator 命令入口。
|
||||
2. 与 [RS274NGC] REMAP 和 [HAL] switchkins HALCMD 交叉校验。
|
||||
3. 不直接作为 RUN 自动插入命令;RUN 程序内的 M428/M429/M430 由 interpreter/remap 处理。
|
||||
```
|
||||
|
||||
`[TRAJ]`:
|
||||
|
||||
```text
|
||||
COORDINATES
|
||||
LINEAR_UNITS
|
||||
ANGULAR_UNITS
|
||||
DEFAULT_LINEAR_VELOCITY
|
||||
MAX_LINEAR_VELOCITY
|
||||
MAX_LINEAR_ACCELERATION
|
||||
DEFAULT_LINEAR_ACCELERATION
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
```text
|
||||
xyzac-trt: COORDINATES = XYZAC
|
||||
xyzbc-trt: COORDINATES = XYZBC
|
||||
LINEAR_UNITS = mm
|
||||
ANGULAR_UNITS = deg
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. COORDINATES 是 UI 轴、G-code 轴、runtime axisPose 的主契约。
|
||||
2. 单位决定 DRO 和 velocity 显示,不能由前端默认值覆盖。
|
||||
3. velocity/acceleration 是 planner/runtime limit 的来源。
|
||||
```
|
||||
|
||||
`[EMCMOT]` 与 `[TASK]`:
|
||||
|
||||
```text
|
||||
[EMCMOT]
|
||||
EMCMOT = motmod
|
||||
SERVO_PERIOD = 1000000
|
||||
COMM_TIMEOUT = 1
|
||||
|
||||
[TASK]
|
||||
TASK = milltask
|
||||
CYCLE_TIME = 0.010
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. SERVO_PERIOD 用于 taskHalStatusLoop 的 servo tick 语义。
|
||||
2. CYCLE_TIME 用于 task cycle 语义和测试断言。
|
||||
3. runtime readiness 必须能报告 task/motion/HAL 三者已按这些参数初始化。
|
||||
```
|
||||
|
||||
`[EMCIO]`:
|
||||
|
||||
```text
|
||||
TOOL_TABLE
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. tool table 必须 stage 到 machine files。
|
||||
2. G43/tool offset 相关状态不能用空表静默替代。
|
||||
```
|
||||
|
||||
`[AXIS_*]`:
|
||||
|
||||
```text
|
||||
MIN_LIMIT
|
||||
MAX_LIMIT
|
||||
MAX_VELOCITY
|
||||
MAX_ACCELERATION
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
```text
|
||||
xyzac-trt: [AXIS_X] [AXIS_Y] [AXIS_Z] [AXIS_A] [AXIS_C]
|
||||
xyzbc-trt: [AXIS_X] [AXIS_Y] [AXIS_Z] [AXIS_B] [AXIS_C]
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. 校验 profile coordinates 对应的 axis section 完整。
|
||||
2. 约束 UI limit、DRO 范围、preview bounds。
|
||||
3. 防止 xyzac/xyzbc 轴表混用。
|
||||
```
|
||||
|
||||
`[JOINT_*]`:
|
||||
|
||||
```text
|
||||
TYPE
|
||||
HOME
|
||||
MIN_LIMIT
|
||||
MAX_LIMIT
|
||||
MAX_VELOCITY
|
||||
MAX_ACCELERATION
|
||||
HOME_SEARCH_VEL
|
||||
HOME_SEQUENCE
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
```text
|
||||
JOINT_0..JOINT_4 必须完整。
|
||||
JOINT_0..JOINT_2 TYPE = LINEAR。
|
||||
xyzac-trt: JOINT_3/JOINT_4 分别对应 A/C rotary。
|
||||
xyzbc-trt: JOINT_3/JOINT_4 分别对应 B/C rotary。
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
```text
|
||||
1. 校验 [KINS] JOINTS=5 与 joint section 数量一致。
|
||||
2. home/allHomed gate 的配置来源。
|
||||
3. task/HAL runtime 初始化 joint status 的来源。
|
||||
```
|
||||
|
||||
### 3.3 INI readiness 输出
|
||||
|
||||
`parseLinuxCncIni()` 应输出或间接形成这些结构化字段:
|
||||
|
||||
```js
|
||||
{
|
||||
profileId: "xyzac-trt",
|
||||
iniPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
machineName: "sim-xyzac-trt-kins (switchkins)",
|
||||
traj: {
|
||||
coordinates: "XYZAC",
|
||||
linearUnits: "mm",
|
||||
angularUnits: "deg"
|
||||
},
|
||||
kinematics: {
|
||||
name: "xyzac-trt-kins",
|
||||
moduleId: "xyzac-trt",
|
||||
switchkins: true,
|
||||
identityFirst: true,
|
||||
joints: 5
|
||||
},
|
||||
rs274ngc: {
|
||||
halPinVars: true,
|
||||
remaps: ["M428", "M429", "M430"],
|
||||
subroutinePath: "./remap_subs",
|
||||
parameterFile: "xyzac.var"
|
||||
},
|
||||
hal: {
|
||||
halui: "halui",
|
||||
halFiles: ["LIB:basic_sim.tcl"],
|
||||
postguiHalFiles: ["switchkins_postgui.hal"],
|
||||
switchkinsSignal: "motion.analog-out-03 => motion.switchkins-type"
|
||||
},
|
||||
emcmot: {
|
||||
module: "motmod",
|
||||
servoPeriodNs: 1000000
|
||||
},
|
||||
task: {
|
||||
module: "milltask",
|
||||
cycleTimeSeconds: 0.010
|
||||
},
|
||||
emcio: {
|
||||
toolTable: "xyzac-trt.tbl"
|
||||
},
|
||||
axes: {
|
||||
X: { minLimit: -200, maxLimit: 200 },
|
||||
Y: { minLimit: -100, maxLimit: 100 },
|
||||
Z: { minLimit: -120, maxLimit: 120 },
|
||||
A: { minLimit: -100, maxLimit: 50 },
|
||||
C: { minLimit: -36000, maxLimit: 36000 }
|
||||
},
|
||||
joints: [
|
||||
{ index: 0, type: "LINEAR", axis: "X" },
|
||||
{ index: 1, type: "LINEAR", axis: "Y" },
|
||||
{ index: 2, type: "LINEAR", axis: "Z" },
|
||||
{ index: 3, type: "ANGULAR", axis: "A" },
|
||||
{ index: 4, type: "ANGULAR", axis: "C" }
|
||||
],
|
||||
validation: {
|
||||
ready: true,
|
||||
errors: []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`xyzbc-trt` 的输出必须只在 profileId、machineName、coordinates、kinematics module、parameter/tool table、rotary axis B/C 等字段上变化,不能沿用 xyzac 的 A/C axis contract。
|
||||
|
||||
### 3.4 INI contract 失败时的阻断
|
||||
|
||||
这些情况必须让 `state.iniConfigReadiness.ready=false`,并阻断 RUN:
|
||||
|
||||
```text
|
||||
缺少 [EMC] MACHINE。
|
||||
缺少 [TRAJ] COORDINATES。
|
||||
COORDINATES 不是 XYZAC 或 XYZBC。
|
||||
[KINS] KINEMATICS 不是 xyzac-trt-kins 或 xyzbc-trt-kins。
|
||||
[KINS] JOINTS 不是 5。
|
||||
缺少 M428/M429/M430 remap。
|
||||
HAL_PIN_VARS 不是 1。
|
||||
缺少 motion.analog-out-03 => motion.switchkins-type HALCMD。
|
||||
缺少 [EMCMOT] SERVO_PERIOD。
|
||||
缺少 [TASK] CYCLE_TIME。
|
||||
缺少 TOOL_TABLE。
|
||||
axis section 与 COORDINATES 不一致。
|
||||
joint section 数量与 [KINS] JOINTS 不一致。
|
||||
xyzac INI 指向 xyzbc tool table/parameter file,或反向混用。
|
||||
```
|
||||
|
||||
阻断消息建议:
|
||||
|
||||
```text
|
||||
run blocked: LinuxCNC INI contract invalid
|
||||
run blocked: INI missing required section [KINS]
|
||||
run blocked: INI coordinates do not match selected profile
|
||||
run blocked: INI switchkins remap incomplete
|
||||
run blocked: INI HAL switchkins signal missing
|
||||
run blocked: INI joint/axis contract mismatch
|
||||
```
|
||||
|
||||
### 3.5 与 RUN gate 的关系
|
||||
|
||||
INI contract 通过只是 RUN 的第一层 gate。RUN 仍必须继续检查:
|
||||
|
||||
```text
|
||||
1. kinematics WASM 已按 INI [KINS] moduleId 加载。
|
||||
2. machine files 已按同一个 INI stage。
|
||||
3. task/HAL runtime 已按同一个 INI/session 初始化。
|
||||
4. selected G-code 已通过 task plan open 打开。
|
||||
5. task status 的 opened file 与 selected staged G-code 一致。
|
||||
6. machine ON、AUTO、homed/no_force_homing 满足。
|
||||
```
|
||||
|
||||
所以不能把 `parseLinuxCncIni().validation.ready=true` 等同于 `RUN` ready。
|
||||
|
||||
## 4. RUN 前置状态检查
|
||||
|
||||
`RUN` action 进入真正执行前,应要求这些状态全部明确:
|
||||
|
||||
@@ -112,6 +490,11 @@ state.taskHalRuntimeReadiness.taskRuntimeReady === true
|
||||
state.taskHalRuntimeReadiness.motionRuntimeReady === true
|
||||
state.taskHalRuntimeReadiness.halRuntimeReady === true
|
||||
state.taskHalSession.programPath 指向当前选中的 G-code
|
||||
state.taskHalSession.openProgram === true
|
||||
state.taskHalStatus.task.file 或 openedProgram.path 指向同一个 staged G-code
|
||||
state.taskHalStatus.task.state 是 ON
|
||||
state.taskHalStatus.task.mode 是 AUTO,或 RUN 前可切换到 AUTO
|
||||
state.taskHalStatus.motion.allHomed === true,或当前仿真 profile 明确 no_force_homing
|
||||
```
|
||||
|
||||
如果任一项不满足,`RUN` 应返回明确 operatorMessage,不应进入 fixture line playback 后伪装成真实运行。
|
||||
@@ -124,11 +507,98 @@ run blocked: machine profile and INI coordinates mismatch
|
||||
run blocked: LinuxCNC kinematics runtime not ready
|
||||
run blocked: task/HAL runtime not ready
|
||||
run blocked: no machine-file G-code opened for task/HAL session
|
||||
run blocked: machine is not on
|
||||
run blocked: machine is not homed
|
||||
run blocked: task mode is not AUTO
|
||||
```
|
||||
|
||||
## 4. 详细测试计划
|
||||
## 5. LinuxCNC 源码对照结论
|
||||
|
||||
### 4.1 INI/profile 解析测试
|
||||
`RUN` 命令修改方向总体合适,但必须严格贴近 LinuxCNC 的 task 语义:
|
||||
|
||||
```text
|
||||
RUN 不是前端 sample playback。
|
||||
RUN 不是直接解析 G-code 后自增 activeLine。
|
||||
RUN 是在已打开程序、机床可运行、AUTO 模式下发送 EMC_TASK_PLAN_RUN,
|
||||
随后由 task 主循环持续 read/execute interpreter,并从 motion/HAL 状态读取反馈。
|
||||
```
|
||||
|
||||
源码证据:
|
||||
|
||||
```text
|
||||
/home/meswork/cnc_wams/linuxcnc/src/emc/usr_intf/halui.cc:1096
|
||||
sendProgramRun(int line)
|
||||
- updateStatus()
|
||||
- 如果 emcStatus->task.file 为空,直接返回 -1
|
||||
- 保存 programStartLine
|
||||
- 设置 EMC_TASK_PLAN_RUN.line
|
||||
- sendAuto()
|
||||
- emcCommandSend(EMC_TASK_PLAN_RUN)
|
||||
|
||||
/home/meswork/cnc_wams/linuxcnc/src/emc/usr_intf/shcom.cc:750
|
||||
sendProgramOpen(program)
|
||||
- 发送 EMC_TASK_PLAN_OPEN
|
||||
- 本地进程发送文件名
|
||||
- remote process 通过 remote_buffer 分块发送文件内容
|
||||
|
||||
/home/meswork/cnc_wams/linuxcnc/src/emc/usr_intf/shcom.cc:814
|
||||
sendProgramRun(int line)
|
||||
- status 为 AUTO update 时先 updateStatus()
|
||||
- task.file 为空时尝试重新打开 lastProgramFile
|
||||
- 再发送 EMC_TASK_PLAN_RUN
|
||||
|
||||
/home/meswork/cnc_wams/linuxcnc/src/emc/task/emctaskmain.cc:2164
|
||||
EMC_TASK_PLAN_OPEN
|
||||
- 接收 remote 文件或打开本地文件
|
||||
- 调用 emcTaskPlanOpen(open_msg->file)
|
||||
- 成功后写入 emcStatus->task.file
|
||||
|
||||
/home/meswork/cnc_wams/linuxcnc/src/emc/task/emctaskmain.cc:2318
|
||||
EMC_TASK_PLAN_RUN
|
||||
- 未 homed 且 no_force_homing=false 时拒绝运行
|
||||
- 清 single stepping
|
||||
- 必要时重新 emcTaskPlanOpen(emcStatus->task.file)
|
||||
- 保存 programStartLine
|
||||
- 设置 interpState=READING
|
||||
- 清 task_paused
|
||||
|
||||
/home/meswork/cnc_wams/linuxcnc/src/emc/task/emctaskmain.cc:2337
|
||||
EMC_TASK_PLAN_PAUSE
|
||||
- emcTrajPause()
|
||||
- interpState=PAUSED
|
||||
- task_paused=1
|
||||
|
||||
/home/meswork/cnc_wams/linuxcnc/src/emc/task/emctaskmain.cc:2369
|
||||
EMC_TASK_PLAN_RESUME
|
||||
- emcTrajResume()
|
||||
- 恢复 interpState
|
||||
- 清 task_paused
|
||||
|
||||
/home/meswork/cnc_wams/linuxcnc/src/emc/task/emctask.cc:543
|
||||
emcTaskPlanOpen(file)
|
||||
- 清 motionLine/currentLine/readLine
|
||||
- interp.open(file)
|
||||
- taskplanopen=1
|
||||
|
||||
/home/meswork/cnc_wams/linuxcnc/src/emc/task/emctask.cc:566
|
||||
emcTaskPlanRead()
|
||||
- interp.read()
|
||||
- 文件未打开时用 emcStatus->task.file 重新 open/read
|
||||
```
|
||||
|
||||
对当前 web runtime 的约束:
|
||||
|
||||
```text
|
||||
1. initializeTaskHalSession({ openProgram: true }) 必须等价于先完成 EMC_TASK_PLAN_OPEN。
|
||||
2. RUN gate 必须确认 opened program 与当前 selected G-code 是同一个 staged machine-file。
|
||||
3. RUN 发送 EMC_TASK_PLAN_RUN 后,UI 不能用 canonical timing 或 fixture sample 冒充 task 反馈。
|
||||
4. 连续反馈必须来自 taskHalRuntime.runCycles/readStatus,且 status 的 task/motion/HAL 字段必须同源。
|
||||
5. PAUSE/RESUME/ABORT 要按 LinuxCNC task command 修改状态循环,而不是只改前端 runState。
|
||||
```
|
||||
|
||||
## 6. 详细测试计划
|
||||
|
||||
### 6.1 INI/profile 解析测试
|
||||
|
||||
目标:证明 `RUN` 使用的不是手写机床参数,而是明确 LinuxCNC INI。
|
||||
|
||||
@@ -175,7 +645,7 @@ node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
|
||||
run_preconditions_ini_profile_smoke=ok
|
||||
```
|
||||
|
||||
### 4.2 运动学模块确认测试
|
||||
### 6.2 运动学模块确认测试
|
||||
|
||||
目标:证明当前机床的五轴运动学算法已经按 INI/profile 加载。
|
||||
|
||||
@@ -199,7 +669,7 @@ node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
|
||||
|
||||
新增 RUN 前置测试应把这个结果纳入 `RUN` gate,而不是只作为独立 smoke。
|
||||
|
||||
### 4.3 machine-file staging 测试
|
||||
### 6.3 machine-file staging 测试
|
||||
|
||||
目标:证明 `RUN` 打开的程序、INI、remap、tool table 是同一个 LinuxCNC 机床目录上下文。
|
||||
|
||||
@@ -226,7 +696,7 @@ node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs
|
||||
```
|
||||
|
||||
### 4.4 RUN 连续 feedback 测试
|
||||
### 6.4 RUN 连续 feedback 测试
|
||||
|
||||
目标:证明点击 `RUN` 后,不是只推进一次 `taskCycles=5`,而是持续从 task/HAL/motion 获取反馈。
|
||||
|
||||
@@ -282,7 +752,7 @@ programRuntimeFeedback.sourceMode 不是 linuxcnc-task-motion-hal-wasm。
|
||||
RTCP 状态与当前 G-code 的 M428/M429 不一致。
|
||||
```
|
||||
|
||||
### 4.5 浏览器端测试
|
||||
### 6.5 浏览器端测试
|
||||
|
||||
建议扩展:
|
||||
|
||||
@@ -318,7 +788,7 @@ qa/web-rtcp-5axis-site-test/output/run-preconditions-feedback.json
|
||||
qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/*.png
|
||||
```
|
||||
|
||||
## 5. 编写 RUN 程序的实现步骤
|
||||
## 7. 编写 RUN 程序的实现步骤
|
||||
|
||||
### Step 1: 增加 `validateRunPreconditions(state)`
|
||||
|
||||
@@ -338,6 +808,8 @@ app/src/state/store.js
|
||||
5. 检查 task/HAL runtime ready。
|
||||
6. 检查 machine file staging ready。
|
||||
7. 检查 taskHalSession.programPath 指向当前 G-code。
|
||||
8. 检查 task status 的 opened file 与当前 staged G-code 一致。
|
||||
9. 检查 machine ON、AUTO mode、homed/no_force_homing。
|
||||
```
|
||||
|
||||
返回结构:
|
||||
@@ -376,21 +848,32 @@ runTaskHalCommandSequence([...], { taskCycles: 5 })
|
||||
2. validateRunPreconditions(state)
|
||||
3. initializeTaskHalSession({ openProgram: true }) 如需要
|
||||
4. 确认 readStatus() 的 openProgram/programPath 与 state.activeProgram 对齐
|
||||
5. 如果 task mode 不是 AUTO,先发送 EMC_TASK_SET_MODE AUTO 并等待 status 确认
|
||||
6. 如果 task state 不是 ON,先发送 EMC_TASK_SET_STATE ON 并等待 status 确认
|
||||
```
|
||||
|
||||
未满足前置条件时,只设置 `operatorMessage`,不推进任何 line/sample。
|
||||
|
||||
### Step 3: 拆分 task command 与执行 pump
|
||||
### Step 3: 拆分 task command 与 taskHalStatusLoop
|
||||
|
||||
`RUN` 应只发送 LinuxCNC task 命令:
|
||||
|
||||
```text
|
||||
EMC_TASK_SET_STATE ON
|
||||
EMC_TASK_SET_MODE AUTO
|
||||
EMC_TASK_PLAN_RUN line = activeLine - programStartLine
|
||||
EMC_TASK_PLAN_RUN line = linuxcncStartLine
|
||||
```
|
||||
|
||||
然后启动 feedback pump:
|
||||
说明:
|
||||
|
||||
```text
|
||||
linuxcncStartLine 应按 LinuxCNC 的 program run line 语义保存。
|
||||
普通从头运行传 0。
|
||||
run-from-line 才传明确 line,并需要单独处理 start line、previous modal state 和安全性。
|
||||
不要用 UI activeLine 自行推导 programStartLine。
|
||||
```
|
||||
|
||||
然后启动 `taskHalStatusLoop`:
|
||||
|
||||
```text
|
||||
while running:
|
||||
@@ -399,22 +882,22 @@ while running:
|
||||
dispatch TASK_HAL_STATUS_APPLIED(status)
|
||||
append feedback history
|
||||
if complete/paused/aborted/stopped:
|
||||
stop pump
|
||||
stop taskHalStatusLoop
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
```text
|
||||
这里的 pump 可以在 store 主线程调度,也可以放到 worker 内部 postMessage 推送。
|
||||
这里的 taskHalStatusLoop 可以在 store 主线程调度,也可以放到 worker 内部 postMessage 推送。
|
||||
关键不是 timer 本身,而是每个 tick 都必须由 task/HAL/motion runtime 的 runCycles/readStatus 产生反馈。
|
||||
```
|
||||
|
||||
### Step 4: 增加 RUN pump 状态
|
||||
### Step 4: 增加 RUN status loop 状态
|
||||
|
||||
建议 state 增加:
|
||||
|
||||
```js
|
||||
taskHalRunPump: {
|
||||
taskHalStatusLoop: {
|
||||
active: false,
|
||||
sequence: 0,
|
||||
profileId: null,
|
||||
@@ -457,24 +940,24 @@ programRuntimeFeedback
|
||||
4. 运行态 axisPose、velocity、DTG、activeLine 必须来自同一个 status snapshot。
|
||||
```
|
||||
|
||||
### Step 6: STOP/ABORT/PAUSE/RESUME 控制 pump
|
||||
### Step 6: STOP/ABORT/PAUSE/RESUME 控制 taskHalStatusLoop
|
||||
|
||||
行为要求:
|
||||
|
||||
```text
|
||||
STOP/ABORT:
|
||||
send EMC_TASK_ABORT
|
||||
stop pump
|
||||
stop taskHalStatusLoop
|
||||
read final status
|
||||
|
||||
PAUSE:
|
||||
send EMC_TASK_PLAN_PAUSE
|
||||
pump 可停止或降频读取 paused status
|
||||
taskHalStatusLoop 可停止或降频读取 paused status
|
||||
runState=paused
|
||||
|
||||
RESUME:
|
||||
send EMC_TASK_PLAN_RESUME
|
||||
restart pump
|
||||
restart taskHalStatusLoop
|
||||
|
||||
STEP:
|
||||
不走 fixture sample playback
|
||||
@@ -509,7 +992,7 @@ fullLinuxCncProgramExecutionReady=false
|
||||
|
||||
在真实 `taskHalRuntime.loaded === true` 且前置条件失败时,不应退回 fixture playback。
|
||||
|
||||
## 6. 开发验收命令
|
||||
## 8. 开发验收命令
|
||||
|
||||
基础检查:
|
||||
|
||||
@@ -518,6 +1001,7 @@ node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
```
|
||||
|
||||
@@ -540,7 +1024,7 @@ QA 证据采集:
|
||||
node qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
```
|
||||
|
||||
## 7. 完成标准
|
||||
## 9. 完成标准
|
||||
|
||||
`RUN` 可以判定为完善,必须同时满足:
|
||||
|
||||
@@ -553,6 +1037,7 @@ node qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
6. RUN 后 taskCycle/servoCycle 随时间持续推进,直到 paused/stopped/complete。
|
||||
7. UI 高亮行和执行历史能复现逐行执行过程。
|
||||
8. fallback playback 不能冒充 LinuxCNC task/HAL runtime。
|
||||
9. RUN 失败时能明确区分 INI/profile、open program、machine ON、AUTO、homed、runtime readiness 哪一项未满足。
|
||||
```
|
||||
|
||||
未满足以上条件时,状态应继续标记为:
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# 04 RUN status loop 完成记录与下一步建议
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 本轮完成范围
|
||||
|
||||
本轮完成 `03-run-step1-preconditions-complete-next-steps.md` 建议中的连续 feedback 工作,但命名从 `pump` 调整为更明确的:
|
||||
|
||||
```text
|
||||
taskHalStatusLoop
|
||||
```
|
||||
|
||||
这里的 loop 不生成 CNC 状态,不解析 G-code,也不自增行号。它只负责在 `RUN` 之后持续调度:
|
||||
|
||||
```text
|
||||
taskHalRuntime.runCycles(...)
|
||||
taskHalRuntime.readStatus()
|
||||
dispatch TASK_HAL_STATUS_APPLIED
|
||||
```
|
||||
|
||||
`activeLine`、`axisPose`、`currentVelocity`、`switchkinsType`、DRO 和 runtime feedback 仍来自同一个 task/HAL/motion status snapshot。
|
||||
|
||||
## 2. 已完成改动
|
||||
|
||||
### 2.1 store 增加 taskHalStatusLoop
|
||||
|
||||
更新:
|
||||
|
||||
```text
|
||||
app/src/state/store.js
|
||||
```
|
||||
|
||||
新增状态:
|
||||
|
||||
```text
|
||||
taskHalStatusLoop:
|
||||
active
|
||||
sequence
|
||||
profileId
|
||||
iniPath
|
||||
kinematicsModuleId
|
||||
tickCount
|
||||
batchSize
|
||||
intervalMs
|
||||
taskPeriodNs
|
||||
servoPeriodNs
|
||||
lastStatusAt
|
||||
lastError
|
||||
stopReason
|
||||
```
|
||||
|
||||
并新增:
|
||||
|
||||
```text
|
||||
programRuntimeFeedbackHistory
|
||||
```
|
||||
|
||||
最多保留 100 条 task/HAL/motion runtime feedback。
|
||||
|
||||
### 2.2 RUN 改为连续读取 task/HAL status
|
||||
|
||||
`RUN` 当前流程:
|
||||
|
||||
```text
|
||||
1. validateRunPreconditions(..., requireTaskHalSession=false)
|
||||
2. 初始化或复用当前 machine-file task/HAL session
|
||||
3. validateRunPreconditions(..., requireTaskHalSession=true)
|
||||
4. 发送:
|
||||
EMC_TASK_SET_STATE ON
|
||||
EMC_TASK_SET_MODE AUTO
|
||||
EMC_TASK_PLAN_RUN
|
||||
5. runCycles/readStatus 一次,应用首帧 status
|
||||
6. 如果 status 仍处于 READING,则启动 taskHalStatusLoop
|
||||
7. loop 持续 runCycles/readStatus,并通过 TASK_HAL_STATUS_APPLIED 更新 UI 状态
|
||||
```
|
||||
|
||||
停止条件:
|
||||
|
||||
```text
|
||||
interpState=IDLE 且 nextProgramLine >= openedLineCount -> complete
|
||||
interpState=PAUSED 或 motion.paused=true -> paused
|
||||
motion.aborted=true -> stopped
|
||||
```
|
||||
|
||||
### 2.3 STOP / ABORT / PAUSE / RESUME / STEP
|
||||
|
||||
更新行为:
|
||||
|
||||
```text
|
||||
STOP/ABORT:
|
||||
先停止 taskHalStatusLoop,再发送 EMC_TASK_ABORT,并读取一次 status。
|
||||
|
||||
PAUSE:
|
||||
先停止 taskHalStatusLoop,再发送 EMC_TASK_PLAN_PAUSE,并读取一次 paused status。
|
||||
|
||||
RESUME:
|
||||
发送 EMC_TASK_PLAN_RESUME,若 status 回到 READING,则重新启动 taskHalStatusLoop。
|
||||
|
||||
STEP:
|
||||
task/HAL runtime loaded 时不走 fixture playback。
|
||||
改为明确 runCycles({ taskCycles: 1 }) 后 readStatus 一次。
|
||||
```
|
||||
|
||||
### 2.4 新增验证
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
tests/node/verify_run_feedback_loop.mjs
|
||||
```
|
||||
|
||||
覆盖:
|
||||
|
||||
```text
|
||||
1. 加载 xyzac-trt INI。
|
||||
2. 加载 xyzac-trt kinematics WASM。
|
||||
3. 加载 task/HAL runtime。
|
||||
4. stage machine files。
|
||||
5. 打开 xyzac_switchkins_test_1.ngc。
|
||||
6. POWER/HOME/AUTO/RUN。
|
||||
7. 等待 programRuntimeFeedbackHistory 至少 3 条。
|
||||
8. 断言每条 feedback sourceMode 都是 linuxcnc-task-motion-hal-wasm。
|
||||
9. 断言 semanticBoundary 是 linuxcnc_task_motion_hal_wasm_simulation_runtime。
|
||||
10. 断言 taskCycle 和 servoCycle 单调递增。
|
||||
11. 断言 STOP 后 taskHalStatusLoop.active=false。
|
||||
```
|
||||
|
||||
`app/package.json` 的 `smoke:node` 已加入:
|
||||
|
||||
```text
|
||||
node ../tests/node/verify_run_feedback_loop.mjs
|
||||
```
|
||||
|
||||
## 3. 已运行验证
|
||||
|
||||
通过:
|
||||
|
||||
```bash
|
||||
node tests/node/verify_run_feedback_loop.mjs
|
||||
node tests/node/verify_linuxcnc_task_hal_runtime.mjs
|
||||
node tests/node/verify_run_preconditions.mjs
|
||||
node tests/node/verify_rtcp_store.mjs
|
||||
npm --prefix app run build
|
||||
npm --prefix app run smoke:node
|
||||
```
|
||||
|
||||
关键输出:
|
||||
|
||||
```text
|
||||
run_feedback_status_loop_smoke=ok
|
||||
linuxcnc_task_hal_runtime_smoke=ok
|
||||
run_preconditions_ini_profile_smoke=ok
|
||||
rtcp_store_smoke=ok
|
||||
gmoccapy_static_build=ok
|
||||
```
|
||||
|
||||
本轮 `npm --prefix app run smoke:node` 已完整通过,包括 native task/HAL audit。
|
||||
|
||||
## 4. 当前 RUN 状态
|
||||
|
||||
当前 `RUN` 已经从:
|
||||
|
||||
```text
|
||||
发送 task command -> runCycles({ taskCycles: 5 }) -> readStatus() 一次
|
||||
```
|
||||
|
||||
推进为:
|
||||
|
||||
```text
|
||||
发送 task command
|
||||
-> runCycles/readStatus 首帧
|
||||
-> taskHalStatusLoop 持续 runCycles/readStatus
|
||||
-> TASK_HAL_STATUS_APPLIED 持续消费同一 task/HAL/motion status 链
|
||||
```
|
||||
|
||||
重要边界仍保持:
|
||||
|
||||
```text
|
||||
不引入 JS G-code parser。
|
||||
不在 UI 自增 activeLine。
|
||||
不在 UI 自造 axisPose。
|
||||
不把 canonical timing estimate 当作真实 task/HAL runtime。
|
||||
status 内容必须来自 task/HAL/motion runtime。
|
||||
```
|
||||
|
||||
## 5. 下一步工作建议
|
||||
|
||||
下一步建议做浏览器端证据采集:
|
||||
|
||||
```text
|
||||
1. 扩展 qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs。
|
||||
2. 新增 07-run-preconditions-and-feedback case。
|
||||
3. 浏览器中执行 POWER/HOME/AUTO/RUN。
|
||||
4. 采集 t=0.2s / 0.5s / 1.0s / 2.0s state。
|
||||
5. 断言:
|
||||
- taskHalStatusLoop.tickCount 增长
|
||||
- programRuntimeFeedbackHistory 增长
|
||||
- activeLine 等于 UI 高亮行
|
||||
- DRO 读取 state.programRuntimeFeedback.axisPose
|
||||
- RTCP canvas 状态与 state.rtcpState 一致
|
||||
6. 输出 JSON 和截图证据。
|
||||
```
|
||||
|
||||
随后再考虑:
|
||||
|
||||
```text
|
||||
1. 把 M428/M429 switchkins 状态变化纳入 RUN loop 浏览器断言。
|
||||
2. 增加 xyzbc-trt 的 RUN status loop 对称测试。
|
||||
3. 如果需要更接近真实 UI 实时性,再评估是否把 loop 下沉到 worker push status,而不是 store setTimeout polling。
|
||||
```
|
||||
@@ -0,0 +1,149 @@
|
||||
# 06 RUN 实施完成情况矩阵与接续工作
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 本轮执行范围
|
||||
|
||||
按 `05-run-program-implementation-detailed-steps.md` 核对并补强当前实现,本轮完成:
|
||||
|
||||
```text
|
||||
1. 建立 05 阶段完成情况矩阵。
|
||||
2. 补强 INI definition contract validation。
|
||||
3. 修正普通 RUN 的 EMC_TASK_PLAN_RUN 起始行为 line=0。
|
||||
4. 补充 INI runtime 测试断言。
|
||||
5. 执行 node smoke 验证。
|
||||
6. 记录剩余接续工作。
|
||||
```
|
||||
|
||||
## 2. 本轮代码改动
|
||||
|
||||
### 2.1 INI contract validation 补强
|
||||
|
||||
更新:
|
||||
|
||||
```text
|
||||
app/src/runtime/linuxcnc-ini-runtime.js
|
||||
tests/node/verify_linuxcnc_ini_runtime.mjs
|
||||
```
|
||||
|
||||
补强内容:
|
||||
|
||||
```text
|
||||
1. parseLinuxCncIni() 现在保留 sourceText。
|
||||
2. 解析并输出 emcmot.module / emcmot.servoPeriodNs。
|
||||
3. 解析并输出 task.module / task.cycleTimeSeconds。
|
||||
4. emcio.toolTable 保持结构化输出。
|
||||
5. applyIniConfigToProfile() 将 emcmot/task/emcio 写入 profile。
|
||||
6. validateIniConfig() 增加 05 要求的关键 gate:
|
||||
- required sections: [EMC], [DISPLAY], [RS274NGC], [KINS], [HAL], [HALUI], [TRAJ], [EMCMOT], [TASK], [EMCIO]
|
||||
- COORDINATES 必须是 XYZAC 或 XYZBC
|
||||
- JOINTS 必须是 5
|
||||
- JOINT_0..JOINT_4 必须存在
|
||||
- KINEMATICS 必须包含 sparm=identityfirst
|
||||
- M428/M429/M430 remap 必须完整
|
||||
- HAL_PIN_VARS=1 必须存在
|
||||
- HALCMD 必须连接 motion.analog-out-03 与 motion.switchkins-type
|
||||
- SERVO_PERIOD / CYCLE_TIME / TOOL_TABLE 必须存在
|
||||
```
|
||||
|
||||
### 2.2 RUN 起始行修正
|
||||
|
||||
更新:
|
||||
|
||||
```text
|
||||
app/src/state/store.js
|
||||
```
|
||||
|
||||
修正:
|
||||
|
||||
```text
|
||||
普通 RUN 发送 EMC_TASK_PLAN_RUN line=0。
|
||||
不再用 activeLine - programStartLine 推导普通 RUN 起始行。
|
||||
run-from-line 后续应作为独立功能处理。
|
||||
```
|
||||
|
||||
## 3. 完成情况矩阵
|
||||
|
||||
| 05 阶段 | 状态 | 证据 | 备注 |
|
||||
| --- | --- | --- | --- |
|
||||
| 阶段 0:建立当前状态基线 | 完成 | `smoke:node` 完整通过 | 当前基线可复现 |
|
||||
| 阶段 1:INI definition contract | 完成 | `verify_linuxcnc_ini_runtime.mjs` | 本轮补强 section/字段 validation |
|
||||
| 阶段 2:绑定 kinematics runtime | 完成 | `verify_linuxcnc_kinematics_runtime.mjs`, `verify_run_preconditions.mjs` | RUN gate 检查 module/frame source |
|
||||
| 阶段 3:machine-file staging | 完成 | `verify_machine_file_staging.mjs` | selected G-code 与 wasmProgramPath 同步 |
|
||||
| 阶段 4:初始化 task/HAL session | 完成 | `verify_linuxcnc_task_hal_runtime.mjs` | openProgram/session/status 链路通过 |
|
||||
| 阶段 5:RUN preconditions gate | 完成 | `verify_run_preconditions.mjs` | 仍可继续扩展更细 ON/AUTO/homed 阻断文案 |
|
||||
| 阶段 6:RUN command sequence | 完成 | `verify_run_feedback_loop.mjs` | 本轮修正普通 RUN line=0 |
|
||||
| 阶段 7:taskHalStatusLoop | 完成 | `verify_run_feedback_loop.mjs` | tick/history/STOP 断言通过 |
|
||||
| 阶段 8:统一 TASK_HAL_STATUS_APPLIED | 完成 | `verify_run_feedback_loop.mjs`, `verify_rtcp_store.mjs` | feedback history 保留 task/HAL/motion source |
|
||||
| 阶段 9:STOP/ABORT/PAUSE/RESUME/STEP | 部分完成 | `verify_linuxcnc_task_hal_runtime.mjs` | PAUSE/RESUME 有测试;STOP 有 feedback-loop 测试;STEP/ABORT 仍需专门断言 |
|
||||
| 阶段 10:UI 显示与交互 | 部分完成 | 现有 browser smoke 未在本轮执行 | 仍需 07 浏览器证据 case |
|
||||
| 阶段 11:full execution boundary | 完成 | `verify_full_execution_boundary.mjs`, native audit | web simulation boundary 通过 |
|
||||
| 阶段 12:Node smoke 集成 | 完成 | `npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node` | 完整通过 |
|
||||
| 阶段 13:浏览器证据采集 | 未完成 | 无新 JSON/截图 | 下一步工作 |
|
||||
|
||||
## 4. 本轮验证结果
|
||||
|
||||
通过:
|
||||
|
||||
```bash
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_ini_runtime.mjs
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
```
|
||||
|
||||
关键输出:
|
||||
|
||||
```text
|
||||
linuxcnc_ini_runtime_smoke=ok
|
||||
gmoccapy_static_build=ok
|
||||
run_feedback_status_loop_smoke=ok
|
||||
linuxcnc_task_hal_runtime_smoke=ok
|
||||
full_execution_boundary_smoke=ok
|
||||
machine_file_staging_smoke=ok
|
||||
rtcp_store_smoke=ok
|
||||
profile_boundary_smoke=ok
|
||||
```
|
||||
|
||||
`smoke:node` 已完整通过。
|
||||
|
||||
## 5. 剩余接续工作
|
||||
|
||||
下一步建议优先做浏览器证据采集:
|
||||
|
||||
```text
|
||||
1. 扩展 qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs。
|
||||
2. 新增 07-run-preconditions-and-feedback case。
|
||||
3. 浏览器执行 POWER/HOME/AUTO/RUN。
|
||||
4. 采集 t=0.2s / 0.5s / 1.0s / 2.0s / 5.0s state。
|
||||
5. 断言 taskHalStatusLoop.tickCount 和 programRuntimeFeedbackHistory 增长。
|
||||
6. 断言 feedback sourceMode 不含 fixture-line-playback。
|
||||
7. 输出 JSON 与截图:
|
||||
qa/web-rtcp-5axis-site-test/output/run-preconditions-feedback.json
|
||||
qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/*.png
|
||||
```
|
||||
|
||||
随后补强控制类测试:
|
||||
|
||||
```text
|
||||
1. ABORT 后 loop active=false,runState/stopped status 正确。
|
||||
2. STEP 在 task/HAL loaded 时 sourceMode 仍是 linuxcnc-task-motion-hal-wasm。
|
||||
3. RUN gate 对 machine ON / AUTO / homed / opened-file 增加更细粒度失败断言。
|
||||
4. taskHalStatusLoop 的 taskPeriodNs/servoPeriodNs 从 INI CYCLE_TIME/SERVO_PERIOD 派生,而不是只使用默认值。
|
||||
5. xyzbc-trt 增加对称 RUN status loop 测试。
|
||||
```
|
||||
|
||||
## 6. 当前结论
|
||||
|
||||
当前 RUN 主链路已经满足 05 的 node 侧核心完成标准:
|
||||
|
||||
```text
|
||||
INI contract ready
|
||||
-> profile/INI/kinematics 一致
|
||||
-> machine files staged
|
||||
-> selected G-code opened by task/HAL session
|
||||
-> RUN sends LinuxCNC task commands
|
||||
-> taskHalStatusLoop continuously runCycles/readStatus
|
||||
-> UI/store consumes TASK_HAL_STATUS_APPLIED snapshots
|
||||
```
|
||||
|
||||
尚未完成的是浏览器端 JSON/截图证据,以及 ABORT/STEP/xyzbc 对称测试的补强。
|
||||
@@ -0,0 +1,135 @@
|
||||
# 07 浏览器 RUN feedback 证据完成记录与下一步
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 本轮执行范围
|
||||
|
||||
按 `06-run-implementation-traceability-matrix-and-next-steps.md` 的优先级,完成浏览器端 `07-run-preconditions-and-feedback` 证据采集。
|
||||
|
||||
本轮没有继续扩展 ABORT/STEP/xyzbc 对称测试。
|
||||
|
||||
## 2. 已完成改动
|
||||
|
||||
更新:
|
||||
|
||||
```text
|
||||
qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
```
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
07-run-preconditions-and-feedback
|
||||
```
|
||||
|
||||
浏览器流程:
|
||||
|
||||
```text
|
||||
1. 等待 app、INI、kinematics runtime、machine files ready。
|
||||
2. 选择 LinuxCNC vendored G-code:
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc
|
||||
3. 确认 taskHalSession.programPath 已 open 同一个 G-code。
|
||||
4. POWER -> MANUAL -> HOME -> AUTO。
|
||||
5. RUN。
|
||||
6. 采集 baseline 和 0.2s / 0.5s / 1.0s / 2.0s / 5.0s state。
|
||||
7. 截图并输出 JSON 证据。
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```text
|
||||
qa/web-rtcp-5axis-site-test/output/run-preconditions-feedback.json
|
||||
qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/
|
||||
```
|
||||
|
||||
## 3. 断言结果
|
||||
|
||||
`run-preconditions-feedback.json` 当前状态:
|
||||
|
||||
```text
|
||||
status=PASS
|
||||
```
|
||||
|
||||
通过的关键断言:
|
||||
|
||||
```text
|
||||
1. INI ready。
|
||||
2. selected G-code 是 xyzac_switchkins_test_1.ngc。
|
||||
3. task/HAL session opened selected G-code。
|
||||
4. RUN 后 taskHalStatusLoop sequence 从 1 变为 2。
|
||||
5. 本次 RUN tickCount=4。
|
||||
6. 本次 RUN programRuntimeFeedbackHistory=5。
|
||||
7. feedback sourceMode 全部是 linuxcnc-task-motion-hal-wasm。
|
||||
8. 不含 fixture-line-playback feedback。
|
||||
9. semanticBoundary 全部是 linuxcnc_task_motion_hal_wasm_simulation_runtime。
|
||||
10. activeLine 等于 UI 高亮行。
|
||||
11. DRO 等于 runtime feedback axisPose。
|
||||
12. RTCP canvas 状态与 state.rtcpState 一致。
|
||||
13. taskCycle/servoCycle 可见。
|
||||
14. canvas 执行轨迹可见。
|
||||
```
|
||||
|
||||
样本摘要:
|
||||
|
||||
```text
|
||||
t=0.2s / 0.5s / 1.0s / 2.0s / 5.0s
|
||||
runState=complete
|
||||
taskHalStatusLoop.sequence=2
|
||||
taskHalStatusLoop.tickCount=4
|
||||
programRuntimeFeedbackHistory.length=5
|
||||
activeLine=29
|
||||
sourceMode=linuxcnc-task-motion-hal-wasm
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
```text
|
||||
xyzac_switchkins_test_1.ngc 很短,浏览器首个 0.2s 样本时程序已 complete。
|
||||
因此证据不要求 5 秒内持续 active=true,而是验证本次 RUN 的新 loop sequence、tick/history、task/HAL feedback 和 UI snapshot 一致性。
|
||||
```
|
||||
|
||||
## 4. 已运行验证
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
node qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
01-home-toolpath: PASS
|
||||
02-operator-demo-toolpath: PASS
|
||||
03-arc-demo-toolpath: PASS
|
||||
04-clear-preview-reference: PASS
|
||||
05-vendored-impeller-toolpath: PASS
|
||||
06-running-rtcp-toolpath: PASS
|
||||
07-run-preconditions-and-feedback: PASS
|
||||
```
|
||||
|
||||
## 5. 接续工作
|
||||
|
||||
下一步建议补控制类 node 测试:
|
||||
|
||||
```text
|
||||
1. ABORT 后 taskHalStatusLoop.active=false,runState/stopped status 正确。
|
||||
2. STEP 在 task/HAL loaded 时 sourceMode 仍是 linuxcnc-task-motion-hal-wasm。
|
||||
3. RUN gate 对 machine ON / AUTO / homed / opened-file 增加更细粒度失败断言。
|
||||
4. taskHalStatusLoop 的 taskPeriodNs/servoPeriodNs 从 INI CYCLE_TIME/SERVO_PERIOD 派生。
|
||||
5. xyzbc-trt 增加 RUN status loop 对称测试。
|
||||
```
|
||||
|
||||
## 6. 当前完成矩阵变化
|
||||
|
||||
`06` 中的阶段 13 状态从:
|
||||
|
||||
```text
|
||||
未完成:无新 JSON/截图
|
||||
```
|
||||
|
||||
更新为:
|
||||
|
||||
```text
|
||||
完成:run-preconditions-feedback.json + run-preconditions-feedback/*.png,07 case PASS
|
||||
```
|
||||
@@ -0,0 +1,131 @@
|
||||
# 08 LinuxCNC 源程序测试资产
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 选择结果
|
||||
|
||||
本轮按用户要求,从 `/home/meswork/cnc_wams/linuxcnc` LinuxCNC 源程序中选定一组更适合项目测试的真实 RTCP/五轴资产:
|
||||
|
||||
```text
|
||||
INI:
|
||||
/home/meswork/cnc_wams/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
|
||||
|
||||
G-code:
|
||||
/home/meswork/cnc_wams/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc
|
||||
```
|
||||
|
||||
已复制到 `working_run`:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source/xyzac-trt.ini
|
||||
web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source/impeller-7bl-xyzac.ngc
|
||||
```
|
||||
|
||||
已移除不合适的短程序副本:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source/xyzac_switchkins_test_1.ngc
|
||||
```
|
||||
|
||||
## 2. 选择原因
|
||||
|
||||
选择 `xyzac-trt.ini` + `impeller-7bl-xyzac.ngc`,原因:
|
||||
|
||||
```text
|
||||
1. INI 是 LinuxCNC 官方 5axis table-rotary-tilting 仿真配置。
|
||||
2. 坐标为 XYZAC,与当前项目默认 profile `xyzac-trt` 一致。
|
||||
3. KINEMATICS = xyzac-trt-kins sparm=identityfirst。
|
||||
4. 包含完整 M428/M429/M430 remap contract。
|
||||
5. 包含 HAL_PIN_VARS=1。
|
||||
6. 包含 motion.analog-out-03 => motion.switchkins-type HALCMD。
|
||||
7. G-code 以 M428 ;TCP:xyzac 进入 TCP/RTCP kinematics。
|
||||
8. G-code 使用 G93 inverse-time feed。
|
||||
9. G-code 包含大量 XYZAC 五轴联动段。
|
||||
10. G-code 共 4510 行,比短 switchkins test 程序更适合测试项目功能完整性。
|
||||
11. G-code 来自 /home/meswork/cnc_wams/linuxcnc 源程序,不是项目手写 fixture。
|
||||
```
|
||||
|
||||
## 3. 源文件哈希
|
||||
|
||||
当前副本与 `/home/meswork/cnc_wams/linuxcnc` 源文件逐字节一致:
|
||||
|
||||
```text
|
||||
xyzac-trt.ini
|
||||
sha256=4b7fa6cb8d3e7031e769b3e0c779b586b67cfd45d715985f40d4ec23c662dc97
|
||||
|
||||
impeller-7bl-xyzac.ngc
|
||||
sha256=e90f0b4b6c43809da94a8170ee1029b5afc3e1bbe9bf2ae66298a8baefad013c
|
||||
```
|
||||
|
||||
验证结果:
|
||||
|
||||
```text
|
||||
ini_cmp=0
|
||||
ngc_cmp=0
|
||||
```
|
||||
|
||||
## 4. 测试覆盖点
|
||||
|
||||
这组资产覆盖:
|
||||
|
||||
```text
|
||||
1. LinuxCNC INI definition contract。
|
||||
2. profile/INI 坐标一致性:XYZAC。
|
||||
3. kinematics module 一致性:xyzac-trt。
|
||||
4. RTCP/TCP kinematics:
|
||||
M428 -> xyzac TCP
|
||||
5. inverse-time feed:
|
||||
G93
|
||||
6. 长路径五轴联动:
|
||||
XYZAC motion lines with A/C rotary axes
|
||||
7. machine-file staging。
|
||||
8. taskHalRuntime.openProgram(programPath)。
|
||||
9. RUN command sequence。
|
||||
10. taskHalStatusLoop feedback。
|
||||
11. UI activeLine/DRO/RTCP/runtime feedback same-snapshot 证据。
|
||||
12. 长路径 preview、执行轨迹、rapid/feed 与五轴姿态变化。
|
||||
```
|
||||
|
||||
## 5. 已运行验证
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_real_linuxcnc_5axis_program_cases.mjs
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
linuxcnc_source_program_case_count=8
|
||||
all_cases_program_preview_points=ok
|
||||
all_cases_executed_path_points=ok_after_run_or_step
|
||||
all_cases_toolpath_preview_source=linuxcnc_interpreter_canonical_motion
|
||||
all_cases_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback
|
||||
all_switchkins_cases_rtcp_state_changes_verified=1
|
||||
all_cases_source_guard=linuxcnc_vendored_5axis_gcode_source_file
|
||||
fixture_toolpath_fallback_not_promoted=1
|
||||
real_linuxcnc_5axis_program_cases_smoke=ok
|
||||
```
|
||||
|
||||
## 6. 后续使用方式
|
||||
|
||||
后续新增测试时优先引用这组资产:
|
||||
|
||||
```text
|
||||
working_run/test_linuxcnc_source/xyzac-trt.ini
|
||||
working_run/test_linuxcnc_source/impeller-7bl-xyzac.ngc
|
||||
```
|
||||
|
||||
如果要测试短 switchkins identity/TCP 循环,再补充:
|
||||
|
||||
```text
|
||||
/home/meswork/cnc_wams/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc
|
||||
```
|
||||
|
||||
如果要测试 xyzbc 对称链路,再补充:
|
||||
|
||||
```text
|
||||
/home/meswork/cnc_wams/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini
|
||||
/home/meswork/cnc_wams/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
# 09 单位换算与 Three.js 米单位场景完成记录
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 本轮目标
|
||||
|
||||
解决英寸、毫米、米之间的单位换算问题,重点覆盖:
|
||||
|
||||
- 实时轴值显示;
|
||||
- 刀具路径预览;
|
||||
- 刀具执行轨迹;
|
||||
- Three.js 绘图坐标统一使用米。
|
||||
|
||||
## 完成情况矩阵
|
||||
|
||||
| 项目 | 完成状态 | 代码落点 | 验证 |
|
||||
| --- | --- | --- | --- |
|
||||
| 公共线性单位换算 | 已完成 | `app/src/runtime/linear-units.js` | `verify_linear_unit_conversion.mjs` |
|
||||
| mm/inch/m 到米转换 | 已完成 | `linearValueToMeters()`、`axesToSceneMeters()` | `25.4mm == 1inch == 0.0254m` |
|
||||
| 执行计时距离归一到毫米 | 已完成 | `app/src/runtime/execution-timing.js` | inch 段长 `1 inch -> 25.4 mm` |
|
||||
| 执行计时进给归一到 mm/min | 已完成 | `app/src/runtime/execution-timing.js` | inch feed `1 inch/min -> 25.4 mm/min` |
|
||||
| G20/G21 程序单位解析 | 已完成 | `app/src/runtime/linuxcnc-interpreter-runtime.js` | G20 motion 标记 `inch`,G21 motion 标记 `mm` |
|
||||
| 刀具路径预览使用米 | 已完成 | `app/src/visualization/five-axis-scene.js` | motion axes 经 `axesToSceneMeters()` |
|
||||
| 刀具执行轨迹使用米 | 已完成 | `app/src/visualization/five-axis-scene.js` | timing samples 携带/继承 motion 单位 |
|
||||
| 当前刀位 marker 使用米 | 已完成 | `app/src/visualization/five-axis-scene.js` | runtime feedback linearUnits 参与转换 |
|
||||
| Three.js 机床参考模型使用米尺度 | 已完成 | `app/src/visualization/five-axis-scene.js` | 参考模型尺寸从任意倍率改为米尺度 |
|
||||
| 2D fallback 使用米尺度 | 已完成 | `app/src/visualization/five-axis-scene.js` | fallback scale 改为米坐标范围 |
|
||||
| 轴值实时显示 | 已确认边界 | `store.axisPose` / DRO | 保留机床/程序单位,不改成米 |
|
||||
|
||||
## 关键设计结论
|
||||
|
||||
实时轴值显示不应强制转换为米。DRO/axisPose 表示当前机床或程序坐标单位,应保留 LinuxCNC/INI/G-code 的语义;Three.js 预览层才统一转换为米。
|
||||
|
||||
Three.js 场景现在通过 dataset 暴露:
|
||||
|
||||
```text
|
||||
data-three-scene-units="m"
|
||||
data-three-linear-units="<mm|inch|m>"
|
||||
data-three-linear-unit-scale-to-meters="<factor>"
|
||||
```
|
||||
|
||||
## 已执行验证
|
||||
|
||||
```text
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linear_unit_conversion.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_real_linuxcnc_5axis_program_cases.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_ini_runtime.mjs
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
```
|
||||
|
||||
结果均通过。
|
||||
|
||||
## 后续工作
|
||||
|
||||
1. 增加浏览器截图证据,确认米尺度 Three.js 画面在 desktop/mobile 下不空白、不偏移。
|
||||
2. 如果后续接入真实 task/motion HAL feedback,需要确认 feedback 坐标是否已经按 INI 单位输出,并继续填充 `programRuntimeFeedback.linearUnits`。
|
||||
3. 可继续补 G-code 中途 G20/G21 切换的多段浏览器端预览用例。
|
||||
@@ -0,0 +1,210 @@
|
||||
# 10 RUN 控制类测试与 INI 周期派生详细步骤
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 本轮目标
|
||||
|
||||
在 `05` 到 `09` 已完成 RUN 主链路、浏览器 RUN 证据和米单位场景的基础上,继续补齐 `07`、`09` 中列出的剩余可自动验证项。
|
||||
|
||||
本轮执行范围:
|
||||
|
||||
```text
|
||||
1. ABORT 后 taskHalStatusLoop.active=false,runState/status 正确。
|
||||
2. STEP 在 task/HAL runtime loaded 时仍消费 linuxcnc-task-motion-hal-wasm feedback。
|
||||
3. RUN gate 对 machine ON / AUTO / homed / opened-file 保持细粒度失败断言。
|
||||
4. taskHalStatusLoop 的 taskPeriodNs/servoPeriodNs 从 INI TASK.CYCLE_TIME / EMCMOT.SERVO_PERIOD 派生。
|
||||
5. xyzbc-trt 增加 RUN status loop 对称 node 测试。
|
||||
```
|
||||
|
||||
不在本轮执行:
|
||||
|
||||
```text
|
||||
1. 新增浏览器 desktop/mobile 米尺度截图证据。
|
||||
2. G20/G21 中途切换浏览器端预览用例。
|
||||
3. 新增真实 LinuxCNC native 对照资产。
|
||||
```
|
||||
|
||||
## 2. 执行顺序
|
||||
|
||||
### 步骤 1:建立当前基线
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
|
||||
```
|
||||
|
||||
通过标准:
|
||||
|
||||
```text
|
||||
现有 xyzac RUN feedback loop、RUN preconditions、task/HAL runtime smoke 均可复现。
|
||||
```
|
||||
|
||||
### 步骤 2:实现 INI 周期派生
|
||||
|
||||
代码落点:
|
||||
|
||||
```text
|
||||
app/src/state/store.js
|
||||
```
|
||||
|
||||
实现要求:
|
||||
|
||||
```text
|
||||
1. 从 state.linuxCncIniConfig.task.cycleTimeSeconds 派生 taskPeriodNs。
|
||||
2. 从 state.linuxCncIniConfig.emcmot.servoPeriodNs 派生 servoPeriodNs。
|
||||
3. runTaskHalCommandSequence 默认使用派生周期。
|
||||
4. startTaskHalStatusLoop 默认使用派生周期。
|
||||
5. 如果 INI 字段缺失,才回退到原默认值 task=10000000ns / servo=1000000ns。
|
||||
```
|
||||
|
||||
验收断言:
|
||||
|
||||
```text
|
||||
xyzac-trt.ini: TASK.CYCLE_TIME=0.010 -> taskPeriodNs=10000000。
|
||||
xyzac-trt.ini: EMCMOT.SERVO_PERIOD=1000000 -> servoPeriodNs=1000000。
|
||||
```
|
||||
|
||||
### 步骤 3:补 ABORT/STEP node 测试
|
||||
|
||||
测试落点:
|
||||
|
||||
```text
|
||||
tests/node/verify_run_feedback_loop.mjs
|
||||
```
|
||||
|
||||
新增断言:
|
||||
|
||||
```text
|
||||
1. RUN 后 loop sequence/tick/history 由 task/HAL feedback 产生。
|
||||
2. ABORT 后 taskHalStatusLoop.active=false。
|
||||
3. ABORT 后 stopReason 为 aborted 或 runState 被 task/HAL status 收敛到 stopped/complete。
|
||||
4. STEP 后 programRuntimeFeedback.sourceMode 是 linuxcnc-task-motion-hal-wasm。
|
||||
5. STEP 后不出现 fixture-line-playback。
|
||||
```
|
||||
|
||||
### 步骤 4:补 RUN gate 细粒度断言
|
||||
|
||||
测试落点:
|
||||
|
||||
```text
|
||||
tests/node/verify_run_preconditions.mjs
|
||||
```
|
||||
|
||||
新增断言:
|
||||
|
||||
```text
|
||||
1. INI/profile/kinematics/machine-file ready 但 machine off 时,RUN gate 返回 machine must be on。
|
||||
2. machine on 但未 home 时,RUN gate 返回 home machine first。
|
||||
3. homed 但 manual mode 时,RUN gate 返回 switch to auto mode first。
|
||||
4. selected G-code/session 缺失时,validateRunPreconditions 返回 no machine-file G-code opened。
|
||||
```
|
||||
|
||||
### 步骤 5:补 xyzbc RUN status loop 对称测试
|
||||
|
||||
测试落点:
|
||||
|
||||
```text
|
||||
tests/node/verify_run_feedback_loop.mjs
|
||||
```
|
||||
|
||||
流程:
|
||||
|
||||
```text
|
||||
1. 加载 profile=xyzbc-trt。
|
||||
2. 解析 xyzbc-trt.ini。
|
||||
3. attach xyzbc-trt kinematics runtime。
|
||||
4. stage machine files。
|
||||
5. 选择 xyzbc_switchkins.ngc 或 boat-xyzbc.ngc。
|
||||
6. POWER -> HOME -> AUTO -> RUN。
|
||||
7. 断言 taskHalStatusLoop/profile/kinematics/sourceMode/history 与 xyzbc 一致。
|
||||
```
|
||||
|
||||
### 步骤 6:验证
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
```
|
||||
|
||||
通过标准:
|
||||
|
||||
```text
|
||||
所有命令返回 0。
|
||||
新增断言覆盖 ABORT、STEP、RUN gate、INI cycle period、xyzbc RUN symmetry。
|
||||
```
|
||||
|
||||
## 3. 完成记录模板
|
||||
|
||||
执行完成后,追加记录:
|
||||
|
||||
```text
|
||||
1. 修改文件。
|
||||
2. 新增断言。
|
||||
3. 验证命令与结果。
|
||||
4. 未完成项和下一步。
|
||||
```
|
||||
|
||||
## 4. 本轮完成记录
|
||||
|
||||
完成时间:2026-06-22 17:45 EDT
|
||||
|
||||
修改文件:
|
||||
|
||||
```text
|
||||
app/src/state/store.js
|
||||
tests/node/verify_run_feedback_loop.mjs
|
||||
tests/node/verify_run_preconditions.mjs
|
||||
working_run/README.md
|
||||
working_run/10-run-control-tests-and-ini-cycle-steps.md
|
||||
```
|
||||
|
||||
完成内容:
|
||||
|
||||
```text
|
||||
1. runTaskHalCommandSequence 默认周期改为从 INI 派生:
|
||||
TASK.CYCLE_TIME -> taskPeriodNs。
|
||||
EMCMOT.SERVO_PERIOD -> servoPeriodNs。
|
||||
2. startTaskHalStatusLoop 默认周期改为从同一个 INI 派生。
|
||||
3. verify_run_feedback_loop.mjs 增加 STOP、ABORT、STEP、xyzbc-trt 对称 RUN 覆盖。
|
||||
4. verify_run_feedback_loop.mjs 断言 taskHalStatusLoop.taskPeriodNs=10000000、
|
||||
servoPeriodNs=1000000,且 feedback sourceMode 不回退 fixture-line-playback。
|
||||
5. verify_run_preconditions.mjs 增加 RUN gate 细粒度断言:
|
||||
machine off、not homed、manual mode、未打开 selected G-code。
|
||||
6. working_run/README.md 增加 10 文档索引,并把下一步改为米尺度浏览器截图证据。
|
||||
```
|
||||
|
||||
验证结果:
|
||||
|
||||
```bash
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
全部通过。
|
||||
run_preconditions_ini_profile_smoke=ok
|
||||
run_feedback_status_loop_smoke=ok
|
||||
linuxcnc_task_hal_runtime_smoke=ok
|
||||
gmoccapy_static_build=ok
|
||||
smoke:node 全部通过。
|
||||
```
|
||||
|
||||
剩余项:
|
||||
|
||||
```text
|
||||
1. 增加浏览器 desktop/mobile 米尺度截图证据。
|
||||
2. 后续可补 G20/G21 中途切换浏览器端预览用例。
|
||||
```
|
||||
@@ -0,0 +1,105 @@
|
||||
# 11 浏览器米尺度场景与 G20/G21 混合单位证据完成记录
|
||||
|
||||
生成时间:2026-06-22 17:55 EDT
|
||||
|
||||
## 1. 本轮目标
|
||||
|
||||
按 `09-linear-units-meter-scene-complete-next-steps.md` 的剩余项继续执行,补齐:
|
||||
|
||||
```text
|
||||
1. 浏览器 desktop/mobile 米尺度 Three.js 截图证据。
|
||||
2. G20/G21 混合单位浏览器端预览用例。
|
||||
3. 对应 JSON 证据和截图归档。
|
||||
```
|
||||
|
||||
## 2. 修改文件
|
||||
|
||||
```text
|
||||
app/src/visualization/five-axis-scene.js
|
||||
qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
working_run/README.md
|
||||
working_run/11-browser-meter-scene-and-mixed-units-complete.md
|
||||
gptlog-process/gpdlog.md
|
||||
```
|
||||
|
||||
## 3. 完成内容
|
||||
|
||||
```text
|
||||
1. Three.js canvas dataset 新增 threePathBoundsMeters。
|
||||
2. browser 证据脚本新增 08-meter-scene-desktop-mobile。
|
||||
3. desktop/mobile 分别采集 canvas screenshot、pixelStats、dataset。
|
||||
4. 断言 sceneUnits=m、linearUnitScaleToMeters 有效、pathFitBounds=ok、bounds maxSpan 为米尺度且 canvas 非空。
|
||||
5. browser 证据脚本新增 09-g20-g21-mixed-units-preview。
|
||||
6. 加载本地 G-code:
|
||||
G90 G20
|
||||
G1 X1.0 Y0 Z0 F10
|
||||
G21
|
||||
G1 X25.4 Y25.4 Z0 F254
|
||||
M2
|
||||
7. 断言 canonical motion 同时保留 inch/mm,并且 1 inch 与 25.4 mm 都映射到 0.0254m 场景坐标。
|
||||
8. RUN case 前恢复 vendored impeller 程序,避免本地混合单位程序污染 RUN 证据上下文。
|
||||
```
|
||||
|
||||
## 4. 新增输出
|
||||
|
||||
```text
|
||||
qa/web-rtcp-5axis-site-test/output/meter-scene-evidence.json
|
||||
qa/web-rtcp-5axis-site-test/screenshots/meter-scene-evidence/08-meter-scene-desktop.png
|
||||
qa/web-rtcp-5axis-site-test/screenshots/meter-scene-evidence/08-meter-scene-mobile.png
|
||||
qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/09-g20-g21-mixed-units-preview.png
|
||||
```
|
||||
|
||||
`toolpath-preview-cases.json` 当前新增 case:
|
||||
|
||||
```text
|
||||
08-meter-scene-desktop-mobile: PASS
|
||||
09-g20-g21-mixed-units-preview: PASS
|
||||
```
|
||||
|
||||
## 5. 验证结果
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linear_unit_conversion.mjs
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
node qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
linear_unit_conversion_smoke=ok
|
||||
gmoccapy_static_build=ok
|
||||
browser evidence: all recorded cases PASS
|
||||
smoke:node: all checks PASS
|
||||
```
|
||||
|
||||
浏览器 JSON 摘要:
|
||||
|
||||
```text
|
||||
meter-scene-evidence.json status=PASS
|
||||
run-preconditions-feedback.json status=PASS
|
||||
toolpath-preview-cases:
|
||||
01-home-toolpath PASS
|
||||
02-operator-demo-toolpath PASS
|
||||
03-arc-demo-toolpath PASS
|
||||
04-clear-preview-reference PASS
|
||||
05-vendored-impeller-toolpath PASS
|
||||
08-meter-scene-desktop-mobile PASS
|
||||
09-g20-g21-mixed-units-preview PASS
|
||||
06-running-rtcp-toolpath PASS
|
||||
07-run-preconditions-and-feedback PASS
|
||||
```
|
||||
|
||||
## 6. 当前结论
|
||||
|
||||
`working_run` 中列出的 RUN 主链路、控制类测试、INI 周期派生、xyzbc 对称测试、浏览器 RUN feedback 证据、Three.js 米尺度场景证据、G20/G21 混合单位预览证据均已完成并可复现。
|
||||
|
||||
后续可选增强:
|
||||
|
||||
```text
|
||||
1. 将浏览器证据脚本拆分为独立小脚本,降低单次运行时间。
|
||||
2. 补更多真实 LinuxCNC native 对照资产。
|
||||
```
|
||||
@@ -0,0 +1,111 @@
|
||||
# 12 working_run 剩余工作最终完成记录
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 执行范围
|
||||
|
||||
按 `working_run` 当前执行情况复核 `01` 到 `11` 的接续项,并完成最终收尾确认。
|
||||
|
||||
本轮重点核对:
|
||||
|
||||
```text
|
||||
1. RUN 主链路是否仍可复现。
|
||||
2. RUN preconditions、status loop、ABORT/STEP、xyzbc 对称测试是否通过。
|
||||
3. INI CYCLE_TIME / SERVO_PERIOD 派生周期是否仍通过测试。
|
||||
4. Three.js 米单位场景、G20/G21 混合单位浏览器证据是否仍通过。
|
||||
5. browser evidence JSON 是否全部 PASS。
|
||||
```
|
||||
|
||||
## 2. 当前结论
|
||||
|
||||
`working_run` 中早期文档保留的“下一步/未完成”项已经由后续文档覆盖:
|
||||
|
||||
```text
|
||||
06 中的浏览器 RUN feedback 证据 -> 07 已完成。
|
||||
07 中的 ABORT/STEP/RUN gate/xyzbc 对称测试 -> 10 已完成。
|
||||
09/10 中的 desktop/mobile 米尺度截图与 G20/G21 混合单位浏览器用例 -> 11 已完成。
|
||||
```
|
||||
|
||||
因此,按 `working_run` 已列出的必做项,当前剩余工作已全部完成并复核通过。
|
||||
|
||||
只保留可选增强:
|
||||
|
||||
```text
|
||||
1. 将浏览器证据脚本拆成多个独立小脚本,降低单次运行时间。
|
||||
2. 后续补更多真实 LinuxCNC native 对照资产。
|
||||
3. 如果接入真实 task/motion HAL feedback,再重新确认 feedback 坐标单位语义。
|
||||
```
|
||||
|
||||
## 3. 本轮复核命令
|
||||
|
||||
已执行:
|
||||
|
||||
```bash
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linear_unit_conversion.mjs
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
node qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
run_preconditions_ini_profile_smoke=ok
|
||||
run_preconditions_kinematics_smoke=ok
|
||||
run_preconditions_machine_file_smoke=ok
|
||||
run_feedback_status_loop_smoke=ok
|
||||
linear_unit_conversion_smoke=ok
|
||||
gmoccapy_static_build=ok
|
||||
smoke:node 全部通过
|
||||
browser evidence 脚本返回 0
|
||||
```
|
||||
|
||||
## 4. 浏览器证据状态
|
||||
|
||||
`qa/web-rtcp-5axis-site-test/output/toolpath-preview-cases.json`:
|
||||
|
||||
```text
|
||||
01-home-toolpath: PASS
|
||||
02-operator-demo-toolpath: PASS
|
||||
03-arc-demo-toolpath: PASS
|
||||
04-clear-preview-reference: PASS
|
||||
05-vendored-impeller-toolpath: PASS
|
||||
08-meter-scene-desktop-mobile: PASS
|
||||
09-g20-g21-mixed-units-preview: PASS
|
||||
06-running-rtcp-toolpath: PASS
|
||||
07-run-preconditions-and-feedback: PASS
|
||||
```
|
||||
|
||||
其他证据:
|
||||
|
||||
```text
|
||||
qa/web-rtcp-5axis-site-test/output/run-preconditions-feedback.json: PASS
|
||||
qa/web-rtcp-5axis-site-test/output/meter-scene-evidence.json: PASS
|
||||
```
|
||||
|
||||
## 5. 最终状态
|
||||
|
||||
当前已完成:
|
||||
|
||||
```text
|
||||
INI definition contract validation
|
||||
profile/INI/kinematics 一致性验证
|
||||
machine-file staging
|
||||
task/HAL session openProgram
|
||||
RUN gate
|
||||
RUN command sequence
|
||||
连续 taskHalStatusLoop
|
||||
TASK_HAL_STATUS_APPLIED runtime feedback 消费
|
||||
STOP/ABORT/STEP 控制类测试
|
||||
INI 周期派生
|
||||
xyzac/xyzbc RUN status loop 对称覆盖
|
||||
LinuxCNC 源程序真实五轴资产覆盖
|
||||
mm/inch/m 到 Three.js 米单位场景转换
|
||||
desktop/mobile 米尺度浏览器截图证据
|
||||
G20/G21 混合单位浏览器预览证据
|
||||
完整 node smoke
|
||||
浏览器 evidence JSON/截图
|
||||
```
|
||||
|
||||
@@ -9,8 +9,17 @@
|
||||
| 文件 | 用途 |
|
||||
| --- | --- |
|
||||
| `01-run-gcode-execution-principle-and-test.md` | G-code 具体执行过程、`RUN` 执行原理、源码证据、测试观察和问题结论 |
|
||||
| `02-run-preconditions-test-and-implementation-steps.md` | `RUN` 前置条件、INI/机床/五轴运动学确认流程、详细测试用例和编写 `RUN` 程序步骤 |
|
||||
| `02-run-preconditions-test-and-implementation-steps.md` | `RUN` 前置条件、INI definition contract、LinuxCNC 源码对照、INI/机床/五轴运动学确认流程、详细测试用例和编写 `RUN` 程序步骤 |
|
||||
| `03-run-step1-preconditions-complete-next-steps.md` | 本轮已完成的 `RUN` 前置条件 gate、验证结果和下一步接续建议 |
|
||||
| `04-run-status-loop-complete-next-steps.md` | 本轮已完成的 `RUN` 连续 task/HAL status loop、验证结果和下一步接续建议 |
|
||||
| `05-run-program-implementation-detailed-steps.md` | 根据 01-04 文档整理的程序实现详细实施步骤、代码落点、测试与验收顺序 |
|
||||
| `06-run-implementation-traceability-matrix-and-next-steps.md` | 按 05 核对后的完成情况矩阵、本轮补强项、验证结果和后续接续工作 |
|
||||
| `07-browser-run-feedback-evidence-complete-next-steps.md` | 浏览器端 RUN feedback JSON/截图证据完成记录和后续控制类测试建议 |
|
||||
| `08-linuxcnc-source-test-assets.md` | 从 LinuxCNC 源程序选定并复制到 working_run 的真实 INI/G-code 测试资产说明 |
|
||||
| `09-linear-units-meter-scene-complete-next-steps.md` | 英寸/毫米/米单位换算、Three.js 米单位场景、执行轨迹单位语义完成记录和后续建议 |
|
||||
| `10-run-control-tests-and-ini-cycle-steps.md` | RUN 控制类测试、INI 周期派生、ABORT/STEP/RUN gate/xyzbc 对称测试实施步骤与完成记录 |
|
||||
| `11-browser-meter-scene-and-mixed-units-complete.md` | 浏览器 desktop/mobile 米尺度截图证据、G20/G21 混合单位预览用例与完成记录 |
|
||||
| `12-working-run-final-completion-record.md` | 按 `working_run` 接续项最终复核后的剩余工作完成记录 |
|
||||
|
||||
## 结论摘要
|
||||
|
||||
@@ -18,10 +27,13 @@
|
||||
|
||||
```text
|
||||
1. 先确定明确的 LinuxCNC INI 文件。
|
||||
2. 由 INI/profile 确定机床类型、坐标轴、joint 配置、HAL/remap/tool-table 资产。
|
||||
3. 由 INI 的 [KINS] KINEMATICS 确定五轴运动学算法和 switchkins 映射。
|
||||
4. 验证对应 LinuxCNC kinematics WASM 已加载,并能产生 source-derived frame。
|
||||
5. 之后才进入 RUN:打开同一 INI 上下文下的 G-code,发送 task command,持续消费 task/HAL/motion feedback。
|
||||
2. 验证 INI definition contract:[EMC]、[DISPLAY]、[RS274NGC]、[KINS]、[HAL]、[TRAJ]、[EMCMOT]、[TASK]、[EMCIO]、[AXIS_*]、[JOINT_*] 必须完整且互相一致。
|
||||
3. 由 INI/profile 确定机床类型、坐标轴、joint 配置、HAL/remap/tool-table 资产。
|
||||
4. 由 INI 的 [KINS] KINEMATICS 确定五轴运动学算法和 switchkins 映射。
|
||||
5. 验证对应 LinuxCNC kinematics WASM 已加载,并能产生 source-derived frame。
|
||||
6. 验证同一 INI 上下文下的 G-code 已通过 task plan open 打开。
|
||||
7. 确认 machine ON、AUTO mode、homed/no_force_homing 等 LinuxCNC task gate。
|
||||
8. 之后才进入 RUN:发送 task command,持续消费 task/HAL/motion feedback。
|
||||
```
|
||||
|
||||
当前项目明确支持的五轴机床上下文是:
|
||||
@@ -33,22 +45,28 @@
|
||||
|
||||
当前实现已经能显示 G-code 程序文本、当前行、高亮行、DRO、runtime feedback 和 task/HAL 状态。
|
||||
|
||||
但按 `textbak` 接续文件的边界要求,当前 `RUN` 还不是连续的 task/HAL feedback 推送执行模型:
|
||||
`04` 完成后,当前 `RUN` 已从单次 task/HAL status 读取推进为连续 status loop:
|
||||
|
||||
```text
|
||||
RUN 点击后:
|
||||
1. UI 发送 EMC_TASK_SET_STATE / EMC_TASK_SET_MODE / EMC_TASK_PLAN_RUN;
|
||||
2. store 调用 taskHalRuntime.runCycles({ taskCycles: 5 });
|
||||
3. 随后 readStatus() 一次;
|
||||
4. TASK_HAL_STATUS_APPLIED 将这一次 status 映射到 activeLine / axisPose / velocity / feedback;
|
||||
5. 没有看到 worker 主动持续 postMessage 推送 status;
|
||||
6. 如果没有再次调用 runCycles/readStatus,状态不会继续随真实时间推进。
|
||||
1. UI 确认 opened program、machine ON、AUTO、homed 后发送 EMC_TASK_SET_STATE / EMC_TASK_SET_MODE / EMC_TASK_PLAN_RUN;
|
||||
2. store 调用 taskHalRuntime.runCycles(...);
|
||||
3. store 调用 taskHalRuntime.readStatus();
|
||||
4. TASK_HAL_STATUS_APPLIED 将 task/HAL/motion status 映射到 activeLine / axisPose / velocity / feedback;
|
||||
5. taskHalStatusLoop 持续重复 runCycles/readStatus;
|
||||
6. programRuntimeFeedbackHistory 保留最近 100 条 task/HAL/motion feedback。
|
||||
```
|
||||
|
||||
因此,当前问题重点不是“前端是否需要定时器”,而是:
|
||||
该顺序与 LinuxCNC 源码中的 `halui.cc::sendProgramRun()`、`shcom.cc::sendProgramOpen/sendProgramRun()`、`emctaskmain.cc::EMC_TASK_PLAN_OPEN/RUN/PAUSE/RESUME` 对齐:RUN 不是前端播放,而是在已打开程序和可运行 task 状态下发送 `EMC_TASK_PLAN_RUN`,后续状态来自 task/motion/HAL feedback。
|
||||
|
||||
当前仍需保持的边界是:
|
||||
|
||||
```text
|
||||
task/HAL runtime 是否能在 G-code 执行期间持续产出状态;
|
||||
UI/store 是否持续消费这些 LinuxCNC-owned 状态;
|
||||
轴值、当前行、速度、DTG、程序时间是否都来自同一条 task/HAL/motion feedback 链。
|
||||
taskHalStatusLoop 只负责调度 runCycles/readStatus;
|
||||
status 内容必须来自 task/HAL/motion runtime;
|
||||
UI/store 不自增 activeLine;
|
||||
UI/store 不自造 axisPose;
|
||||
浏览器端米尺度 desktop/mobile 截图证据已补齐;
|
||||
G20/G21 混合单位浏览器预览证据已补齐。
|
||||
working_run 已列出的必做接续项已全部复核通过,仅保留可选增强。
|
||||
```
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
[APPLICATIONS]
|
||||
# uncomment to enable:
|
||||
#APP = halshow --fformat %.5f switchkins.halshow
|
||||
|
||||
[EMC]
|
||||
VERSION = 1.1
|
||||
MACHINE = sim-xyzac-trt-kins (switchkins)
|
||||
|
||||
[DISPLAY]
|
||||
GEOMETRY = XYZ-A
|
||||
OPEN_FILE = ./demos/xyzac_switchkins.ngc
|
||||
PYVCP = ./xyzac-trt.xml
|
||||
JOG_AXES = XYZC
|
||||
DISPLAY = axis
|
||||
MAX_ANGULAR_VELOCITY = 360
|
||||
MAX_LINEAR_VELOCITY = 1000
|
||||
POSITION_OFFSET = RELATIVE
|
||||
POSITION_FEEDBACK = ACTUAL
|
||||
MAX_FEED_OVERRIDE = 2
|
||||
PROGRAM_PREFIX = ../../nc_files
|
||||
INTRO_GRAPHIC = emc2.gif
|
||||
INTRO_TIME = 1
|
||||
#EDITOR = geany
|
||||
TOOL_EDITOR = tooledit z diam
|
||||
|
||||
TKPKG = Ngcgui 1.0
|
||||
NGCGUI_FONT = Helvetica -12 normal
|
||||
NGCGUI_SUBFILE = xyzac_switchkins_sub.ngc
|
||||
NGCGUI_SUBFILE = centering.ngc
|
||||
|
||||
[RS274NGC]
|
||||
SUBROUTINE_PATH = ./remap_subs
|
||||
HAL_PIN_VARS = 1
|
||||
REMAP = M428 modalgroup=10 ngc=428remap
|
||||
REMAP = M429 modalgroup=10 ngc=429remap
|
||||
REMAP = M430 modalgroup=10 ngc=430remap
|
||||
PARAMETER_FILE = xyzac.var
|
||||
|
||||
[KINS]
|
||||
#NOTE: for backwrds compatibility !!!!!!!!!!!!!!!!!!!
|
||||
# default switchkins-type == 0 is xyzac-trt-kins
|
||||
# here switchkins-type == 0 is identity kins
|
||||
KINEMATICS = xyzac-trt-kins sparm=identityfirst
|
||||
JOINTS = 5
|
||||
|
||||
[HAL]
|
||||
HALUI = halui
|
||||
HALFILE = LIB:basic_sim.tcl
|
||||
POSTGUI_HALFILE = switchkins_postgui.hal
|
||||
|
||||
# net for control of motion.switchkins-type
|
||||
HALCMD = net :kinstype-select <= motion.analog-out-03 => motion.switchkins-type
|
||||
|
||||
# vismach xyzac-trt-gui items
|
||||
HALCMD = loadusr -W xyzac-trt-gui
|
||||
HALCMD = net :table-x joint.0.pos-fb xyzac-trt-gui.table-x
|
||||
HALCMD = net :saddle-y joint.1.pos-fb xyzac-trt-gui.saddle-y
|
||||
HALCMD = net :spindle-z joint.2.pos-fb xyzac-trt-gui.spindle-z
|
||||
HALCMD = net :tilt-a joint.3.pos-fb xyzac-trt-gui.tilt-a
|
||||
HALCMD = net :rotate-c joint.4.pos-fb xyzac-trt-gui.rotate-c
|
||||
HALCMD = net :tool-offset motion.tooloffset.z
|
||||
HALCMD = net :tool-offset xyzac-trt-kins.tool-offset xyzac-trt-gui.tool-offset
|
||||
HALCMD = net :y-offset xyzac-trt-kins.y-offset xyzac-trt-gui.y-offset
|
||||
HALCMD = net :z-offset xyzac-trt-kins.z-offset xyzac-trt-gui.z-offset
|
||||
HALCMD = sets :y-offset 20
|
||||
HALCMD = sets :z-offset 10
|
||||
|
||||
# not currently supported by xyzac-trt-gui:
|
||||
HALCMD = setp xyzac-trt-kins.x-rot-point 0
|
||||
HALCMD = setp xyzac-trt-kins.y-rot-point 0
|
||||
HALCMD = setp xyzac-trt-kins.z-rot-point 0
|
||||
HALCMD = setp xyzac-trt-kins.conventional-directions 0
|
||||
|
||||
[HALUI]
|
||||
# NOTE: kinstype==0 is identity kins because sparm=identityfirst
|
||||
# M429:identity kins (motion.switchkins-type==0 startupDEFAULT)
|
||||
# M428:xyzac kins (motion.switchkins-type==1)
|
||||
# M430:userk kins (motion.switchkins-type==2)
|
||||
MDI_COMMAND = M429
|
||||
MDI_COMMAND = M428
|
||||
MDI_COMMAND = M430
|
||||
|
||||
[TRAJ]
|
||||
COORDINATES = XYZAC
|
||||
LINEAR_UNITS = mm
|
||||
ANGULAR_UNITS = deg
|
||||
DEFAULT_LINEAR_VELOCITY = 20
|
||||
MAX_LINEAR_VELOCITY = 35
|
||||
MAX_LINEAR_ACCELERATION = 400
|
||||
DEFAULT_LINEAR_ACCELERATION = 300
|
||||
|
||||
[EMCMOT]
|
||||
EMCMOT = motmod
|
||||
SERVO_PERIOD = 1000000
|
||||
COMM_TIMEOUT = 1
|
||||
|
||||
[TASK]
|
||||
TASK = milltask
|
||||
CYCLE_TIME = 0.010
|
||||
|
||||
[EMCIO]
|
||||
TOOL_TABLE = xyzac-trt.tbl
|
||||
|
||||
[AXIS_X]
|
||||
MIN_LIMIT = -200
|
||||
MAX_LIMIT = 200
|
||||
MAX_VELOCITY = 20
|
||||
MAX_ACCELERATION = 300
|
||||
|
||||
[AXIS_Y]
|
||||
MIN_LIMIT = -100
|
||||
MAX_LIMIT = 100
|
||||
MAX_VELOCITY = 20
|
||||
MAX_ACCELERATION = 300
|
||||
|
||||
[AXIS_Z]
|
||||
MIN_LIMIT = -120
|
||||
MAX_LIMIT = 120
|
||||
MAX_VELOCITY = 20
|
||||
MAX_ACCELERATION = 300
|
||||
|
||||
[AXIS_A]
|
||||
MIN_LIMIT = -100
|
||||
MAX_LIMIT = 50
|
||||
MAX_VELOCITY = 30
|
||||
MAX_ACCELERATION = 300
|
||||
|
||||
[AXIS_C]
|
||||
MIN_LIMIT = -36000
|
||||
MAX_LIMIT = 36000
|
||||
MAX_VELOCITY = 30
|
||||
MAX_ACCELERATION = 300
|
||||
|
||||
[JOINT_0]
|
||||
TYPE = LINEAR
|
||||
HOME = 0
|
||||
MAX_VELOCITY = 20
|
||||
MAX_ACCELERATION = 300
|
||||
MIN_LIMIT = -200
|
||||
MAX_LIMIT = 200
|
||||
HOME_SEARCH_VEL = 0
|
||||
HOME_SEQUENCE = 0
|
||||
|
||||
[JOINT_1]
|
||||
TYPE = LINEAR
|
||||
HOME = 0
|
||||
MAX_VELOCITY = 20
|
||||
MAX_ACCELERATION = 300
|
||||
MIN_LIMIT = -100
|
||||
MAX_LIMIT = 100
|
||||
HOME_SEARCH_VEL = 0
|
||||
HOME_SEQUENCE = 0
|
||||
|
||||
[JOINT_2]
|
||||
TYPE = LINEAR
|
||||
HOME = 0
|
||||
MAX_VELOCITY = 20
|
||||
MAX_ACCELERATION = 300
|
||||
MIN_LIMIT = -120
|
||||
MAX_LIMIT = 120
|
||||
HOME_SEARCH_VEL = 0
|
||||
HOME_SEQUENCE = 0
|
||||
|
||||
[JOINT_3]
|
||||
TYPE = ANGULAR
|
||||
HOME = 0
|
||||
MAX_VELOCITY = 30
|
||||
MAX_ACCELERATION = 300
|
||||
MIN_LIMIT = -100
|
||||
MAX_LIMIT = 50
|
||||
HOME_SEARCH_VEL = 0
|
||||
HOME_SEQUENCE = 0
|
||||
|
||||
[JOINT_4]
|
||||
TYPE = ANGULAR
|
||||
HOME = 0
|
||||
MAX_VELOCITY = 30
|
||||
MAX_ACCELERATION = 300
|
||||
MIN_LIMIT = -36000
|
||||
MAX_LIMIT = 36000
|
||||
HOME_SEARCH_VEL = 0
|
||||
HOME_SEQUENCE = 0
|
||||