完善五轴 RTCP 仿真与验证资料

This commit is contained in:
2026-07-01 21:49:58 -04:00
parent ac4e855b2b
commit d0d58998ac
159 changed files with 6771594 additions and 339 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,550 @@
import fs from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import { randomUUID } from "node:crypto";
import puppeteer from "puppeteer-core";
import { PNG } from "pngjs";
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 EVIDENCE_SCOPE = "working7-manual-flow-evidence";
const SCREENSHOT_DIR = path.join(OUTPUT_DIR, EVIDENCE_SCOPE);
const CHROME_PATH = process.env.CHROME_PATH || process.env.CHROMIUM || "/usr/bin/google-chrome";
const TARGET_URL = process.env.TARGET_URL || "";
const APP_URL = process.env.APP_URL || "/web-rtcp-5axis-sim-plan/app/index.html";
const JOB_ID = process.env.JOB_ID || `w7-${new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}-${randomUUID().slice(0, 8)}`;
const REPORT_ID = process.env.REPORT_ID || `report-${JOB_ID}`;
const REPORT_BASENAME = `${EVIDENCE_SCOPE}-report`;
await fs.mkdir(OUTPUT_DIR, { recursive: true });
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
let server = null;
let targetUrl = TARGET_URL;
if (!targetUrl) {
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");
targetUrl = `http://127.0.0.1:${address.port}${APP_URL}`;
}
const browser = await puppeteer.launch({
headless: true,
executablePath: CHROME_PATH,
defaultViewport: { width: 1500, height: 1050, deviceScaleFactor: 1 },
ignoreHTTPSErrors: true,
args: [
"--ignore-certificate-errors",
"--disable-gpu",
"--enable-webgl",
"--use-angle=swiftshader",
"--enable-unsafe-swiftshader",
"--no-sandbox",
],
});
const page = await browser.newPage();
const consoleErrors = [];
const pageErrors = [];
const networkErrors = [];
page.on("console", (msg) => {
if (msg.type() === "error") consoleErrors.push(msg.text());
});
page.on("pageerror", (error) => pageErrors.push(error.message));
page.on("response", (response) => {
if (response.status() >= 400) {
networkErrors.push(`${response.status()} ${response.url()}`);
}
});
const report = {
jobId: JOB_ID,
reportId: REPORT_ID,
evidenceScope: EVIDENCE_SCOPE,
generatedAt: new Date().toISOString(),
targetUrl,
chromePath: CHROME_PATH,
screenshotsDir: SCREENSHOT_DIR,
steps: [],
checks: [],
consoleErrors,
pageErrors,
networkErrors,
};
try {
await page.goto(targetUrl, { waitUntil: "networkidle2", timeout: 60000 });
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 });
await page.waitForFunction(() => document.querySelector("[data-five-axis-canvas]")?.dataset?.threeReady === "true", { timeout: 20000 });
await windowReady();
await captureStep("01-loaded", "Main shell loaded", "The gmoccapy shell, canvas, diagnostics, and public store API are ready.");
await click("power");
await waitForState((state) => state.machine.powerOn === true && state.machine.taskState === "on", 10000, "power on");
await click("HOME");
await waitForState((state) => state.machine.allHomed === true && state.machine.mode === "manual", 10000, "home complete");
await captureStep("02-powered-homed-manual", "POWER and HOME", "Machine is powered, homed, and in MANUAL mode.");
await click("mode-auto");
await waitForState((state) => state.machine.mode === "auto" && state.machine.powerOn === true && state.machine.allHomed === true, 10000, "auto mode");
await captureStep("03-auto-active", "AUTO mode active", "AUTO mode becomes active while power and home state are preserved.");
await click("mode-manual");
await waitForState((state) => state.machine.mode === "manual" && state.machine.powerOn === true, 10000, "manual mode");
const beforeJog = await getState();
await click("JOG_X_POS");
await waitForState((state) => Number(state.axisPose?.x || 0) > Number(beforeJog.axisPose?.x || 0), 10000, "X+ jog changes position");
await captureStep("04-manual-jog-x", "Manual X+ jog", "X+ jog changes the X axis position in MANUAL mode.");
await click("mode-mdi");
await waitForState((state) => state.machine.mode === "mdi", 10000, "mdi mode");
await setMdiCommand("G90 X12.5 Y-4 Z1.25 F900");
await submitMdiCommand();
await waitForState((state) => (
state.machine.mode === "mdi" &&
/G90 X12\.5 Y-4 Z1\.25 F900/.test(state.machine.mdiCommand || state.operatorMessage || "")
), 10000, "MDI command accepted");
await captureStep("05-mdi-command", "MDI command", "MDI accepts a coordinate command and records the staged/executed command state.");
await click("mode-manual");
await waitForState((state) => state.machine.mode === "manual", 10000, "manual before overrides");
await click("rapid-override-up");
await waitForState((state) => Number(state.feed.rapidOverride) === 110, 8000, "rapid override up");
await click("rapid-override-reset");
await waitForState((state) => Number(state.feed.rapidOverride) === 100, 8000, "rapid override reset");
await click("feed-override-down");
await waitForState((state) => Number(state.feed.feedOverride) === 90, 8000, "feed override down");
await click("feed-override-reset");
await waitForState((state) => Number(state.feed.feedOverride) === 100, 8000, "feed override reset");
await click("ignore-limits");
await waitForState((state) => state.gmoccapyGui.ignoreLimits === true, 8000, "ignore limits on");
await click("block-delete");
await waitForState((state) => state.gmoccapyGui.optionalBlocks === true, 8000, "block delete on");
await click("optional-stop");
await waitForState((state) => state.gmoccapyGui.optionalStop === true, 8000, "optional stop on");
await captureStep("06-overrides-hal", "Overrides and HAL inputs", "Rapid/feed overrides reset to 100 and HAL input toggles are active.");
await click("spindle-forward");
await waitForState((state) => state.spindle.enabled === true && state.spindle.direction === "forward", 8000, "spindle forward");
await click("spindle-override-up");
await waitForState((state) => Number(state.spindle.override) === 110, 8000, "spindle override up");
await click("spindle-override-reset");
await waitForState((state) => Number(state.spindle.override) === 100, 8000, "spindle override reset");
await click("toggle-flood");
await waitForState((state) => state.coolant.flood === true, 8000, "flood toggled on");
await click("toggle-mist");
await waitForState((state) => state.coolant.mist === true, 8000, "mist toggled on");
await click("spindle-stop");
await waitForState((state) => state.spindle.enabled === false && state.spindle.direction === "stop", 8000, "spindle stop");
await captureStep("07-spindle-coolant", "Spindle and coolant", "Spindle, spindle override, flood, and mist controls obey the powered machine gate.");
const savedSnapshot = await page.evaluate(() => window.webRtcp5AxisSimulation.saveSession({
sessionId: "working7-manual-flow",
filename: "working7-session.json",
}));
await waitForState((state) => state.sessionPersistence.status === "saved", 10000, "session saved");
await page.select('[data-action="select-profile"]', "gmoccapy-xyzab");
await waitForState((state) => state.machineProfile === "gmoccapy-xyzab", 15000, "profile changed before restore");
await page.evaluate(() => window.webRtcp5AxisSimulation.restoreSession({
sessionId: "working7-manual-flow",
filename: "working7-session.json",
}));
await waitForState((state) => state.sessionPersistence.status === "restored" && state.machineProfile === "xyzac-trt", 15000, "session restored");
const restoredSession = await getState();
report.sessionEvidence = {
savedPath: savedSnapshot.path,
savedStorageMode: savedSnapshot.storageMode,
restoredPath: restoredSession.sessionPersistence.path,
restoredStorageMode: restoredSession.sessionPersistence.storageMode,
};
await captureStep("08-session-restored", "Save and restore session", "Saved profile and control state are restored after switching to another profile.");
await captureStep("09-diagnostics", "Diagnostics", "Task policy, INI, Task/HAL, full boundary, gmoccapy communication, and HAL diagnostics are visible.");
addChecks();
report.status = report.checks.every((check) => check.pass) && pageErrors.length === 0 ? "PASS" : "FAIL";
const jsonPath = path.join(OUTPUT_DIR, `${REPORT_BASENAME}.json`);
const pdfPath = path.join(OUTPUT_DIR, `${REPORT_BASENAME}.pdf`);
report.jsonPath = jsonPath;
report.pdfPath = pdfPath;
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
await writePdfReport(pdfPath, report);
console.log(`working7_manual_flow_status=${report.status}`);
console.log(`working7_manual_flow_job_id=${report.jobId}`);
console.log(`working7_manual_flow_report_id=${report.reportId}`);
console.log(`working7_manual_flow_json=${jsonPath}`);
console.log(`working7_manual_flow_pdf=${pdfPath}`);
console.log(`working7_manual_flow_screenshots=${SCREENSHOT_DIR}`);
if (report.status !== "PASS") process.exitCode = 1;
} finally {
await page.close().catch(() => {});
await browser.close().catch(() => {});
if (server) await new Promise((resolve) => server.close(resolve));
}
async function windowReady() {
await waitForState((state) => (
state.kinematicsRuntimeReadiness?.loaded === true &&
state.interpreterRuntimeReadiness?.loaded === true &&
state.taskHalRuntimeReadiness?.loaded === true &&
state.machineFileStaging?.status === "staged"
), 30000, "runtime and machine files ready");
}
async function captureStep(name, title, description) {
const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
const [state, dom, pixelStats] = await Promise.all([
getState(),
getDomEvidence(),
analyzePng(screenshotPath),
]);
const step = {
name,
title,
description,
screenshotPath,
pixelStats,
state: summarizeState(state),
dom,
};
report.steps.push(step);
return step;
}
async function getDomEvidence() {
return page.evaluate(() => {
const text = (selector) => document.querySelector(selector)?.textContent?.trim() || "";
const button = (action) => {
const element = document.querySelector(`[data-action="${action}"]`);
return {
exists: Boolean(element),
disabled: Boolean(element?.disabled),
active: element?.dataset?.active || null,
commandReady: element?.dataset?.commandReady || null,
title: element?.getAttribute("title") || "",
};
};
return {
regions: window.webRtcp5AxisSimulation.getRegions?.() || null,
canvas: { ...(document.querySelector("[data-five-axis-canvas]")?.dataset || {}) },
sidebar: {
power: button("power"),
manual: button("mode-manual"),
auto: button("mode-auto"),
mdi: button("mode-mdi"),
},
bottom: {
home: button("HOME"),
jogXPlus: button("JOG_X_POS"),
mdiRun: button("MDI_RUN"),
},
values: {
rapidOverride: text('[data-value="rapid-override"]'),
feedOverride: text('[data-value="feed-override"]'),
spindleOverride: text('[data-value="spindle-override"]'),
halLast: text('[data-value="gmoccapy-hal-last"]'),
session: text('[data-session-persistence="status"]'),
taskGates: text('[data-linuxcnc-task-policy="gates"]'),
taskHal: text('[data-task-hal-runtime="readiness"]'),
gmoccapyHal: text('[data-gmoccapy-hal="boundary"]'),
operatorMessage: text("[data-operator-message]"),
},
};
});
}
async function setMdiCommand(command) {
await page.$eval('[data-action="mdi-command"]', (input, value) => {
input.value = value;
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
}, command);
await waitForState((state) => state.machine.mdiCommand === command.toUpperCase(), 5000, "MDI input staged");
}
async function submitMdiCommand() {
await page.$eval('[data-action="mdi-form"]', (form) => {
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
});
}
async function click(action) {
await page.evaluate((selector) => {
const element = document.querySelector(selector);
if (!element) throw new Error(`missing action ${selector}`);
element.click();
}, `[data-action="${action}"]`);
}
async function getState() {
return page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
}
async function waitForState(predicate, timeoutMs, label) {
const started = Date.now();
let lastState = null;
while (Date.now() - started < timeoutMs) {
lastState = await getState();
if (predicate(lastState)) return lastState;
await wait(50);
}
throw new Error(`timeout waiting for ${label}: ${JSON.stringify(summarizeState(lastState || {}))}`);
}
function addChecks() {
const byName = Object.fromEntries(report.steps.map((step) => [step.name, step]));
const loaded = byName["01-loaded"]?.state;
const manual = byName["02-powered-homed-manual"]?.state;
const auto = byName["03-auto-active"]?.state;
const jog = byName["04-manual-jog-x"]?.state;
const mdi = byName["05-mdi-command"]?.state;
const overrides = byName["06-overrides-hal"]?.state;
const spindleCoolant = byName["07-spindle-coolant"]?.state;
const restored = byName["08-session-restored"]?.state;
const diagnostics = byName["09-diagnostics"]?.dom;
report.checks.push(
check("Main shell regions and canvas are ready", Object.values(byName["01-loaded"]?.dom?.regions || {}).every(Boolean) && byName["01-loaded"]?.dom?.canvas?.threeReady === "true", JSON.stringify({ regions: byName["01-loaded"]?.dom?.regions, canvas: byName["01-loaded"]?.dom?.canvas })),
check("POWER and HOME leave MANUAL ready", manual?.machine?.powerOn === true && manual?.machine?.allHomed === true && manual?.machine?.mode === "manual", JSON.stringify(manual?.machine)),
check("AUTO activates without losing power/home", auto?.machine?.mode === "auto" && auto?.machine?.powerOn === true && auto?.machine?.allHomed === true, JSON.stringify(auto?.machine)),
check("Manual jog changes X", Number(jog?.axisPose?.x || 0) > Number(manual?.axisPose?.x || 0), JSON.stringify({ before: manual?.axisPose, after: jog?.axisPose })),
check("MDI command is accepted in MDI mode", mdi?.machine?.mode === "mdi" && /G90 X12\.5 Y-4 Z1\.25 F900/.test(`${mdi?.machine?.mdiCommand || ""} ${mdi?.operatorMessage || ""}`), JSON.stringify({ machine: mdi?.machine, operatorMessage: mdi?.operatorMessage })),
check("Overrides reset and HAL toggles are active", overrides?.feed?.rapidOverride === 100 && overrides?.feed?.feedOverride === 100 && overrides?.gmoccapyGui?.ignoreLimits === true && overrides?.gmoccapyGui?.optionalBlocks === true && overrides?.gmoccapyGui?.optionalStop === true, JSON.stringify({ feed: overrides?.feed, gmoccapyGui: overrides?.gmoccapyGui })),
check("Spindle and coolant controls update state", spindleCoolant?.spindle?.direction === "stop" && spindleCoolant?.spindle?.override === 100 && spindleCoolant?.coolant?.flood === true && spindleCoolant?.coolant?.mist === true, JSON.stringify({ spindle: spindleCoolant?.spindle, coolant: spindleCoolant?.coolant })),
check("Session restore returns to saved profile", restored?.machineProfile === "xyzac-trt" && restored?.sessionPersistence?.status === "restored", JSON.stringify({ machineProfile: restored?.machineProfile, sessionPersistence: restored?.sessionPersistence, evidence: report.sessionEvidence })),
check("Diagnostics expose Task/HAL and gmoccapy HAL", Boolean(diagnostics?.values?.taskHal) && Boolean(diagnostics?.values?.gmoccapyHal), JSON.stringify(diagnostics?.values)),
check("Screenshots are nonblank", report.steps.every((step) => step.pixelStats.nonBlackRatio > 0.1), report.steps.map((step) => `${step.name}:${step.pixelStats.nonBlackRatio}`).join(", ")),
);
}
async function writePdfReport(pdfPath, data) {
const reportPage = await browser.newPage();
const rows = data.checks.map((item) => `
<tr>
<td>${escapeHtml(item.name)}</td>
<td class="${item.pass ? "pass" : "fail"}">${item.pass ? "PASS" : "FAIL"}</td>
<td>${escapeHtml(item.detail)}</td>
</tr>
`).join("");
const steps = data.steps.map((step) => `
<section>
<h2>${escapeHtml(step.name)} - ${escapeHtml(step.title)}</h2>
<p>${escapeHtml(step.description)}</p>
<p><strong>Screenshot:</strong> ${escapeHtml(step.screenshotPath)}</p>
<p><strong>State:</strong> ${escapeHtml(JSON.stringify({
machineProfile: step.state.machineProfile,
runState: step.state.runState,
machine: step.state.machine,
axisPose: step.state.axisPose,
feed: step.state.feed,
spindle: step.state.spindle,
coolant: step.state.coolant,
sessionPersistence: step.state.sessionPersistence,
}))}</p>
</section>
`).join("");
await reportPage.setContent(`<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { font-family: Arial, sans-serif; margin: 28px; color: #17202a; }
h1 { font-size: 22px; margin-bottom: 6px; }
h2 { font-size: 16px; margin-top: 18px; }
table { border-collapse: collapse; width: 100%; margin-top: 14px; }
th, td { border: 1px solid #9aa5b1; padding: 6px; font-size: 11px; vertical-align: top; }
th { background: #eef2f7; }
.pass { color: #126b37; font-weight: 700; }
.fail { color: #a61b1b; font-weight: 700; }
.meta { font-size: 12px; line-height: 1.45; }
section { break-inside: avoid; border-top: 1px solid #d8dee6; padding-top: 8px; }
</style>
</head>
<body>
<h1>working7 Manual Flow Evidence</h1>
<div class="meta">
<div><strong>Status:</strong> ${escapeHtml(data.status)}</div>
<div><strong>Job ID:</strong> ${escapeHtml(data.jobId)}</div>
<div><strong>Report ID:</strong> ${escapeHtml(data.reportId)}</div>
<div><strong>Target:</strong> ${escapeHtml(data.targetUrl)}</div>
<div><strong>Generated:</strong> ${escapeHtml(data.generatedAt)}</div>
<div><strong>Screenshots:</strong> ${escapeHtml(data.screenshotsDir)}</div>
</div>
<h2>Checks</h2>
<table>
<thead><tr><th>Check</th><th>Status</th><th>Evidence</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<h2>Steps</h2>
${steps}
</body>
</html>`, { waitUntil: "load" });
await reportPage.pdf({
path: pdfPath,
format: "A4",
printBackground: true,
margin: { top: "12mm", right: "10mm", bottom: "12mm", left: "10mm" },
});
await reportPage.close();
}
function summarizeState(state = {}) {
return {
machineProfile: state.machineProfile,
activeProgram: state.activeProgram,
runState: state.runState,
activeLine: state.activeLine,
machine: {
powerOn: state.machine?.powerOn,
taskState: state.machine?.taskState,
mode: state.machine?.mode,
allHomed: state.machine?.allHomed,
interpState: state.machine?.interpState,
taskPaused: state.machine?.taskPaused,
mdiCommand: state.machine?.mdiCommand,
},
axisPose: pickAxes(state.axisPose),
dro: pickAxes(state.dro),
feed: {
rapidOverride: Number(state.feed?.rapidOverride),
feedOverride: Number(state.feed?.feedOverride),
feedRate: Number(state.feed?.feedRate),
currentVelocity: Number(state.feed?.currentVelocity),
},
spindle: {
enabled: Boolean(state.spindle?.enabled),
direction: state.spindle?.direction,
override: Number(state.spindle?.override),
rpm: Number(state.spindle?.rpm),
},
coolant: {
flood: Boolean(state.coolant?.flood),
mist: Boolean(state.coolant?.mist),
},
gmoccapyGui: {
ignoreLimits: Boolean(state.gmoccapyGui?.ignoreLimits),
optionalBlocks: Boolean(state.gmoccapyGui?.optionalBlocks),
optionalStop: Boolean(state.gmoccapyGui?.optionalStop),
lastHalPinEffect: state.gmoccapyGui?.lastHalPinEffect,
},
mdiHistory: state.mdiHistory || [],
sessionPersistence: {
status: state.sessionPersistence?.status,
storageMode: state.sessionPersistence?.storageMode,
path: state.sessionPersistence?.path,
savedAt: state.sessionPersistence?.savedAt,
restoredAt: state.sessionPersistence?.restoredAt,
},
machineFileStaging: {
status: state.machineFileStaging?.status,
fileCount: state.machineFileStaging?.fileCount,
selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel,
},
taskHalRuntimeReadiness: {
loaded: Boolean(state.taskHalRuntimeReadiness?.loaded),
halSyncReady: Boolean(state.taskHalRuntimeReadiness?.halSyncReady),
},
operatorMessage: state.operatorMessage,
};
}
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)),
};
}
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 check(name, pass, detail) {
return { name, pass: Boolean(pass), detail: String(detail ?? "-") };
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

View File

@@ -4,7 +4,9 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"evidence:working7": "node capture-working7-manual-flow-evidence.mjs",
"evidence:working7:full": "node capture-working7-full-functional-evidence.mjs",
"test": "node run-site-test.mjs"
},
"keywords": [],
"author": "",

