结论:完成 LinuxCNC kinematics WASM ABI 覆盖,并将 web-rtcp-5axis-sim-plan 的 RTCP frame/boundary adapter 接到 xyzac-trt kinematics SDK;Node、build、browser smoke 验证通过。
316 lines
14 KiB
HTML
316 lines
14 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>gmoccapy shell browser smoke</title>
|
|
</head>
|
|
<body>
|
|
<pre id="result">gmoccapy_shell_smoke=pending</pre>
|
|
<iframe id="app-frame" src="../../app/index.html" title="gmoccapy shell"></iframe>
|
|
<script type="module">
|
|
const result = document.querySelector("#result");
|
|
const frame = document.querySelector("#app-frame");
|
|
|
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
async function runSmoke() {
|
|
await new Promise((resolve, reject) => {
|
|
frame.addEventListener("load", resolve, { once: true });
|
|
frame.addEventListener("error", reject, { once: true });
|
|
});
|
|
await wait(250);
|
|
|
|
const doc = frame.contentDocument;
|
|
const win = frame.contentWindow;
|
|
const regions = [
|
|
"titlebar",
|
|
"preview",
|
|
"dro",
|
|
"gcode",
|
|
"status-sidebar",
|
|
"info-tabs",
|
|
"override",
|
|
"spindle-coolant",
|
|
"bottom-controls",
|
|
];
|
|
|
|
for (const region of regions) {
|
|
const element = doc.querySelector(`[data-region="${region}"]`);
|
|
if (!element) {
|
|
throw new Error(`missing region: ${region}`);
|
|
}
|
|
if (!element.textContent.trim() && region !== "preview") {
|
|
throw new Error(`empty region: ${region}`);
|
|
}
|
|
}
|
|
|
|
if (!doc.querySelector(".machine-preview")) {
|
|
throw new Error("missing machine preview");
|
|
}
|
|
let canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
if (!canvas) {
|
|
throw new Error("missing Three.js preview canvas");
|
|
}
|
|
if (
|
|
canvas.dataset.threeReady !== "true" ||
|
|
canvas.dataset.threeFrameApi !== "web-rtcp-5axis-motion-frame" ||
|
|
Number(canvas.dataset.threePathPoints ?? 0) < 64 ||
|
|
Number(canvas.dataset.threeSceneObjects ?? 0) < 12 ||
|
|
!canvas.dataset.threeToolhead ||
|
|
!canvas.dataset.threeToolAxis ||
|
|
!canvas.dataset.threeTcpPose
|
|
) {
|
|
throw new Error(`Three.js preview did not expose ready render state: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
assertCanvasNonblank(canvas, "initial Three.js preview");
|
|
if (!doc.querySelector(".dro-row")) {
|
|
throw new Error("missing DRO rows");
|
|
}
|
|
if (!doc.querySelector(".gcode-row.active")) {
|
|
throw new Error("missing active gcode row");
|
|
}
|
|
if (!win.webRtcp5AxisSimulation) {
|
|
throw new Error("missing public simulation API");
|
|
}
|
|
if (win.React || win.Vue || win.angular || win.Svelte) {
|
|
throw new Error("forbidden frontend framework global detected");
|
|
}
|
|
if (doc.querySelector("[data-reactroot], [data-v-app], [ng-version], [svelte]")) {
|
|
throw new Error("forbidden frontend framework DOM marker detected");
|
|
}
|
|
const packageJson = await fetch("../../app/package.json").then((response) => response.json());
|
|
const dependencyNames = [
|
|
...Object.keys(packageJson.dependencies || {}),
|
|
...Object.keys(packageJson.devDependencies || {}),
|
|
];
|
|
const forbiddenDependencies = ["react", "vue", "@angular/core", "svelte"];
|
|
const forbidden = dependencyNames.filter((name) => forbiddenDependencies.includes(name));
|
|
if (forbidden.length > 0) {
|
|
throw new Error(`forbidden frontend framework dependency detected: ${forbidden.join(", ")}`);
|
|
}
|
|
|
|
const state = win.webRtcp5AxisSimulation.getState();
|
|
if (state.sourceMode !== "fixture-ui-only") {
|
|
throw new Error(`unexpected source mode: ${state.sourceMode}`);
|
|
}
|
|
if (state.machineProfile !== "xyzac-trt") {
|
|
throw new Error(`unexpected profile: ${state.machineProfile}`);
|
|
}
|
|
if (state.rtcpFrame?.apiName !== "web-rtcp-5axis-motion-frame") {
|
|
throw new Error("missing RTCP frame state");
|
|
}
|
|
if (state.rtcpFrame?.readiness?.linuxCncKinematicsReady !== false) {
|
|
throw new Error("fixture RTCP frame must not claim LinuxCNC kinematics readiness");
|
|
}
|
|
if (!doc.querySelector('[data-rtcp-value="tcp"]')?.textContent.includes("TCP")) {
|
|
throw new Error("missing TCP DRO strip");
|
|
}
|
|
if (!doc.querySelector('[data-rtcp-diagnostic="boundary"]')?.textContent.includes("fixture_frame_ui_plumbing")) {
|
|
throw new Error("missing RTCP boundary diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-tool-preview="summary"]')?.textContent.includes("T1")) {
|
|
throw new Error("missing tool preview summary");
|
|
}
|
|
if (!doc.querySelector('[data-action="OPEN_FILE"]')) {
|
|
throw new Error("missing G-code file input");
|
|
}
|
|
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power off")) {
|
|
throw new Error("initial machine state should show power off");
|
|
}
|
|
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
|
|
await wait(50);
|
|
if (!win.webRtcp5AxisSimulation.getState().operatorMessage.includes("blocked")) {
|
|
throw new Error("RUN should be blocked before power on");
|
|
}
|
|
doc.querySelector('[data-action="power"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().machine.powerOn !== true) {
|
|
throw new Error("POWER action did not turn machine on");
|
|
}
|
|
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power on")) {
|
|
throw new Error("machine state did not render power on");
|
|
}
|
|
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "LOAD_PROGRAM",
|
|
filename: "operator-demo.ngc",
|
|
content: [
|
|
"G0 X0 Y0 Z0",
|
|
"G1 X10 F100",
|
|
"G1 Y10",
|
|
"G1 X0",
|
|
"G1 Y0",
|
|
"G0 Z5",
|
|
"M5",
|
|
"M30",
|
|
].join("\n"),
|
|
});
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().activeProgram !== "operator-demo.ngc") {
|
|
throw new Error("LOAD_PROGRAM did not update active program");
|
|
}
|
|
if (doc.querySelector('[data-active-program-line]')?.textContent !== "Current line 1") {
|
|
throw new Error("loaded program did not render current line 1");
|
|
}
|
|
if (doc.querySelector(".gcode-row.active")?.dataset.programLine !== "1") {
|
|
throw new Error("loaded program active row should be line 1");
|
|
}
|
|
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
|
|
throw new Error("RUN action did not update state after power on");
|
|
}
|
|
if (doc.querySelector(".gcode-row.active")?.dataset.programLine !== "6") {
|
|
throw new Error("RUN did not highlight the executing current line");
|
|
}
|
|
const tcpButton = doc.querySelector('[data-action="kins-tcp"]');
|
|
tcpButton.click();
|
|
await wait(50);
|
|
const rtcpState = win.webRtcp5AxisSimulation.getState();
|
|
if (rtcpState.rtcpState !== "on" || rtcpState.kinsType !== "tcp-xyzac") {
|
|
throw new Error(`TCP mode did not enable RTCP: ${rtcpState.rtcpState}/${rtcpState.kinsType}`);
|
|
}
|
|
if (!doc.querySelector('[data-rtcp-value="state"]')?.textContent.includes("RTCP on")) {
|
|
throw new Error("RTCP DRO strip did not render enabled state");
|
|
}
|
|
if (!doc.querySelector('[data-rtcp-diagnostic="frame"]')?.textContent.includes("on")) {
|
|
throw new Error("RTCP diagnostics did not render enabled frame");
|
|
}
|
|
if (canvas.dataset.threeRtcpState !== "on") {
|
|
throw new Error("Three.js preview did not consume RTCP enabled frame");
|
|
}
|
|
if (doc.querySelector('[data-rtcp-diagnostic="kinematics-ready"]')?.textContent !== "pending") {
|
|
throw new Error("RTCP diagnostics must keep LinuxCNC kinematics pending for fixture mode");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-boundary="adapter"]')?.textContent.includes("linuxcnc-boundary-adapter")) {
|
|
throw new Error("missing LinuxCNC boundary adapter diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-boundary="panel"]')?.textContent.includes("xyzac-trt-switchkins-pyvcp")) {
|
|
throw new Error("missing PyVCP panel schema diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-boundary="profile-summary"]')?.textContent.includes("XYZAC / 5 joints / 10 tools")) {
|
|
throw new Error("missing LinuxCNC profile summary diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-boundary="readiness"]')?.textContent.includes("blocked")) {
|
|
throw new Error("LinuxCNC boundary readiness should remain blocked");
|
|
}
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
const tcpPoseBeforeStep = canvas.dataset.threeTcpPose;
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "STEP" });
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().activeLine <= rtcpState.activeLine) {
|
|
throw new Error("STEP did not advance RTCP frame line");
|
|
}
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
if (canvas.dataset.threeTcpPose === tcpPoseBeforeStep) {
|
|
throw new Error("Three.js preview did not update TCP pose after STEP");
|
|
}
|
|
assertCanvasNonblank(canvas, "updated Three.js preview");
|
|
doc.querySelector('[data-action="mode-jog"]').click();
|
|
await wait(50);
|
|
const xBeforeJog = win.webRtcp5AxisSimulation.getState().axisPose.x;
|
|
doc.querySelector('[data-action="JOG_X_POS"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().axisPose.x <= xBeforeJog) {
|
|
throw new Error("JOG X+ did not move the axis");
|
|
}
|
|
doc.querySelector('[data-action="mode-mdi"]').click();
|
|
doc.querySelector('[data-action="MDI_RUN"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "mdi") {
|
|
throw new Error("MDI action did not update run state");
|
|
}
|
|
doc.querySelector('[data-action="reset"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "idle") {
|
|
throw new Error("RESET did not return to idle");
|
|
}
|
|
doc.querySelector('[data-action="feed-override-down"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().feed.feedOverride !== 90) {
|
|
throw new Error("feed override button did not update state");
|
|
}
|
|
if (doc.querySelector('[data-value="feed-override"]')?.textContent.trim() !== "90 %") {
|
|
throw new Error("feed override DOM did not update");
|
|
}
|
|
doc.querySelector('[data-action="rapid-override-up"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().feed.rapidOverride !== 110) {
|
|
throw new Error("rapid override button did not update state");
|
|
}
|
|
doc.querySelector('[data-action="spindle-override-up"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().spindle.override !== 110) {
|
|
throw new Error("spindle override button did not update state");
|
|
}
|
|
doc.querySelector('[data-action="toggle-flood"]').click();
|
|
doc.querySelector('[data-action="toggle-mist"]').click();
|
|
await wait(50);
|
|
const coolantState = win.webRtcp5AxisSimulation.getState().coolant;
|
|
if (coolantState.flood !== false || coolantState.mist !== true) {
|
|
throw new Error("coolant buttons did not update state");
|
|
}
|
|
doc.querySelector('[data-action="view-x"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().preview.selectedView !== "x") {
|
|
throw new Error("preview view button did not update state");
|
|
}
|
|
doc.querySelector('[data-action="clear-preview"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().preview.pathPoints !== 0) {
|
|
throw new Error("clear preview button did not update path points");
|
|
}
|
|
doc.querySelector('[data-action="RELOAD"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().preview.pathPoints !== 8) {
|
|
throw new Error("reload button did not restore path points");
|
|
}
|
|
doc.querySelector('[data-action="FULL"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().preview.fullscreen !== true) {
|
|
throw new Error("fullscreen button did not update state");
|
|
}
|
|
doc.querySelector('[data-action="HOME"]').click();
|
|
await wait(50);
|
|
const homedState = win.webRtcp5AxisSimulation.getState();
|
|
if (homedState.axisPose.x !== 43) {
|
|
throw new Error("home button did not restore fixture origin");
|
|
}
|
|
doc.querySelector('[data-action="estop"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "estopped") {
|
|
throw new Error("E-STOP did not update run state");
|
|
}
|
|
|
|
result.textContent = "gmoccapy_shell_smoke=ok";
|
|
}
|
|
|
|
runSmoke().catch((error) => {
|
|
result.textContent = `gmoccapy_shell_smoke=fail ${error.message}`;
|
|
});
|
|
|
|
function assertCanvasNonblank(canvas, context) {
|
|
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
|
if (!gl) {
|
|
throw new Error(`${context}: missing WebGL context`);
|
|
}
|
|
const pixel = new Uint8Array(4);
|
|
gl.readPixels(
|
|
Math.floor(canvas.width / 2),
|
|
Math.floor(canvas.height / 2),
|
|
1,
|
|
1,
|
|
gl.RGBA,
|
|
gl.UNSIGNED_BYTE,
|
|
pixel,
|
|
);
|
|
if (pixel[0] === 0 && pixel[1] === 0 && pixel[2] === 0 && pixel[3] === 0) {
|
|
throw new Error(`${context}: center pixel was blank`);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|