View File

@@ -1,17 +1,72 @@
import fs from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import puppeteer from "puppeteer-core";
import { PNG } from "pngjs";
const REPO_ROOT = path.resolve("/home/meswork/cnc_wams");
const ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
const OUTPUT_DIR = path.join(ROOT, "output");
const SCREENSHOT_DIR = path.join(ROOT, "screenshots");
const URL = "https://82.156.24.101:8092/";
const CHROME_PATH = "/usr/bin/google-chrome";
const TARGET_URL = process.env.TARGET_URL || "";
const APP_URL = process.env.APP_URL || "/web-rtcp-5axis-sim-plan/app/index.html";
const CHROME_PATH = process.env.CHROME_PATH || process.env.CHROMIUM || "/usr/bin/google-chrome";
const localProgramPath = path.join(ROOT, "fixtures", "test-program.ngc");
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const MIME_TYPES = {
".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",
".png": "image/png",
".svg": "image/svg+xml",
".wasm": "application/wasm",
".xml": "application/xml; charset=utf-8",
};
function contentTypeFor(filePath) {
return MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream";
}
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 stat = await fs.stat(targetPath).catch(() => null);
let filePath = targetPath;
if (stat?.isDirectory()) {
filePath = path.join(targetPath, "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));
}
});
}
await fs.mkdir(OUTPUT_DIR, { recursive: true });
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
await fs.mkdir(path.dirname(localProgramPath), { recursive: true });
@@ -20,6 +75,17 @@ const findings = [];
const consoleLogs = [];
const pageErrors = [];
const requestFailures = [];
let server = null;
let targetUrl = TARGET_URL;
if (!targetUrl) {
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 local static server");
}
targetUrl = `http://127.0.0.1:${address.port}${APP_URL}`;
}
const browser = await puppeteer.launch({
headless: true,
@@ -83,7 +149,7 @@ try {
"",
].join("\n"), "utf8");
await page.goto(URL, { waitUntil: "networkidle2", timeout: 60000 });
await page.goto(targetUrl, { waitUntil: "networkidle2", timeout: 60000 });
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 });
await page.waitForFunction(() => {
@@ -140,6 +206,34 @@ try {
).catch(() => null);
}
async function waitForTaskHalReady(timeoutMs = 30000) {
return waitForState((state) => (
state.taskHalRuntimeReadiness?.loaded === true &&
state.taskHalRuntimeReadiness?.taskRuntimeReady === true &&
state.taskHalRuntimeReadiness?.motionRuntimeReady === true &&
state.taskHalRuntimeReadiness?.halRuntimeReady === true &&
state.taskHalRuntimeReadiness?.halSyncReady === true &&
!state.taskHalExecutionPending &&
!state.interpreterExecutionPending
), timeoutMs, "Task/HAL ready and app idle").catch(() => null);
}
async function clickJogAndWait(action, axis, direction, beforeValue) {
const selector = `[data-action="${action}"]`;
await waitForTaskHalReady(30000);
for (let attempt = 0; attempt < 2; attempt += 1) {
await clickAndWait(selector, 900);
const changed = await waitForState((nextState) => {
const nextValue = Number(nextState.axisPose?.[axis]);
return direction > 0
? nextValue > Number(beforeValue)
: nextValue < Number(beforeValue);
}, 6000, `${action} axis change`).catch(() => null);
if (changed) return changed;
}
return getState();
}
async function getSummary() {
return page.evaluate(() => {
const state = window.webRtcp5AxisSimulation.getState();
@@ -179,12 +273,31 @@ try {
}
async function clickAndWait(selector, waitMs = 600) {
await page.click(selector);
await page.waitForSelector(selector, { timeout: 10000 });
await page.waitForFunction((targetSelector) => {
const element = document.querySelector(targetSelector);
if (!element) return false;
const ariaDisabled = element.getAttribute("aria-disabled") === "true";
const commandReady = element.getAttribute("data-command-ready") === "false";
return !element.disabled && !ariaDisabled && !commandReady;
}, { timeout: 15000 }, selector);
await page.evaluate((targetSelector) => {
const element = document.querySelector(targetSelector);
if (!element) {
throw new Error(`missing click target ${targetSelector}`);
}
if (element.disabled || element.getAttribute("aria-disabled") === "true") {
throw new Error(`disabled click target ${targetSelector}`);
}
element.scrollIntoView({ block: "center", inline: "center" });
element.click();
}, selector);
await sleep(waitMs);
await waitForAppIdle();
}
async function setInputValue(selector, value) {
await page.waitForSelector(selector, { timeout: 10000 });
await page.$eval(selector, (el, nextValue) => {
el.value = nextValue;
el.dispatchEvent(new Event("input", { bubbles: true }));
@@ -193,6 +306,21 @@ try {
await sleep(400);
}
async function selectValue(selector, value) {
await page.waitForSelector(selector, { timeout: 10000 });
await page.evaluate((targetSelector, nextValue) => {
const element = document.querySelector(targetSelector);
if (!element) {
throw new Error(`missing select target ${targetSelector}`);
}
element.value = nextValue;
element.dispatchEvent(new Event("input", { bubbles: true }));
element.dispatchEvent(new Event("change", { bubbles: true }));
}, selector, value);
await sleep(400);
await waitForAppIdle();
}
function classify(condition, passText, failText) {
return condition ? { status: "PASS", text: passText } : { status: "FAIL", text: failText };
}
@@ -253,10 +381,11 @@ try {
initialBoundaryStatus.status,
);
const taskHalReadyState = await waitForTaskHalReady(30000);
const initialTaskHalStatus = classifyWarn(
!/pending|blocked/i.test(initial.taskHal),
`Task/HAL 已就绪:${initial.taskHal}`,
`Task/HAL 未完成就绪:${initial.taskHal}`,
Boolean(taskHalReadyState),
`Task/HAL 已就绪:${taskHalReadyState?.taskHalRuntimeReadiness?.semanticBoundary || initial.taskHal}`,
`Task/HAL 冷启动时尚未完成就绪:${initial.taskHal}`,
);
await recordResult(
"initial-task-hal",
@@ -290,6 +419,7 @@ try {
await clickAndWait('[data-action="HOME"]');
await waitForState((nextState) => nextState.machine.allHomed === true, 8000, "machine homed").catch(() => null);
await waitForTaskHalReady(30000);
state = await getState();
await recordResult(
"home",
@@ -300,8 +430,7 @@ try {
);
const beforeJogX = state.axisPose.x;
await clickAndWait('[data-action="JOG_X_POS"]');
state = await getState();
state = await clickJogAndWait("JOG_X_POS", "x", 1, beforeJogX);
await recordResult(
"jog-x-plus",
"JOG X+",
@@ -311,8 +440,7 @@ try {
);
const beforeJogY = state.axisPose.y;
await clickAndWait('[data-action="JOG_Y_NEG"]');
state = await getState();
state = await clickJogAndWait("JOG_Y_NEG", "y", -1, beforeJogY);
await recordResult(
"jog-y-minus",
"JOG Y-",
@@ -332,6 +460,9 @@ try {
state.machine.mode === "auto" ? "PASS" : "FAIL",
);
await clickAndWait('[data-action="mode-manual"]');
await waitForState((nextState) => nextState.machine.mode === "manual" && nextState.machine.interpState === "idle", 10000, "MANUAL idle before MDI").catch(() => null);
await waitForTaskHalReady(30000);
await clickAndWait('[data-action="mode-mdi"]');
await waitForState((nextState) => nextState.machine.mode === "mdi", 8000, "MDI mode").catch(() => null);
state = await getState();
@@ -343,7 +474,17 @@ try {
state.machine.mode === "mdi" ? "PASS" : "FAIL",
);
if (state.machine.mode !== "mdi") {
await clickAndWait('[data-action="mode-manual"]');
await waitForState((nextState) => nextState.machine.mode === "manual", 10000, "retry manual before MDI").catch(() => null);
await waitForTaskHalReady(30000);
await clickAndWait('[data-action="mode-mdi"]');
await waitForState((nextState) => nextState.machine.mode === "mdi", 10000, "retry MDI mode").catch(() => null);
state = await getState();
}
await setInputValue('[data-action="mdi-command"]', "M428");
await waitForState((nextState) => nextState.machine.mode === "mdi", 10000, "MDI active before M428");
await clickAndWait('[data-action="mdi-submit"]', 800);
state = await getState();
await recordResult(
@@ -493,7 +634,7 @@ try {
fullOn && stateAfterFullOff.preview.fullscreen === false ? "PASS" : "FAIL",
);
await page.select('[data-action="select-profile"]', "xyzbc-trt");
await selectValue('[data-action="select-profile"]', "xyzbc-trt");
await sleep(2000);
state = await getState();
await recordResult(
@@ -505,7 +646,7 @@ try {
);
await capture("02-profile-xyzbc");
await page.select('[data-action="select-profile"]', "xyzac-trt");
await selectValue('[data-action="select-profile"]', "xyzac-trt");
await sleep(2000);
state = await getState();
await recordResult(
@@ -534,7 +675,7 @@ try {
if (stagedCount > 0) {
const sourceRel = summary.state.machineFileStaging.gcodeSources[0].sourceRel;
await page.select('[data-action="select-linuxcnc-gcode-source"]', sourceRel);
await selectValue('[data-action="select-linuxcnc-gcode-source"]', sourceRel);
await sleep(4000);
summary = await getSummary();
const loadedVendored = summary.state.programSource === "linuxcnc-vendored-5axis-gcode";
@@ -604,9 +745,13 @@ try {
await recordResult(
"step-program",
"Step 单步执行",
"点击 Step 后 runState=steppingactiveLine 前进或保持受控",
`runState=${state.runState}, activeLine=${state.activeLine}`,
state.runState === "stepping" || state.activeLine !== beforeStepLine ? "PASS" : "FAIL",
"点击 Step 后 Task/HAL 记录 singleStepping=true并保持暂停态等待后续操作",
`runState=${state.runState}, activeLine=${state.activeLine}, taskPaused=${state.machine.taskPaused}, singleStepping=${state.taskHalStatus?.task?.singleStepping}`,
state.machine.taskPaused === true
&& state.taskHalStatus?.task?.singleStepping === true
&& Number(state.activeLine) >= Number(beforeStepLine)
? "PASS"
: "FAIL",
);
await clickAndWait('[data-action="STOP"]', 1200);
@@ -657,7 +802,10 @@ try {
saveSessionStatus.status,
);
await clickAndWait('[data-action="mode-manual"]');
await waitForState((nextState) => nextState.machine.mode === "manual", 8000, "manual mode before session mutation").catch(() => null);
await clickAndWait('[data-action="JOG_X_POS"]');
await waitForState((nextState) => Number(nextState.axisPose.x) !== Number(state.axisPose.x), 8000, "axis changed before restore").catch(() => null);
const modifiedState = await getState();
await clickAndWait('[data-action="RESTORE_SESSION"]', 2500);
state = await getState();
@@ -712,7 +860,7 @@ try {
const report = {
generatedAt: new Date().toISOString(),
targetUrl: URL,
targetUrl,
chromePath: CHROME_PATH,
screenshots,
screenshotAnalysis,
@@ -722,10 +870,20 @@ try {
pageErrors,
requestFailures,
};
report.status = findings.some((finding) => finding.status === "FAIL") || pageErrors.length > 0
? "FAIL"
: "PASS";
await fs.writeFile(path.join(OUTPUT_DIR, "site-test-report.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
console.log(`site_test_status=${report.status}`);
console.log(`site_test_target_url=${targetUrl}`);
console.log(`site_test_report=/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/site-test-report.json`);
if (report.status !== "PASS") {
process.exitCode = 1;
}
} finally {
await browser.close();
await browser.close().catch(() => {});
if (server) await new Promise((resolve) => server.close(resolve));
}
async function analyzeScreenshot(filePath) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 247 KiB

View File

@@ -23,10 +23,27 @@ static std::vector<std::string> g_canon_events;
static double g_external_feed_rate = 0.0;
static int g_external_axis_mask = Interp::AXIS_MASK_X | Interp::AXIS_MASK_Y | Interp::AXIS_MASK_Z;
static std::string g_parameter_file_name;
static CANON_PLANE g_external_plane = CANON_PLANE::XY;
struct ExternalPosition {
double x = 0.0;
double y = 0.0;
double z = 0.0;
double a = 0.0;
double b = 0.0;
double c = 0.0;
double u = 0.0;
double v = 0.0;
double w = 0.0;
};
static ExternalPosition g_external_position;
void reset_canon_events()
{
g_canon_events.clear();
g_external_position = ExternalPosition{};
g_external_plane = CANON_PLANE::XY;
}
void push_canon_event(const std::string &event)
@@ -56,6 +73,65 @@ int external_axis_mask()
return g_external_axis_mask;
}
void set_external_position(double x, double y, double z,
double a, double b, double c,
double u, double v, double w)
{
g_external_position.x = x;
g_external_position.y = y;
g_external_position.z = z;
g_external_position.a = a;
g_external_position.b = b;
g_external_position.c = c;
g_external_position.u = u;
g_external_position.v = v;
g_external_position.w = w;
}
void set_external_arc_position(double first_end, double second_end,
double axis_end_point,
double a, double b, double c,
double u, double v, double w)
{
ExternalPosition next = g_external_position;
switch (g_external_plane) {
case CANON_PLANE::XY:
next.x = first_end;
next.y = second_end;
next.z = axis_end_point;
break;
case CANON_PLANE::YZ:
next.x = axis_end_point;
next.y = first_end;
next.z = second_end;
break;
case CANON_PLANE::XZ:
next.x = second_end;
next.y = axis_end_point;
next.z = first_end;
break;
case CANON_PLANE::UV:
next.u = first_end;
next.v = second_end;
next.w = axis_end_point;
break;
case CANON_PLANE::VW:
next.u = axis_end_point;
next.v = first_end;
next.w = second_end;
break;
case CANON_PLANE::UW:
next.u = second_end;
next.v = axis_end_point;
next.w = first_end;
break;
}
next.a = a;
next.b = b;
next.c = c;
g_external_position = next;
}
void reset_parameter_file_name()
{
g_parameter_file_name.clear();
@@ -138,6 +214,7 @@ void CANON_UPDATE_END_POINT(double x, double y, double z,
double a, double b, double c,
double u, double v, double w)
{
standalone::set_external_position(x, y, z, a, b, c, u, v, w);
std::ostringstream oss;
oss << "CANON_UPDATE_END_POINT"
<< " x=" << x
@@ -159,6 +236,7 @@ void USE_LENGTH_UNITS(CANON_UNITS units)
}
void SELECT_PLANE(CANON_PLANE plane)
{
standalone::g_external_plane = plane;
std::ostringstream oss;
oss << "SELECT_PLANE plane=" << static_cast<int>(plane);
standalone::push_canon_event(oss.str());
@@ -515,16 +593,16 @@ void SET_PARAMETER_FILE_NAME(const char *filename)
{
standalone::set_parameter_file_name(filename);
}
CANON_PLANE GET_EXTERNAL_PLANE() { return CANON_PLANE::XY; }
double GET_EXTERNAL_POSITION_A() { return 0.0; }
double GET_EXTERNAL_POSITION_B() { return 0.0; }
double GET_EXTERNAL_POSITION_C() { return 0.0; }
double GET_EXTERNAL_POSITION_X() { return 0.0; }
double GET_EXTERNAL_POSITION_Y() { return 0.0; }
double GET_EXTERNAL_POSITION_Z() { return 0.0; }
double GET_EXTERNAL_POSITION_U() { return 0.0; }
double GET_EXTERNAL_POSITION_V() { return 0.0; }
double GET_EXTERNAL_POSITION_W() { return 0.0; }
CANON_PLANE GET_EXTERNAL_PLANE() { return standalone::g_external_plane; }
double GET_EXTERNAL_POSITION_A() { return standalone::g_external_position.a; }
double GET_EXTERNAL_POSITION_B() { return standalone::g_external_position.b; }
double GET_EXTERNAL_POSITION_C() { return standalone::g_external_position.c; }
double GET_EXTERNAL_POSITION_X() { return standalone::g_external_position.x; }
double GET_EXTERNAL_POSITION_Y() { return standalone::g_external_position.y; }
double GET_EXTERNAL_POSITION_Z() { return standalone::g_external_position.z; }
double GET_EXTERNAL_POSITION_U() { return standalone::g_external_position.u; }
double GET_EXTERNAL_POSITION_V() { return standalone::g_external_position.v; }
double GET_EXTERNAL_POSITION_W() { return standalone::g_external_position.w; }
double GET_EXTERNAL_PROBE_POSITION_A() { return 0.0; }
double GET_EXTERNAL_PROBE_POSITION_B() { return 0.0; }
double GET_EXTERNAL_PROBE_POSITION_C() { return 0.0; }
@@ -629,6 +707,7 @@ void STRAIGHT_TRAVERSE(int lineno,
double a, double b, double c,
double u, double v, double w)
{
standalone::set_external_position(x, y, z, a, b, c, u, v, w);
std::ostringstream oss;
oss << "STRAIGHT_TRAVERSE line=" << lineno
<< " x=" << x
@@ -648,6 +727,7 @@ void STRAIGHT_FEED(int lineno,
double a, double b, double c,
double u, double v, double w)
{
standalone::set_external_position(x, y, z, a, b, c, u, v, w);
std::ostringstream oss;
oss << "STRAIGHT_FEED line=" << lineno
<< " x=" << x
@@ -669,6 +749,8 @@ void ARC_FEED(int lineno,
double a, double b, double c,
double u, double v, double w)
{
standalone::set_external_arc_position(first_end, second_end, axis_end_point,
a, b, c, u, v, w);
std::ostringstream oss;
oss << "ARC_FEED line=" << lineno
<< " first_end=" << first_end

View File

@@ -870,6 +870,7 @@ char *lcinterp_run_fiveaxis_remap_file(const char *path, const char *ini_path)
ok &= file_execute_count < max_file_steps;
ok &= hal_rc == INTERP_OK && hal_status == 1;
append_events_and_state(output, interp);
output << "fiveaxis_linuxcnc_remap_file_execute=" << (ok ? 1 : 0) << "\n";
unsetenv("INI_FILE_NAME");
return copy_result(output.str());

View File

@@ -603,3 +603,45 @@ nc_files/arcspiral.ngc
nc_files/factorial.ngc
nc_files/hole-circle.ngc
nc_files/m6demo.ngc
configs/sim/gmoccapy/macros/change.ngc
configs/sim/gmoccapy/macros/change_g43.ngc
configs/sim/gmoccapy/macros/go_to_position.ngc
configs/sim/gmoccapy/macros/halo_world.ngc
configs/sim/gmoccapy/macros/i_am_lost.ngc
configs/sim/gmoccapy/macros/images/goto_x_y_z.png
configs/sim/gmoccapy/macros/images/i_am_lost.png
configs/sim/gmoccapy/macros/images/macro_8.png
configs/sim/gmoccapy/macros/increment.ngc
configs/sim/gmoccapy/macros/jog_around.ngc
configs/sim/gmoccapy/macros/macro_0.ngc
configs/sim/gmoccapy/macros/macro_1.ngc
configs/sim/gmoccapy/macros/macro_10.ngc
configs/sim/gmoccapy/macros/macro_11.ngc
configs/sim/gmoccapy/macros/macro_12.ngc
configs/sim/gmoccapy/macros/macro_13.ngc
configs/sim/gmoccapy/macros/macro_14.ngc
configs/sim/gmoccapy/macros/macro_15.ngc
configs/sim/gmoccapy/macros/macro_2.ngc
configs/sim/gmoccapy/macros/macro_3.ngc
configs/sim/gmoccapy/macros/macro_4.ngc
configs/sim/gmoccapy/macros/macro_5.ngc
configs/sim/gmoccapy/macros/macro_6.ngc
configs/sim/gmoccapy/macros/macro_7.ngc
configs/sim/gmoccapy/macros/macro_8.ngc
configs/sim/gmoccapy/macros/macro_9.ngc
configs/sim/gmoccapy/macros/macro_Instructions.txt
configs/sim/gmoccapy/macros/on_abort.ngc
configs/sim/gmoccapy/macros/settool_g43.ngc
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/README
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/test-xyzac.ngc
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/test-xyzbc.ngc
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/postgui.hal
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/sim-xyzac-trt.pref
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.tbl
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt_cmds.hal
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac.var
configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac.var.bak

View File

@@ -0,0 +1,75 @@
o<change> sub
;(debug, in change tool_in_spindle=#<tool_in_spindle> current_pocket=#<current_pocket>)
;(debug, selected_tool=#<selected_tool> selected_pocket=#<selected_pocket>)
;otherwise after the M6 this information is gone!
#<tool> = #<selected_tool>
#<pocket> = #<selected_pocket>
; we must execute this only in the milltask interpreter
; or preview will break, so test for '#<_task>' which is 1 for
; the milltask interpreter and 0 in the UI's
O100 if [#<_task> EQ 0]
(debug, Task ist Null)
O100 return [999]
O100 endif
;first go up
G53 G0 Z[#<_ini[CHANGE_POSITION]Z>]
; then move to change position
G53 G0 X[#<_ini[CHANGE_POSITION]X>] Y[#<_ini[CHANGE_POSITION]Y>]
; cancel tool offset
G49
; using the code being remapped here means 'use builtin behaviour'
M6
O200 if [#<_hal[gmoccapy.toolmeasurement]> EQ 0]
O200 return [3] ; indicate no tool measurement
O200 endif
G53 G0 X[#<_ini[TOOLSENSOR]X>] Y[#<_ini[TOOLSENSOR]Y>]
G53 G0 Z[#<_ini[TOOLSENSOR]Z>]
O300 if [#<_hal[gmoccapy.searchvel]> LE 0]
O300 return [-1] ; indicate searchvel <= 0
O300 endif
O400 if [#<_hal[gmoccapy.probevel]> LE 0]
O400 return [-2] ; indicate probevel <= 0
O400 endif
F #<_hal[gmoccapy.searchvel]>
G91
G38.2 Z #<_ini[TOOLSENSOR]MAXPROBE>
G0 Z2
; This is commented out only for sim.
;F #<_hal[gmoccapy.probevel]>
;G38.2 Z-4
O500 if [#5070 EQ 0]
G90
O500 return [-3] ; indicate probe contact failure to epilog
O500 endif
G90
G53 G0 Z[#<_ini[CHANGE_POSITION]Z>]
#<touch_result> = #5063
#<probeheight> = #<_hal[gmoccapy.probeheight]>
#<blockheight> = #<_hal[gmoccapy.blockheight]>
;(DEBUG, #<touch_result> #<probeheight> #<blockheight>)
G10 L1 P#<tool> Z[#<touch_result> - #<_hal[gmoccapy.probeheight]> + #<_hal[gmoccapy.blockheight]>]
G43
;G10 L1 P#<tool> Z#<touch_result>
;G10 L2 P0 Z[#<workpieceheight> + #<probeheight> + #<touch_result>]
; signal success be returning a value > 0:
o<change> endsub [1]

View File

@@ -0,0 +1,21 @@
o<change_g43> sub
;(debug, in change tool_in_spindle=#<tool_in_spindle> current_pocket=#<current_pocket>)
;(debug, selected_tool=#<selected_tool> selected_pocket=#<selected_pocket>)
; we must execute this only in the milltask interpreter
; or preview will break, so test for '#<_task>' which is 1 for
; the milltask interpreter and 0 in the UI's
O100 if [#<_task> EQ 0]
(debug, Task ist Null)
O100 return [999]
O100 endif
; using the code being remapped here means 'use builtin behaviour'
M6
; set tool offset
G43
; signal success be returning a value > 0:
o<change_g43> endsub [1]
M2

View File

@@ -0,0 +1,26 @@
; Testfile go to position
; will jog the machine to a given position
; the image path must be relative from your config dir or absolute, "~" is allowed
(IMAGE, ./macros/images/goto_x_y_z.png)
O<go_to_position> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<go_to_position> endsub
M2

View File

@@ -0,0 +1,25 @@
; Testfile "hello world"
; will just give messages
O<halo_world> sub
G17
G21
G54
G61
G40
G49
G80
G90
G0 X10
(MSG, Hallo Welt)
(MSG, hello world)
G0X-10
O<halo_world> endsub
M2

View File

@@ -0,0 +1,27 @@
; Testfile I am Lost
; will jog to machine zero and set all axis to zero
; the image path must be relative from your config dir or absolute, "~" is allowed
(IMAGE, ./macros/images/i_am_lost.png)
O<i_am_lost> sub
G17
G21
G54
G61
G40
G49
G80
G90
(MSG, Will now move to machine zero)
G53 G0 X0 Y0 Z0
(MSG, will now set all axis to zero)
G10 L20 P0 X0 Y0 Z0
(MSG, all done)
O<i_am_lost> endsub
M2

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View File

@@ -0,0 +1,22 @@
; Testfile "increment"
; will move the machine in relative coordinates
O<increment> sub
G17
G21
G54
G61
G40
G49
G80
G90
G91 G0 X#1 Y#2
G90
(DEBUG, X was [#1] and Y was [#2])
O<increment> endsub
M2

View File

@@ -0,0 +1,29 @@
; Testfile "Jog around"
; will just jog a little bit around
O<jog_around> sub
G17
G21
G54
G61
G40
G49
G80
G90
G91 G0 X 25
Y-25
Z-25
Y25
X-25
Z25
F250
G2 I 25
(MSG, It is done!)
O<jog_around> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_0> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_0> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_1> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_1> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_10> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_10> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_11> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_11> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_12> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_12> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_13> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_13> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_14> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_14> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_15> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_15> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_2> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_2> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_3> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_3> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_4> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_4> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_5> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_5> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_6> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_6> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_7> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_7> endsub
M2

View File

@@ -0,0 +1,27 @@
; Testfile go to position
; will jog the machine to a position to give
; the image path must be relative from your config dir or absolute, "~" is allowed
(IMAGE, ./macros/images/macro_8.png)
O<macro_8> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_8> endsub
M2

View File

@@ -0,0 +1,24 @@
; Testfile go to position
; will jog the machine to a position to give
O<macro_9> sub
G17
G21
G54
G61
G40
G49
G80
G90
;#1 = <X-Pos>
;#2 = <Y-Pos>
;#3 = <Z-Pos>
(DEBUG, Will now move machine to X = #1 , Y = #2 , Z = #3)
G0 X #1 Y #2 Z #3
O<macro_9> endsub
M2

View File

@@ -0,0 +1,44 @@
This is a small instruction to include macros in gmoccapy.
In your INI File you need to introduce a section called [MACROS]
and for every macro you'll need to include a one-liner like so:
MACRO = jog_around
or
MACRO = increment xinc yinc
where xinc and yinc are placeholders
During execution of the macro, you will be asked to enter the values.
You are allowed to introduce 9 macros!
If you enter more macros, only the first 9 will appear with button in gmoccapy.
In the [RS274NGC] section you may want to give a path to your macros like so:
[RS274NGC]
SUBROUTINE_PATH = nc_files/subroutines
or you place your macros in the nc_files folder.
Each macro must have it's own file in one of the mentioned folders and they are normal subroutines, so the must begin with:
O<jog_around> sub
and end with
O<jog_around> endsub
M2
The name of the file must be jog_around.ngc.
And an macro must contain at least one movement of one axis.
macro name in INI file have to be the same as the file name and the sub must have also the same name (case sensitive!)!
BE CAREFUL:
At this development step, the macros can only be interrupted by pressing the machine off button or the emergency exit button! I will check how to make this more secure!
Hope this does help.
Norbert

View File

@@ -0,0 +1,9 @@
%
o<on_abort> sub
G90
G40
G49
o<on_abort> endsub
%

View File

@@ -0,0 +1,20 @@
o<settool_g43> sub
;(debug, tool=#<tool> pocket=#<pocket>)
; we must execute this only in the milltask interpreter
; or preview will break, so test for '#<_task>' which is 1 for
; the milltask interpreter and 0 in the UI's
O100 if [#<_task> EQ 0]
(debug, Task ist Null)
O100 return [999]
O100 endif
; using the code being remapped here means 'use builtin behaviour'
m61 q#<tool>
; set tool offset
G43
; signal success be returning a value > 0:
o<settool_g43> endsub [1]
M2

View File

@@ -0,0 +1,12 @@
Table Rotary/Tilting (trt) Sim configs
xyzac-trt
xyzbc-trt
Example ngc files:
examples/boat-xyzac.ngc (!!! TOUCHOFF table Z to -13.25 !!!)
examples/boat-xyzbc.ngc (!!! TOUCHOFF table Z to - 8.30 !!!)
Example ngc files (ngcgui compatible subroutines):
examples/test-xyzac.ngc
examples/test-xyzbc.ngc

View File

@@ -0,0 +1,47 @@
o<test-xyzac> sub
(test-xyzac-kins with offsets and tool change)
( 5 AXIS XYZAC MILLING )
#<a_angle> = #1 (=20)
#<c_angle> = #2 (=30)
#<xy_max> = #3 (=30)
#<z_max> = #4 (=10)
#<feedrate> = #5 (=200)
#<tool1> = #6 (=1 1st Tool)
#<tool2> = #7 (=2 2nd Tool)
#<speed1> = #8 (=500)
#<speed2> = #9 (=600)
; 'square with 2 rounded corners'
#<x_max> = #<xy_max>
#<y_max> = #<xy_max>
#<g3_j> = #<xy_max>
N40 G00 G17 G40 G90 G94 Z10
(first tool - no z-offset)
T#<tool1> M6
S#<speed1> M3
G00 A#<a_angle> C#<c_angle>
G43 H#<tool1> Z0
G01 X#<x_max> F#<feedrate>
G01 Y#<y_max>
G01 X0
G03 X-#<x_max> Y0 I0 J-#<g3_j>
G00 Z#<z_max>
G00 X0
M5
(next tool- with z-offset)
T#<tool2> M6
S#<speed2> M3
G00 A#<a_angle> C#<c_angle>
G43 H#<tool2> Z0
G01 X-#<x_max> F#<feedrate>
G01 Y-#<y_max>
G01 X0
G03 X#<x_max> Y0 I0 J#<g3_j>
G0 Z#<z_max>
G0 X0 Y0
M5
A0 C0
o<test-xyzac> endsub

View File

@@ -0,0 +1,47 @@
o<test-xyzbc> sub
(test-xyzbc-trt-kins with offsets and tool change)
( 5 AXIS XYZBC MILLING )
#<b_angle> = #1 (=20)
#<c_angle> = #2 (=30)
#<xy_max> = #3 (=30)
#<z_max> = #4 (=10)
#<feedrate> = #5 (=200)
#<tool1> = #6 (=1 1st Tool)
#<tool2> = #7 (=2 2nd Tool)
#<speed1> = #8 (=500)
#<speed2> = #9 (=600)
;'square with 2 rounded corners'
#<x_max> = #<xy_max>
#<y_max> = #<xy_max>
#<g3_j> = #<xy_max>
G00 G17 G40 G90 G94 Z10
(first tool - no z-offset)
T#<tool1> M6
S#<speed1> M3
G00 B#<b_angle> C#<c_angle>
G43 H#<tool1> Z0
G01 X#<x_max> F#<feedrate>
G01 Y#<y_max>
G01 X0
G03 X-#<x_max> Y0 I0 J-#<g3_j>
G00 Z#<z_max>
G00 X0
M5
(next tool- with z-offset)
T#<tool2> M6
S#<speed2> M3
G00 B#<b_angle> C#<c_angle>
G43 H#<tool2> Z0
G01 X-#<x_max> F#<feedrate>
G01 Y-#<y_max>
G01 X0
G03 X#<x_max> Y0 I0 J#<g3_j>
G0 Z#<z_max>
G0 X0 Y0
M5
B0 C0
o<test-xyzbc> endsub

View File

@@ -0,0 +1,22 @@
###################################################################
# moccapy_postgui.hal file from Norbert Schechner #
###################################################################
loadrt abs names=abs_spindle_feedback
addf abs_spindle_feedback servo-thread
net spindle-speed-limited => abs_spindle_feedback.in
net spindle-abs abs_spindle_feedback.out => gmoccapy.spindle_feedback_bar
net spindle-at-speed gmoccapy.spindle_at_speed_led
# the unlink pin commands are only used, because they are connected
# in core_sim.hal and we use this file to simulate
unlinkp iocontrol.0.tool-change
unlinkp iocontrol.0.tool-changed
net tool-change gmoccapy.toolchange-change <= iocontrol.0.tool-change
net tool-changed gmoccapy.toolchange-changed => iocontrol.0.tool-changed
net tool-prep-number gmoccapy.toolchange-number <= iocontrol.0.tool-prep-number
net tool-offset gmoccapy.tooloffset-z

View File

@@ -0,0 +1,97 @@
[DEFAULT]
spindle_start_rpm = 300
scale_jog_vel = 21.0
scale_spindle_override = 1
scale_feed_override = 1
scale_rapid_override = 1
hide_turtle_jog_button = False
turtle_jog_factor = 10
dro_size = 28
open_file =
screen1 = window
x_pos = 40
y_pos = 30
width = 979
height = 750
gtk_theme = Follow System Theme
audio_alert = /usr/share/sounds/freedesktop/stereo/dialog-warning.oga
audio_error = /usr/share/sounds/freedesktop/stereo/dialog-error.oga
grid_size = 1.0
view = p
mouse_btn_mode = 4
hide_cursor = False
system_name_tool = Working Offset
system_name_g5x = G5x
system_name_rot = Rot
system_name_g92 = G92
system_name_g54 = G54
system_name_g55 = G55
system_name_g56 = G56
system_name_g57 = G57
system_name_g58 = G58
system_name_g59 = G59
system_name_g59.1 = G59.1
system_name_g59.2 = G59.2
system_name_g59.3 = G59.3
jump_to_dir = /home/gmoccapy
show_keyboard_on_offset = False
show_keyboard_on_tooledit = False
show_keyboard_on_edit = False
show_keyboard_on_mdi = False
show_keyboard_on_file_selection = False
spindle_bar_min = 0.0
spindle_bar_max = 6000.0
x_pos_popup = 45.0
y_pos_popup = 55
width_popup = 250.0
max_messages = 10
message_font = sans 10
use_frames = True
show_dro_btn = False
use_auto_units = True
blockdel = False
opstop = False
enable_dro = False
show_offsets = False
show_dtg = False
view_tool_path = True
view_dimension = True
gremlin_view = rbt_view_p
run_from_line = no_run
unlock_way = no
unlock_code = 123
show_preview_on_offset = False
use_keyboard_shortcuts = True
abs_color = #0000FF
rel_color = #000000
dtg_color = #FFFF00
homed_color = #00FF00
unhomed_color = #FF0000
dro_digits = 3
toggle_readout = True
reload_tool = True
tool_in_spindle = 0
blockheight = 0.0
use_toolmeasurement = False
kbd_height = 250
kbd_width = 880
kbd_set_height = False
kbd_set_width = False
info_tab_page = 0
jog_btn_size = 48
jog_box_width = 360
toolpage_use_calc = True
offsetpage_use_calc = True
gcodeview_font = monospace 10
hide_titlebar = False
icon_theme = classic
gcode_theme = classic
audio_enabled = True
hide_tooltips = False
system_name_g28 = G28
system_name_g30 = G30
iconview_sortorder = 0
iconview_sortbydate = 1
iconview_folderfirst = 0
sort_by_date = False

View File

@@ -0,0 +1,156 @@
[EMC]
VERSION = 1.1
MACHINE = sim-xyzac-trt
DEBUG = 0
[DISPLAY]
OPEN_FILE = ./examples/impeller-7bl-xyzac.ngc
JOG_AXES = XYZC
DISPLAY = gmoccapy
POSITION_OFFSET = RELATIVE
POSITION_FEEDBACK = ACTUAL
MAX_FEED_OVERRIDE = 2
PROGRAM_PREFIX = ../../../nc_files
INTRO_GRAPHIC = emc2.gif
INTRO_TIME = 1
TKPKG = Ngcgui 1.0
NGCGUI_FONT = Helvetica -12 normal
NGCGUI_SUBFILE = test-xyzac.ngc
[RS274NGC]
PARAMETER_FILE = xyzac.var
SUBROUTINE_PATH = ./examples:../../macros
REMAP=M6 modalgroup=6 prolog=change_prolog ngc=change_g43 epilog=change_epilog
REMAP=M61 modalgroup=6 prolog=settool_prolog ngc=settool_g43 epilog=settool_epilog
# the Python plugins serves interpreter and task
[PYTHON]
PATH_PREPEND = ../../python
TOPLEVEL = ../../python/toplevel.py
LOG_LEVEL = 0
[HAL]
HALUI = halui
HALFILE = LIB:basic_sim.tcl -no_use_hal_manualtoolchange
# 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
POSTGUI_HALFILE = postgui.hal
[KINS]
KINEMATICS = xyzac-trt-kins
JOINTS = 5
[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
[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
[AXIS_Y]
MIN_LIMIT = -100
MAX_LIMIT = 100
MAX_VELOCITY = 20
MAX_ACCELERATION = 300
[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
[AXIS_Z]
MIN_LIMIT = -120
MAX_LIMIT = 120
MAX_VELOCITY = 20
MAX_ACCELERATION = 300
[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
[AXIS_A]
MIN_LIMIT = -100
MAX_LIMIT = 50
MAX_VELOCITY = 30
MAX_ACCELERATION = 300
[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
[AXIS_C]
MIN_LIMIT = -36000
MAX_LIMIT = 36000
MAX_VELOCITY = 30
MAX_ACCELERATION = 300
[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

View File

@@ -0,0 +1,10 @@
T1 P1 Z0 D6 ;end mill
T2 P2 Z15 D8 ;end mill
T3 P3 Z0 D4.2 ;#7 tap drill
T4 P4 Z0 D10
T5 P5 Z30 D10
T6 P6 Z30 D10
T7 P7 Z30 D10
T8 P8 Z30 D10
T9 P9 Z30 D10
T10 P10 D0.5

View File

@@ -0,0 +1,202 @@
# Fri Jun 26 05:57:43 EDT 2026
#
# This file: ./xyzac-trt_cmds.hal
# Created by: /home/meswork/cnc_wams/linuxcnc/lib/hallib/basic_sim.tcl
# With options: -no_use_hal_manualtoolchange
# From inifile: /home/meswork/cnc_wams/linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini
# Halfiles: {LIB:basic_sim.tcl -no_use_hal_manualtoolchange}
#
# This file contains the hal commands produced by basic_sim.tcl
# (and any hal commands executed prior to its execution).
# ------------------------------------------------------------------
# To use ./xyzac-trt_cmds.hal in the original inifile (or a copy of it),
# edit to change:
# [HAL]
# HALFILE = LIB:basic_sim.tcl parameters
# to:
# [HAL]
# HALFILE = ./xyzac-trt_cmds.hal
#
# Notes:
# 1) Inifile Variables substitutions specified in the inifile
# and interpreted by halcmd are automatically substituted
# in the created halfile (./xyzac-trt_cmds.hal).
# 2) Input pins connected to a signal with no writer are
# not included in the setp listings herein so must be added
# manually
#
# components
#preloaded module: loadrt tpmod
#preloaded module: loadrt homemod
loadrt xyzac-trt-kins
loadrt motmod base_period_nsec=0 servo_period_nsec=1000000 num_joints=5
#loadrt __servo-thread (not loaded by loadrt, no args saved)
loadrt pid names=J0_pid,J1_pid,J2_pid,J3_pid,J4_pid
loadrt mux2 names=J0_mux,J1_mux,J2_mux,J3_mux,J4_mux
loadrt ddt names=J0_vel,J0_accel,J1_vel,J1_accel,J2_vel,J2_accel,J3_vel,J3_accel,J4_vel,J4_accel
loadrt sim_home_switch names=J0_switch,J1_switch,J2_switch,J3_switch,J4_switch
loadrt sim_spindle names=sim_spindle
loadrt limit2 names=limit_speed
loadrt lowpass names=spindle_mass
loadrt near names=near_speed
loadrt scale names=rpm_rps
# pin aliases
# param aliases
# signals
# nets
net J0:acc J0_accel.out
net J0:enable joint.0.amp-enable-out => J0_pid.enable
net J0:homesw J0_switch.home-sw => joint.0.home-sw-in
net J0:on-pos J0_pid.output => J0_mux.in1
net J0:pos-cmd joint.0.motor-pos-cmd => J0_pid.command
net J0:pos-fb J0_mux.out => J0_mux.in0 J0_switch.cur-pos J0_vel.in joint.0.motor-pos-fb
net J0:vel J0_vel.out => J0_accel.in
net J1:acc J1_accel.out
net J1:enable joint.1.amp-enable-out => J1_pid.enable
net J1:homesw J1_switch.home-sw => joint.1.home-sw-in
net J1:on-pos J1_pid.output => J1_mux.in1
net J1:pos-cmd joint.1.motor-pos-cmd => J1_pid.command
net J1:pos-fb J1_mux.out => J1_mux.in0 J1_switch.cur-pos J1_vel.in joint.1.motor-pos-fb
net J1:vel J1_vel.out => J1_accel.in
net J2:acc J2_accel.out
net J2:enable joint.2.amp-enable-out => J2_pid.enable
net J2:homesw J2_switch.home-sw => joint.2.home-sw-in
net J2:on-pos J2_pid.output => J2_mux.in1
net J2:pos-cmd joint.2.motor-pos-cmd => J2_pid.command
net J2:pos-fb J2_mux.out => J2_mux.in0 J2_switch.cur-pos J2_vel.in joint.2.motor-pos-fb
net J2:vel J2_vel.out => J2_accel.in
net J3:acc J3_accel.out
net J3:enable joint.3.amp-enable-out => J3_pid.enable
net J3:homesw J3_switch.home-sw => joint.3.home-sw-in
net J3:on-pos J3_pid.output => J3_mux.in1
net J3:pos-cmd joint.3.motor-pos-cmd => J3_pid.command
net J3:pos-fb J3_mux.out => J3_mux.in0 J3_switch.cur-pos J3_vel.in joint.3.motor-pos-fb
net J3:vel J3_vel.out => J3_accel.in
net J4:acc J4_accel.out
net J4:enable joint.4.amp-enable-out => J4_pid.enable
net J4:homesw J4_switch.home-sw => joint.4.home-sw-in
net J4:on-pos J4_pid.output => J4_mux.in1
net J4:pos-cmd joint.4.motor-pos-cmd => J4_pid.command
net J4:pos-fb J4_mux.out => J4_mux.in0 J4_switch.cur-pos J4_vel.in joint.4.motor-pos-fb
net J4:vel J4_vel.out => J4_accel.in
net estop:loop iocontrol.0.user-enable-out => iocontrol.0.emc-enable-in
net sample:enable motion.motion-enabled => J0_mux.sel J1_mux.sel J2_mux.sel J3_mux.sel J4_mux.sel
net spindle-at-speed near_speed.out => spindle.0.at-speed
net spindle-index-enable sim_spindle.index-enable <=> spindle.0.index-enable
net spindle-orient spindle.0.orient => spindle.0.is-oriented
net spindle-pos sim_spindle.position-fb => spindle.0.revs
net spindle-rpm-filtered spindle_mass.out => near_speed.in2 rpm_rps.in
net spindle-rps-filtered rpm_rps.out => spindle.0.speed-in
net spindle-speed-cmd spindle.0.speed-out => limit_speed.in near_speed.in1
net spindle-speed-limited limit_speed.out => sim_spindle.velocity-cmd spindle_mass.in
net tool:change-loop iocontrol.0.tool-change => iocontrol.0.tool-changed
net tool:prep-loop iocontrol.0.tool-prepare => iocontrol.0.tool-prepared
# parameter values
setp J0_accel.tmax 0
setp J0_mux.tmax 0
setp J0_pid.do-pid-calcs.tmax 0
setp J0_switch.tmax 0
setp J0_vel.tmax 0
setp J1_accel.tmax 0
setp J1_mux.tmax 0
setp J1_pid.do-pid-calcs.tmax 0
setp J1_switch.tmax 0
setp J1_vel.tmax 0
setp J2_accel.tmax 0
setp J2_mux.tmax 0
setp J2_pid.do-pid-calcs.tmax 0
setp J2_switch.tmax 0
setp J2_vel.tmax 0
setp J3_accel.tmax 0
setp J3_mux.tmax 0
setp J3_pid.do-pid-calcs.tmax 0
setp J3_switch.tmax 0
setp J3_vel.tmax 0
setp J4_accel.tmax 0
setp J4_mux.tmax 0
setp J4_pid.do-pid-calcs.tmax 0
setp J4_switch.tmax 0
setp J4_vel.tmax 0
setp limit_speed.tmax 0
setp motion-command-handler.tmax 0
setp motion-controller.tmax 0
setp near_speed.difference 10
setp near_speed.scale 1.1
setp near_speed.tmax 0
setp rpm_rps.tmax 0
setp servo-thread.tmax 0
setp sim_spindle.scale 0.01666667
setp sim_spindle.tmax 0
setp spindle_mass.gain 0.07
setp spindle_mass.tmax 0
# realtime thread/function links
addf motion-command-handler servo-thread
addf motion-controller servo-thread
addf J0_pid.do-pid-calcs servo-thread
addf J1_pid.do-pid-calcs servo-thread
addf J2_pid.do-pid-calcs servo-thread
addf J3_pid.do-pid-calcs servo-thread
addf J4_pid.do-pid-calcs servo-thread
addf J0_mux servo-thread
addf J1_mux servo-thread
addf J2_mux servo-thread
addf J3_mux servo-thread
addf J4_mux servo-thread
addf J0_vel servo-thread
addf J0_accel servo-thread
addf J1_vel servo-thread
addf J1_accel servo-thread
addf J2_vel servo-thread
addf J2_accel servo-thread
addf J3_vel servo-thread
addf J3_accel servo-thread
addf J4_vel servo-thread
addf J4_accel servo-thread
addf J0_switch servo-thread
addf J1_switch servo-thread
addf J2_switch servo-thread
addf J3_switch servo-thread
addf J4_switch servo-thread
addf limit_speed servo-thread
addf spindle_mass servo-thread
addf rpm_rps servo-thread
addf near_speed servo-thread
addf sim_spindle servo-thread
# setp commands for unconnected input pins
setp J0_pid.FF0 1.0
setp J0_pid.Pgain 0
setp J0_pid.Dgain 0
setp J0_pid.Igain 0
setp J0_pid.FF1 0
setp J0_pid.FF2 0
setp J1_pid.FF0 1.0
setp J1_pid.Pgain 0
setp J1_pid.Dgain 0
setp J1_pid.Igain 0
setp J1_pid.FF1 0
setp J1_pid.FF2 0
setp J2_pid.FF0 1.0
setp J2_pid.Pgain 0
setp J2_pid.Dgain 0
setp J2_pid.Igain 0
setp J2_pid.FF1 0
setp J2_pid.FF2 0
setp J3_pid.FF0 1.0
setp J3_pid.Pgain 0
setp J3_pid.Dgain 0
setp J3_pid.Igain 0
setp J3_pid.FF1 0
setp J3_pid.FF2 0
setp J4_pid.FF0 1.0
setp J4_pid.Pgain 0
setp J4_pid.Dgain 0
setp J4_pid.Igain 0
setp J4_pid.FF1 0
setp J4_pid.FF2 0
setp sim_spindle.scale 0.01666667
setp limit_speed.maxv 5000.0
setp spindle_mass.gain .07
setp near_speed.scale 1.1
setp near_speed.difference 10

View File

@@ -0,0 +1,119 @@
5161 0.000000
5162 0.000000
5163 0.000000
5164 0.000000
5165 0.000000
5166 0.000000
5167 0.000000
5168 0.000000
5169 0.000000
5181 0.000000
5182 0.000000
5183 0.000000
5184 0.000000
5185 0.000000
5186 0.000000
5187 0.000000
5188 0.000000
5189 0.000000
5210 0.000000
5211 0.000000
5212 0.000000
5213 0.000000
5214 0.000000
5215 0.000000
5216 0.000000
5217 0.000000
5218 0.000000
5219 0.000000
5220 1.000000
5221 0.000000
5222 0.000000
5223 0.000000
5224 0.000000
5225 0.000000
5226 0.000000
5227 0.000000
5228 0.000000
5229 0.000000
5230 0.000000
5241 0.000000
5242 0.000000
5243 0.000000
5244 0.000000
5245 0.000000
5246 0.000000
5247 0.000000
5248 0.000000
5249 0.000000
5250 0.000000
5261 0.000000
5262 0.000000
5263 0.000000
5264 0.000000
5265 0.000000
5266 0.000000
5267 0.000000
5268 0.000000
5269 0.000000
5270 0.000000
5281 0.000000
5282 0.000000
5283 0.000000
5284 0.000000
5285 0.000000
5286 0.000000
5287 0.000000
5288 0.000000
5289 0.000000
5290 0.000000
5301 0.000000
5302 0.000000
5303 0.000000
5304 0.000000
5305 0.000000
5306 0.000000
5307 0.000000
5308 0.000000
5309 0.000000
5310 0.000000
5321 0.000000
5322 0.000000
5323 0.000000
5324 0.000000
5325 0.000000
5326 0.000000
5327 0.000000
5328 0.000000
5329 0.000000
5330 0.000000
5341 0.000000
5342 0.000000
5343 0.000000
5344 0.000000
5345 0.000000
5346 0.000000
5347 0.000000
5348 0.000000
5349 0.000000
5350 0.000000
5361 0.000000
5362 0.000000
5363 0.000000
5364 0.000000
5365 0.000000
5366 0.000000
5367 0.000000
5368 0.000000
5369 0.000000
5370 0.000000
5381 0.000000
5382 0.000000
5383 0.000000
5384 0.000000
5385 0.000000
5386 0.000000
5387 0.000000
5388 0.000000
5389 0.000000
5390 0.000000

View File

@@ -0,0 +1,119 @@
5161 0.000000
5162 0.000000
5163 0.000000
5164 0.000000
5165 0.000000
5166 0.000000
5167 0.000000
5168 0.000000
5169 0.000000
5181 0.000000
5182 0.000000
5183 0.000000
5184 0.000000
5185 0.000000
5186 0.000000
5187 0.000000
5188 0.000000
5189 0.000000
5210 0.000000
5211 0.000000
5212 0.000000
5213 0.000000
5214 0.000000
5215 0.000000
5216 0.000000
5217 0.000000
5218 0.000000
5219 0.000000
5220 1.000000
5221 0.000000
5222 0.000000
5223 0.000000
5224 0.000000
5225 0.000000
5226 0.000000
5227 0.000000
5228 0.000000
5229 0.000000
5230 0.000000
5241 0.000000
5242 0.000000
5243 0.000000
5244 0.000000
5245 0.000000
5246 0.000000
5247 0.000000
5248 0.000000
5249 0.000000
5250 0.000000
5261 0.000000
5262 0.000000
5263 0.000000
5264 0.000000
5265 0.000000
5266 0.000000
5267 0.000000
5268 0.000000
5269 0.000000
5270 0.000000
5281 0.000000
5282 0.000000
5283 0.000000
5284 0.000000
5285 0.000000
5286 0.000000
5287 0.000000
5288 0.000000
5289 0.000000
5290 0.000000
5301 0.000000
5302 0.000000
5303 0.000000
5304 0.000000
5305 0.000000
5306 0.000000
5307 0.000000
5308 0.000000
5309 0.000000
5310 0.000000
5321 0.000000
5322 0.000000
5323 0.000000
5324 0.000000
5325 0.000000
5326 0.000000
5327 0.000000
5328 0.000000
5329 0.000000
5330 0.000000
5341 0.000000
5342 0.000000
5343 0.000000
5344 0.000000
5345 0.000000
5346 0.000000
5347 0.000000
5348 0.000000
5349 0.000000
5350 0.000000
5361 0.000000
5362 0.000000
5363 0.000000
5364 0.000000
5365 0.000000
5366 0.000000
5367 0.000000
5368 0.000000
5369 0.000000
5370 0.000000
5381 0.000000
5382 0.000000
5383 0.000000
5384 0.000000
5385 0.000000
5386 0.000000
5387 0.000000
5388 0.000000
5389 0.000000
5390 0.000000

View File

@@ -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_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_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_icon_manifest.mjs && node ../tests/node/verify_gmoccapy_icon_registry.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && node ../tests/node/verify_linear_unit_conversion.mjs && node ../tests/node/verify_gmoccapy_trt_project_sidebar.mjs && node ../tests/node/verify_linuxcnc_parity_matrix.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_tool_db_web_simulation.mjs && node ../tests/node/verify_tool_db_user_m_simulation.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_gmoccapy_xyzac_trt_parity.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_icon_manifest.mjs && node ../tests/node/verify_gmoccapy_icon_registry.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && node ../tests/node/verify_linear_unit_conversion.mjs && node ../tests/node/verify_gmoccapy_trt_project_sidebar.mjs && node ../tests/node/verify_linuxcnc_parity_matrix.mjs"
},
"devDependencies": {
"playwright": "^1.61.1"

View File

@@ -64,10 +64,17 @@ async function copyLinuxCncConfigAssets() {
const configSrcDir = join(repoRoot, "wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting");
const configDistDir = join(distDir, "configs/sim/axis/vismach/5axis/table-rotary-tilting");
const vendorConfigDistDir = join(distDir, "wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting");
const gmoccapyConfigSrcDir = join(repoRoot, "wasm-port/vendor/linuxcnc/configs/sim/gmoccapy");
const gmoccapyConfigDistDir = join(distDir, "configs/sim/gmoccapy");
const gmoccapyVendorConfigDistDir = join(distDir, "wasm-port/vendor/linuxcnc/configs/sim/gmoccapy");
await mkdir(configDistDir, { recursive: true });
await mkdir(vendorConfigDistDir, { recursive: true });
await mkdir(gmoccapyConfigDistDir, { recursive: true });
await mkdir(gmoccapyVendorConfigDistDir, { recursive: true });
await cp(configSrcDir, configDistDir, { recursive: true });
await cp(configSrcDir, vendorConfigDistDir, { recursive: true });
await cp(gmoccapyConfigSrcDir, gmoccapyConfigDistDir, { recursive: true });
await cp(gmoccapyConfigSrcDir, gmoccapyVendorConfigDistDir, { recursive: true });
}
async function copyLinuxCncReferenceAssets() {

View File

@@ -62,6 +62,9 @@ window.webRtcp5AxisSimulation = {
saveSession: store.saveSession,
restoreSession: store.restoreSession,
stageMachineFiles: store.stageMachineFiles,
queryToolDb: store.queryToolDb,
editToolDb: store.editToolDb,
saveToolDb: store.saveToolDb,
runFullBoundaryAudit: store.runFullBoundaryAudit,
getRegions: shell.getRegions,
iniConfigReady,
@@ -126,6 +129,11 @@ async function ensureDefaultLinuxCncProgramPreview(store) {
function selectDefaultLinuxCncSource(state) {
const sources = state.machineFileStaging?.gcodeSources || [];
const profileDefault = state.profile?.machineFileStaging?.defaultProgramFilename;
if (profileDefault) {
const match = sources.find((source) => source.filename === profileDefault);
if (match) return match;
}
const preferredFilename = `${state.machineProfile}_switchkins.ngc`;
return sources.find((source) => source.filename === preferredFilename)
|| sources.find((source) => source.filename.includes(state.machineProfile))

View File

@@ -1,8 +1,14 @@
import { xyzacTrtProfile } from "./xyzac-trt.js";
import { xyzbcTrtProfile } from "./xyzbc-trt.js";
import { gmoccapyXyzacTrtProfile } from "./gmoccapy-xyzac-trt.js";
import { gmoccapyXyzabProfile } from "./gmoccapy-xyzab.js";
export const fiveAxisProfiles = [xyzacTrtProfile, xyzbcTrtProfile, gmoccapyXyzabProfile];
export const fiveAxisProfiles = [
xyzacTrtProfile,
xyzbcTrtProfile,
gmoccapyXyzacTrtProfile,
gmoccapyXyzabProfile,
];
export function getFiveAxisProfile(profileId = "xyzac-trt") {
const profile = fiveAxisProfiles.find((entry) => entry.id === profileId);

View File

@@ -31,6 +31,7 @@ export const xyzacTrtProfile = {
coordinates: ["X", "Y", "Z", "A", "C"],
joints: ["joint.0", "joint.1", "joint.2", "joint.3", "joint.4"],
kinematics: "xyzac-trt-kins",
kinematicsModuleId: "xyzac-trt",
kinematicsParameters: {
sparm: "identityfirst",
defaultSwitchkinsType: 0,

View File

@@ -0,0 +1,219 @@
const DEFAULT_ALLOWED_USER_M_CODES = {
M428: {
code: "M428",
label: "TCP kinematics",
kinsType: "tcp",
switchkinsType: 1,
rtcpState: "on",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
},
M429: {
code: "M429",
label: "Identity kinematics",
kinsType: "identity",
switchkinsType: 0,
rtcpState: "off",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
},
M430: {
code: "M430",
label: "User kinematics",
kinsType: "userk",
switchkinsType: 2,
rtcpState: "on",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
},
M128: {
code: "M128",
label: "controlled millturn mill-mode user-M",
kinsType: "mill",
switchkinsType: 0,
rtcpState: "off",
sourceRel: "linuxcnc millturn user-M reference",
},
M129: {
code: "M129",
label: "controlled millturn turn-mode user-M",
kinsType: "turn",
switchkinsType: 1,
rtcpState: "off",
sourceRel: "linuxcnc millturn user-M reference",
},
};
export function createControlledUserMSimulation({ allowedCodes = DEFAULT_ALLOWED_USER_M_CODES } = {}) {
return {
apiName: "web-rtcp-5axis-controlled-user-m-simulation",
ready: true,
processReady: true,
processScope: "web_simulation_only",
hostProcessReady: false,
hostProcessExecution: false,
arbitraryUserMExecution: false,
allowedCodes: Object.fromEntries(
Object.entries(allowedCodes).map(([code, definition]) => [normalizeMCode(code), {
...definition,
code: normalizeMCode(definition.code || code),
}]),
),
events: [],
blockedEvents: [],
semanticBoundary: "controlled_user_m_web_simulation_whitelist_not_host_process",
linuxCncReferences: [
"linuxcnc/src/emc/rs274ngc",
"linuxcnc/src/emc/task/emctask.cc",
"linuxcnc/src/emc/task/emccanon.cc",
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
],
};
}
export function runControlledUserM(simulation, code, context = {}) {
const state = cloneSimulation(simulation);
const normalizedCode = normalizeMCode(code);
const definition = state.allowedCodes[normalizedCode] || null;
if (!definition) {
const event = createUserMEvent({
code: normalizedCode,
allowed: false,
reason: "not_in_controlled_user_m_whitelist",
context,
});
state.blockedEvents.push(event);
return {
simulation: state,
event,
statePatch: {},
halPatch: {},
};
}
const profileKinsType = definition.kinsType === "tcp"
? tcpKinsTypeForProfile(context.profile)
: definition.kinsType;
const halPatch = {
"motion.switchkins-type": definition.switchkinsType,
"motion.analog-out-03": definition.switchkinsType,
};
const statePatch = {
kinsType: profileKinsType,
rtcpState: definition.rtcpState,
};
const event = createUserMEvent({
code: normalizedCode,
allowed: true,
definition,
context,
halPatch,
statePatch,
});
state.events.push(event);
return {
simulation: state,
event,
statePatch,
halPatch,
};
}
export function createControlledUserMReadiness(simulation) {
const ready = simulation?.processReady === true && simulation?.processScope === "web_simulation_only";
return {
apiName: "web-rtcp-5axis-controlled-user-m-readiness",
ready,
externalUserMProcessReady: ready,
externalUserMProcessScope: ready ? "web_simulation_only" : "not_ready",
hostExternalUserMProcessReady: false,
hostProcessExecution: false,
arbitraryUserMExecution: false,
allowedCodeCount: Object.keys(simulation?.allowedCodes || {}).length,
blockedEventCount: simulation?.blockedEvents?.length || 0,
semanticBoundary: "external_user_m_process_ready_for_web_simulation_only",
};
}
export function extractControlledUserMCodesFromProgram(programText = "") {
const matches = [];
const lines = String(programText).split(/\r?\n/);
lines.forEach((line, index) => {
const stripped = line.replace(/\([^)]*\)/g, " ");
for (const match of stripped.matchAll(/\bM\s*(\d{2,3})\b/gi)) {
const code = normalizeMCode(`M${match[1]}`);
if (Number(match[1]) >= 100 || ["M428", "M429", "M430"].includes(code)) {
matches.push({
code,
line: index + 1,
rawLine: line,
});
}
}
});
return matches;
}
export function runControlledUserMProgramScan(simulation, programText = "", context = {}) {
let state = simulation;
const events = [];
for (const occurrence of extractControlledUserMCodesFromProgram(programText)) {
const result = runControlledUserM(state, occurrence.code, {
...context,
line: occurrence.line,
rawLine: occurrence.rawLine,
});
state = result.simulation;
events.push(result.event);
}
return { simulation: state, events };
}
function createUserMEvent({
code,
allowed,
definition = null,
context = {},
halPatch = {},
statePatch = {},
reason = null,
}) {
return {
apiName: "web-rtcp-5axis-controlled-user-m-event",
code,
allowed,
reason,
label: definition?.label || null,
sourceRel: definition?.sourceRel || context.sourceRel || null,
line: context.line || null,
halPatch,
statePatch,
createdAt: new Date().toISOString(),
semanticBoundary: allowed
? "vendored_or_whitelisted_user_m_web_simulation_event"
: "arbitrary_external_user_m_blocked",
promotionScope: "web_simulation_only",
hostProcessExecution: false,
arbitraryUserMExecution: false,
};
}
function tcpKinsTypeForProfile(profile) {
if (profile?.id === "xyzbc-trt") return "tcp-xyzbc";
return "tcp-xyzac";
}
function normalizeMCode(code) {
const normalized = String(code || "").trim().toUpperCase().replace(/\s+/g, "");
const match = normalized.match(/^M0*(\d+)$/);
if (!match) return normalized;
return `M${Number(match[1])}`;
}
function cloneSimulation(simulation) {
return {
...simulation,
allowedCodes: { ...(simulation.allowedCodes || {}) },
events: [...(simulation.events || [])],
blockedEvents: [...(simulation.blockedEvents || [])],
};
}

View File

@@ -27,7 +27,10 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
&& state.machineFileStaging?.fileCount > 0;
const machineFileRemapReady = Boolean(
machineFileExecution?.sourceMode === "linuxcnc-machine-file-remap-wasm" &&
machineFileExecution?.summary?.machineFileExecutionReady === true &&
machineFileExecution?.summary?.machineFileExecutionReady === true,
);
const remapRuntimeReady = Boolean(
machineFileExecution?.summary?.remapRuntimeReady === true ||
MACHINE_FILE_FLAGS.every((flag) => machineFileText.includes(flag)),
);
const plannerRuntimeReady = Boolean(
@@ -55,6 +58,10 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
const taskHalComparisonReady = Boolean(taskHalSummary.taskHalComparisonReady === true);
const nativeTaskReady = taskRuntimeReady;
const nativeHalSyncReady = halRuntimeReady && motionRuntimeReady && halSyncReady;
const toolDbReadiness = state.toolDbReadiness || {};
const controlledUserMReadiness = state.controlledUserMReadiness || {};
const toolDbProcessReady = toolDbReadiness.toolDbProcessReady === true;
const externalUserMProcessReady = controlledUserMReadiness.externalUserMProcessReady === true;
const fullLinuxCncProgramExecutionReady = Boolean(
kinematicsReady &&
interpreterReady &&
@@ -64,7 +71,9 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
plannerRuntimeReady &&
nativeTaskReady &&
nativeHalSyncReady &&
taskHalComparisonReady
taskHalComparisonReady &&
toolDbProcessReady &&
externalUserMProcessReady
);
const satisfied = [
@@ -80,6 +89,8 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
halRuntimeReady ? "linuxcnc-hal-runtime" : null,
halSyncReady ? "task-motion-hal-sync" : null,
taskHalComparisonReady ? "task-hal-cycle-artifact" : null,
toolDbProcessReady ? "tool-db-web-simulation" : null,
externalUserMProcessReady ? "controlled-user-m-web-simulation" : null,
].filter(Boolean);
const missing = [];
@@ -95,11 +106,15 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
if (!halRuntimeReady) missing.push("LinuxCNC HAL runtime");
if (!halSyncReady) missing.push("task/motion/HAL synchronization");
if (!taskHalComparisonReady) missing.push("task cycle and HAL servo cycle artifact");
if (!toolDbProcessReady) missing.push("tool DB Web/WASM simulation process");
if (!externalUserMProcessReady) missing.push("controlled user-M Web/WASM simulation process");
const blockers = [];
if (!nativeTaskReady) blockers.push("LinuxCNC task runtime is not promoted");
if (!nativeHalSyncReady) blockers.push("realtime HAL synchronization is not promoted");
blockers.push("external user-M process and full tool DB process are not promoted");
if (!toolDbProcessReady) blockers.push("tool DB Web simulation process is not ready");
if (!externalUserMProcessReady) blockers.push("controlled user-M Web simulation process is not ready");
blockers.push("host external user-M process and host tool DB process remain disabled; Web simulation boundary only");
if (!plannerRuntimeReady) {
blockers.push("LinuxCNC trajectory planner queue is not promoted as browser runtime");
}
@@ -126,7 +141,7 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
: programExecution?.sourceMode || state.programExecutionSourceMode || "fixture-line-playback",
readyForUiSimulation: kinematicsReady && interpreterReady && canonicalProgramReady,
machineFileBackedRemapReady: machineFileRemapReady,
remapRuntimeReady: machineFileRemapReady,
remapRuntimeReady,
halSwitchkinsEvidenceReady,
plannerRuntimeReady,
taskRuntimeReady,
@@ -140,8 +155,13 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
promotionAllowed: fullLinuxCncProgramExecutionReady,
hardwareDrive: false,
hostRealtimeKernel: false,
externalUserMProcessReady: false,
toolDbProcessReady: false,
externalUserMProcessReady,
externalUserMProcessScope: externalUserMProcessReady ? "web_simulation_only" : "not_ready",
toolDbProcessReady,
toolDbProcessScope: toolDbProcessReady ? "web_simulation_only" : "not_ready",
hostExternalUserMProcessReady: false,
hostToolDbProcessReady: false,
arbitraryUserMExecution: false,
satisfied,
missing,
blockers,
@@ -153,12 +173,15 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
plannerTiming: plannerRuntimeReady ? programExecution.plannerTiming?.semanticBoundary : null,
machineFileFlags: MACHINE_FILE_FLAGS.filter((flag) => machineFileText.includes(flag)),
machineFileExecutionReady: machineFileExecution?.summary?.machineFileExecutionReady === true,
remapRuntimeReady,
stagedFileCount: state.machineFileStaging?.fileCount || 0,
taskHal: taskHalSummary,
taskCycle: state.taskHalStatus?.ui?.taskCycle || 0,
servoCycle: state.taskHalStatus?.ui?.servoCycle || 0,
motionQueueDepth: state.taskHalStatus?.ui?.motionQueueDepth || 0,
halChangedPinCount: state.taskHalStatus?.ui?.halChangedPinCount || 0,
toolDb: toolDbReadiness,
controlledUserM: controlledUserMReadiness,
},
};
}

View File

@@ -139,7 +139,9 @@ export function applyIniConfigToProfile(profile, iniConfig) {
...iniConfig.kinematicsParameters,
switchkinsTypes: mergeSwitchkinsTypes(
profile.kinematicsParameters?.switchkinsTypes || [],
iniConfig.kinematicsParameters.switchkinsTypes,
iniConfig.halui.mdiCommands.length > 0
? iniConfig.kinematicsParameters.switchkinsTypes
: [],
),
},
display: {
@@ -363,18 +365,25 @@ function validateIniConfig({
emcio,
}) {
const missing = [];
const displayName = getFirstValue(sections, "DISPLAY", "DISPLAY") || "";
const isSwitchkinsTrt = String(kinsText || "").includes("sparm=identityfirst");
const isGmoccapyFixedTrt = String(displayName).toLowerCase() === "gmoccapy"
&& /^xyz[ab]c-trt-kins\b/i.test(String(kinsText || ""))
&& !isSwitchkinsTrt;
const requiredSections = [
"EMC",
"DISPLAY",
"RS274NGC",
"KINS",
"HAL",
"HALUI",
"TRAJ",
"EMCMOT",
"TASK",
"EMCIO",
];
if (isSwitchkinsTrt) {
requiredSections.push("HALUI");
}
for (const section of requiredSections) {
if (!sections.has(section)) missing.push(`[${section}]`);
}
@@ -383,7 +392,7 @@ function validateIniConfig({
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")) {
if (!isSwitchkinsTrt && !isGmoccapyFixedTrt) {
missing.push("KINS.KINEMATICS sparm=identityfirst");
}
for (const axis of String(coordinates || "").split("")) {
@@ -395,20 +404,38 @@ function validateIniConfig({
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 (isSwitchkinsTrt) {
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");
} else if (isGmoccapyFixedTrt) {
for (const code of ["M6", "M61"]) {
if (!remaps.some((remap) => remap.code === code && remap.ngc)) {
missing.push(`RS274NGC.REMAP ${code}`);
}
}
} else {
if (rs274ngc.halPinVars !== true) missing.push("RS274NGC.HAL_PIN_VARS=1");
}
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"))) {
if (isSwitchkinsTrt && !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 (isGmoccapyFixedTrt) {
for (const token of ["xyzac-trt-gui", "xyzac-trt-kins.tool-offset", "xyzac-trt-kins.y-offset", "xyzac-trt-kins.z-offset"]) {
if (!hal.halcmd.some((line) => line.includes(token))) {
missing.push(`HAL.HALCMD ${token}`);
}
}
} else if (isSwitchkinsTrt && 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");

View File

@@ -129,10 +129,15 @@ function createProgramExecutionResult({
semanticBoundary = SEMANTIC_BOUNDARY,
plannerTiming = null,
}) {
const canonicalEventCount = String(resultText).split("\n").filter((line) => line.startsWith("canon_event=")).length;
const machineFileExecutionReady = Boolean(
machineFilePlan && FIVE_AXIS_REMAP_FLAGS.every((flag) => String(resultText).includes(flag)),
);
const resultTextString = String(resultText);
const canonicalEventCount = resultTextString.split("\n").filter((line) => line.startsWith("canon_event=")).length;
const remapRuntimeReady = Boolean(machineFilePlan && resultTextString.includes("fiveaxis_remaps_ready=1"));
const machineFileRunCompleted = Boolean(machineFilePlan && (
FIVE_AXIS_REMAP_FLAGS.every((flag) => resultTextString.includes(flag)) ||
resultTextString.includes("fiveaxis_file_reached_exit=1") ||
resultTextString.includes("fiveaxis_linuxcnc_remap_file_execute=0")
));
const machineFileExecutionReady = Boolean(machineFileRunCompleted && motion.length > 0);
const plannerRuntimeReady = plannerTiming?.plannerRuntimeReady === true
&& plannerTiming.motionCount === motion.length;
return {
@@ -167,7 +172,7 @@ function createProgramExecutionResult({
switchkinsEventCount: switchkinsEvents.length,
switchkinsCodes: [...new Set(switchkinsEvents.map((event) => event.code))],
switchkinsRemapBoundary: switchkinsEvents.length > 0 ? SWITCHKINS_REMAP_BOUNDARY : null,
remapRuntimeReady: machineFileExecutionReady,
remapRuntimeReady,
plannerRuntimeReady,
plannerSemanticBoundary: plannerRuntimeReady ? PLANNER_TIMING_BOUNDARY : null,
machineFileExecutionReady,
@@ -245,7 +250,7 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
let activeLinearUnits = "mm";
for (const line of String(resultText).split("\n")) {
const feedRate = readCanonicalNumber(line, "feed_rate");
const feedRate = readCanonicalFeedRate(line);
if (Number.isFinite(feedRate) && feedRate > 0) {
activeFeedRate = feedRate;
}
@@ -290,7 +295,8 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
const event = latestSwitchkinsEventAtOrBeforeLine(switchkinsByLine, sourceLine);
if (event) activeSwitchkinsEvent = event;
const sourceFeedRate = latestFeedRateAtOrBeforeLine(feedRatesByLine, sourceLine);
if (Number.isFinite(sourceFeedRate) && sourceFeedRate > 0) {
if ((!Number.isFinite(activeFeedRate) || activeFeedRate <= 0)
&& Number.isFinite(sourceFeedRate) && sourceFeedRate > 0) {
activeFeedRate = sourceFeedRate;
}
activeFeedMode = latestFeedModeAtOrBeforeLine(feedModesByLine, sourceLine) || activeFeedMode;
@@ -485,6 +491,17 @@ function readCanonicalNumber(line, field) {
return match ? Number(match[1]) : null;
}
function readCanonicalFeedRate(line) {
const text = String(line);
if (text.startsWith("canon_event=SET_FEED_RATE")) {
return readCanonicalNumber(text, "rate");
}
if (text.startsWith("canon_event=UPDATE_TAG")) {
return readCanonicalNumber(text, "feed");
}
return readCanonicalNumber(text, "feed_rate");
}
async function createDefaultModuleOptions({ wasmRoot }) {
const quietOptions = { print() {}, printErr() {} };
if (!isNodeRuntime()) return quietOptions;

View File

@@ -18,6 +18,8 @@ const DEFAULT_TEST_SOURCE_ROOT_URLS = [
];
const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting";
const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`;
const GMOCAPY_TRT_MACHINE_REL = "gmoccapy/non_trivial_kinematics/table-rotary-tilting";
const GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX = `configs/sim/${GMOCAPY_TRT_MACHINE_REL}/examples/`;
const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
let browserMemoryMachineFileStorage = null;
@@ -38,20 +40,26 @@ export async function createMachineFileStagingPlan({
);
const resolvedIniText = iniText ?? await readTextFromCandidateUrls(sourceUrlsFor(profile.iniPath));
const iniFile = basename(profile.iniPath);
const machineRel = machineRelForProfile(profile);
const demoDirectory = demoDirectoryForProfile(profile);
const plan = planSimConfigStaging({
manifestText: resolvedManifestText,
machineRel: TRT_MACHINE_REL,
machineRel,
iniFile,
iniText: resolvedIniText,
wasmDir: wasmDir || `/work/sim/${TRT_MACHINE_REL}/${profile.id}`,
wasmDir: wasmDir || profile.machineFileStaging?.wasmDir || `/work/sim/${machineRel}/${profile.id}`,
});
const files = addVendoredDemoSources(plan.files, resolvedManifestText, plan.wasmDir);
const files = addVendoredDemoSources(plan.files, resolvedManifestText, plan.wasmDir, {
sourcePrefix: sourcePrefixForProfile(profile),
demoDirectory,
});
return {
apiName: "web-rtcp-5axis-machine-file-staging-plan",
profileId: profile.id,
machineRel: TRT_MACHINE_REL,
machineRel,
demoDirectory,
iniPath: profile.iniPath,
wasmDir: plan.wasmDir,
wasmIniPath: plan.iniPath,
@@ -75,7 +83,7 @@ export async function createMachineFileStagingPlan({
export function listLinuxCncGcodeSources(save) {
return [...(save?.files || [])]
.filter((file) => file.kind === "demo" && isLinuxCncFiveAxisGcodeSourceRel(file.sourceRel))
.filter((file) => file.kind === "demo" && isLinuxCncFiveAxisGcodeSourceRel(file.sourceRel, save?.plan || save))
.map((file) => ({
sourceRel: file.sourceRel,
wasmPath: file.wasmPath,
@@ -107,7 +115,7 @@ export function listProjectGcodeFiles(save) {
}
export function selectMachineFileProgram(plan, save, sourceRel) {
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) {
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel, plan)) {
throw new Error(`5-axis G-code source must come from LinuxCNC source demos: ${sourceRel}`);
}
const selectedFile = (save?.files || []).find((file) => file.sourceRel === sourceRel);
@@ -163,7 +171,8 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
storageMode: storage.mode,
storageCapability: storage.capability,
files: savedFiles,
gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }),
plan,
gcodeSources: listLinuxCncGcodeSources({ ...plan, files: savedFiles }),
gcodeFiles: listProjectGcodeFiles({ files: savedFiles }),
summary: summarizeSavedFiles(savedFiles),
taskHalSession: {
@@ -279,7 +288,11 @@ function summarizePlan(files) {
const kinds = countKinds(files.map((file) => classifySourceRel(file.sourceRel)));
return {
fileCount: files.length,
requiredFileCount: files.filter((file) => file.sourceRel.endsWith(".ini") || file.sourceRel.includes("/demos/")).length,
requiredFileCount: files.filter((file) => (
file.sourceRel.endsWith(".ini")
|| file.sourceRel.includes("/demos/")
|| file.sourceRel.includes("/examples/")
)).length,
gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0),
remapFileCount: kinds.remap || 0,
demoFileCount: kinds.demo || 0,
@@ -303,25 +316,29 @@ function createTaskHalSession({ profileId, wasmDir, iniPath, programPath, files
};
}
function addVendoredDemoSources(files, manifestText, wasmDir) {
function addVendoredDemoSources(files, manifestText, wasmDir, {
sourcePrefix = TRT_DEMO_SOURCE_PREFIX,
demoDirectory = "demos",
} = {}) {
const bySourceRel = new Map(files.map((file) => [file.sourceRel, file]));
for (const sourceRel of String(manifestText).split(/\r?\n/)) {
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) continue;
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel, { gcodeSourcePrefix: sourcePrefix })) continue;
if (bySourceRel.has(sourceRel)) continue;
bySourceRel.set(sourceRel, {
sourceRel,
wasmPath: `${wasmDir}/demos/${basename(sourceRel)}`,
wasmPath: `${wasmDir}/${demoDirectory}/${basename(sourceRel)}`,
executable: false,
});
}
return [...bySourceRel.values()];
}
function isLinuxCncFiveAxisGcodeSourceRel(sourceRel) {
function isLinuxCncFiveAxisGcodeSourceRel(sourceRel, context = {}) {
const value = String(sourceRel || "");
return value.startsWith(TRT_DEMO_SOURCE_PREFIX)
const sourcePrefix = gcodeSourcePrefixForContext(context);
return value.startsWith(sourcePrefix)
&& value.endsWith(".ngc")
&& !value.slice(TRT_DEMO_SOURCE_PREFIX.length).includes("/");
&& !value.slice(sourcePrefix.length).includes("/");
}
function summarizeSavedFiles(files) {
@@ -347,12 +364,35 @@ function classifySourceRel(sourceRel) {
if (sourceRel.endsWith(".tbl")) return "toolTable";
if (sourceRel.endsWith(".hal")) return "hal";
if (sourceRel.includes("/remap_subs/")) return "remap";
if (sourceRel.includes("/demos/")) return "demo";
if (sourceRel.includes("/demos/") || sourceRel.includes("/examples/")) return "demo";
if (sourceRel.endsWith(".xml")) return "pyvcp";
if (sourceRel.endsWith(".var")) return "parameters";
return "asset";
}
function machineRelForProfile(profile = {}) {
return profile.machineFileStaging?.machineRel || TRT_MACHINE_REL;
}
function demoDirectoryForProfile(profile = {}) {
return profile.machineFileStaging?.demoDirectory || "demos";
}
function sourcePrefixForProfile(profile = {}) {
if (machineRelForProfile(profile) === GMOCAPY_TRT_MACHINE_REL) {
return GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX;
}
return `configs/sim/${machineRelForProfile(profile)}/${demoDirectoryForProfile(profile)}/`;
}
function gcodeSourcePrefixForContext(context = {}) {
if (context.gcodeSourcePrefix) return context.gcodeSourcePrefix;
if (context.machineRel === GMOCAPY_TRT_MACHINE_REL || context.demoDirectory === "examples") {
return GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX;
}
return TRT_DEMO_SOURCE_PREFIX;
}
function opfsPathFor(profileId, sourceRel) {
return `${OPFS_ROOT}/${assertPathSegment(profileId)}/${String(sourceRel).replaceAll("\\", "/")}`;
}

Some files were not shown because too many files have changed in this diff Show More