继续完成 web-rtcp-5axis-sim-plan
结论:完成 LinuxCNC kinematics WASM ABI 覆盖,并将 web-rtcp-5axis-sim-plan 的 RTCP frame/boundary adapter 接到 xyzac-trt kinematics SDK;Node、build、browser smoke 验证通过。
This commit is contained in:
315
web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html
Normal file
315
web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html
Normal file
@@ -0,0 +1,315 @@
|
||||
<!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>
|
||||
74
web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
Executable file
74
web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
CHROMIUM="${CHROMIUM:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}"
|
||||
|
||||
if [[ -z "$CHROMIUM" ]]; then
|
||||
echo "missing Chromium-compatible browser; set CHROMIUM=/path/to/browser" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
PORT_FILE="$TMP_DIR/port"
|
||||
SERVER_LOG="$TMP_DIR/server.log"
|
||||
CHROME_PROFILE="$TMP_DIR/chrome-profile"
|
||||
mkdir -p "$CHROME_PROFILE"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${SERVER_PID:-}" ]]; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
python3 - <<'PY' "$ROOT_DIR" "$PORT_FILE" >"$SERVER_LOG" 2>&1 &
|
||||
import functools
|
||||
import http.server
|
||||
import pathlib
|
||||
import socketserver
|
||||
import sys
|
||||
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
port_file = pathlib.Path(sys.argv[2])
|
||||
|
||||
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root))
|
||||
with socketserver.TCPServer(("127.0.0.1", 0), handler) as httpd:
|
||||
port_file.write_text(str(httpd.server_address[1]), encoding="ascii")
|
||||
httpd.serve_forever()
|
||||
PY
|
||||
SERVER_PID=$!
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
[[ -s "$PORT_FILE" ]] && break
|
||||
sleep 0.05
|
||||
done
|
||||
|
||||
if [[ ! -s "$PORT_FILE" ]]; then
|
||||
echo "gmoccapy shell browser smoke HTTP server did not start" >&2
|
||||
cat "$SERVER_LOG" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORT="$(cat "$PORT_FILE")"
|
||||
URL="http://127.0.0.1:$PORT/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html"
|
||||
OUT="$TMP_DIR/chromium-gmoccapy-shell.out"
|
||||
|
||||
"$CHROMIUM" \
|
||||
--headless=new \
|
||||
--disable-gpu \
|
||||
--no-sandbox \
|
||||
--user-data-dir="$CHROME_PROFILE" \
|
||||
--virtual-time-budget=10000 \
|
||||
--dump-dom \
|
||||
"$URL" >"$OUT" 2>&1
|
||||
|
||||
if ! grep -Fq "gmoccapy_shell_smoke=ok" "$OUT"; then
|
||||
echo "gmoccapy shell browser smoke failed" >&2
|
||||
sed -n '1,260p' "$OUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "gmoccapy_shell_smoke=ok"
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
|
||||
|
||||
const runtime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" });
|
||||
const readiness = runtime.readiness();
|
||||
|
||||
assert.equal(runtime.apiName, "web-rtcp-5axis-linuxcnc-kinematics-runtime");
|
||||
assert.equal(runtime.moduleId, "xyzac-trt");
|
||||
assert.equal(runtime.loaded, true);
|
||||
assert.equal(runtime.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(runtime.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||
assert.equal(runtime.wasmFile, "linuxcnc_xyzac_trt_kinematics.wasm");
|
||||
assert.equal(readiness.loaded, true);
|
||||
assert.equal(readiness.supportedModules.includes("xyzac-trt"), true);
|
||||
assert.equal(runtime.switchRc, 0);
|
||||
|
||||
const linuxCncKinematicsResult = runtime.frameForJoints([10, 20, 30, 25, 40]);
|
||||
assert.equal(linuxCncKinematicsResult.moduleId, "xyzac-trt");
|
||||
assert.equal(linuxCncKinematicsResult.forward.rc, 0);
|
||||
assert.equal(linuxCncKinematicsResult.inverse.rc, 0);
|
||||
assert.deepEqual(
|
||||
linuxCncKinematicsResult.inverse.joints.map((value) => Math.round(value * 1e6) / 1e6),
|
||||
[10, 20, 30, 25, 40],
|
||||
);
|
||||
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: { x: 10, y: 20, z: 30, a: 25, b: 0, c: 40 },
|
||||
activeLine: 777,
|
||||
kinsType: "tcp-xyzac",
|
||||
rtcpEnabled: true,
|
||||
linuxCncKinematicsResult,
|
||||
});
|
||||
|
||||
assert.equal(frame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(frame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||
assert.equal(frame.readiness.linuxCncKinematicsReady, true);
|
||||
assert.equal(frame.readiness.promotionAllowed, true);
|
||||
assert.equal(frame.readiness.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.equal(frame.kinematicsModuleId, "xyzac-trt");
|
||||
assert.equal(frame.kinematicsForwardRc, 0);
|
||||
assert.equal(frame.kinematicsInverseRc, 0);
|
||||
assert.equal(frame.jointPose[3].value, 25);
|
||||
assert.equal(frame.jointPose[4].value, 40);
|
||||
assert.equal(frame.tcpPose.x, linuxCncKinematicsResult.forward.pose.x);
|
||||
assert.equal(frame.tcpPose.y, linuxCncKinematicsResult.forward.pose.y);
|
||||
assert.equal(frame.tcpPose.z, linuxCncKinematicsResult.forward.pose.z);
|
||||
|
||||
console.log("linuxcnc_kinematics_runtime_smoke=ok");
|
||||
@@ -0,0 +1,93 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { xyzacTrtProfile } from "../../app/src/profiles/xyzac-trt.js";
|
||||
import { createPyvcpHalBindingSummary, xyzacTrtPyvcpPanelSchema } from "../../app/src/panel-schema/xyzac-trt-pyvcp.js";
|
||||
import { createLinuxCncBoundaryAdapter, createLinuxCncBoundaryReadiness } from "../../app/src/runtime/linuxcnc-boundary-adapter.js";
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { createProfileSourceReferenceSummary } from "../../app/src/profiles/source-reference-map.js";
|
||||
|
||||
assert.equal(xyzacTrtProfile.id, "xyzac-trt");
|
||||
assert.equal(xyzacTrtProfile.iniPath.endsWith("xyzac-trt.ini"), true);
|
||||
assert.deepEqual(xyzacTrtProfile.coordinates, ["X", "Y", "Z", "A", "C"]);
|
||||
assert.equal(xyzacTrtProfile.kinematics, "xyzac-trt-kins");
|
||||
assert.equal(xyzacTrtProfile.machineName, "sim-xyzac-trt-kins (switchkins)");
|
||||
assert.equal(xyzacTrtProfile.display.jogAxes.join(""), "XYZC");
|
||||
assert.equal(xyzacTrtProfile.rs274ngc.halPinVars, true);
|
||||
assert.equal(xyzacTrtProfile.traj.coordinates, "XYZAC");
|
||||
assert.equal(xyzacTrtProfile.axisLimits.A.max, 50);
|
||||
assert.equal(xyzacTrtProfile.jointConfig.length, 5);
|
||||
assert.equal(xyzacTrtProfile.halui.mdiCommands.join(","), "M429,M428,M430");
|
||||
assert.equal(xyzacTrtProfile.hal.halcmd.switchkinsSelectNet.target, "motion.switchkins-type");
|
||||
assert.equal(xyzacTrtProfile.hal.halcmd.feedbackNets.length, 5);
|
||||
assert.equal(xyzacTrtProfile.hal.halcmd.offsetNets.length, 4);
|
||||
assert.equal(xyzacTrtProfile.toolTable.toolCount, 10);
|
||||
assert.equal(xyzacTrtProfile.toolTable.tools[1].zOffset, 15);
|
||||
assert.equal(xyzacTrtProfile.kinematicsParameters.sparm, "identityfirst");
|
||||
assert.equal(xyzacTrtProfile.remaps.map((remap) => remap.code).join(","), "M428,M429,M430");
|
||||
assert.equal(xyzacTrtProfile.remaps.every((remap) => remap.analogOutputIndex === 3), true);
|
||||
assert.equal(xyzacTrtProfile.halPins.includes("motion.switchkins-type"), true);
|
||||
assert.equal(xyzacTrtProfile.halPins.includes("xyzac-trt-kins.tool-offset"), true);
|
||||
assert.equal(xyzacTrtProfile.promotionAllowed, false);
|
||||
assert.equal(xyzacTrtProfile.linuxCncKinematicsReady, false);
|
||||
|
||||
const sourceSummary = createProfileSourceReferenceSummary("xyzac-trt");
|
||||
assert.equal(sourceSummary.referenceCount >= 8, true);
|
||||
assert.equal(sourceSummary.kinds.includes("tool_table"), true);
|
||||
assert.equal(sourceSummary.sourceRequiredCount >= 3, true);
|
||||
assert.equal(sourceSummary.promotionAllowed, false);
|
||||
assert.equal(sourceSummary.semanticBoundary, "profile_source_map_only_not_runtime_proof");
|
||||
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("xyzac-trt.ini")));
|
||||
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("trtfuncs.c")));
|
||||
|
||||
const panelSummary = createPyvcpHalBindingSummary(xyzacTrtPyvcpPanelSchema);
|
||||
assert.equal(panelSummary.schemaId, "xyzac-trt-switchkins-pyvcp");
|
||||
assert.equal(panelSummary.controlCount, 5);
|
||||
assert.equal(panelSummary.buttonCount, 4);
|
||||
assert.deepEqual(panelSummary.mdiCommands, ["M429", "M428", "M430"]);
|
||||
assert.ok(panelSummary.halNets.some((net) => net.target === "halui.mdi-command-01"));
|
||||
assert.equal(panelSummary.promotionAllowed, false);
|
||||
|
||||
const adapter = createLinuxCncBoundaryAdapter();
|
||||
assert.equal(adapter.apiName, "web-rtcp-5axis-linuxcnc-boundary-adapter");
|
||||
assert.equal(adapter.profileId, "xyzac-trt");
|
||||
assert.equal(adapter.runtimeReady, false);
|
||||
assert.equal(adapter.kinematicsRuntimeReady, false);
|
||||
assert.equal(adapter.interpreterRuntimeReady, false);
|
||||
assert.equal(adapter.linuxCncKinematicsReady, false);
|
||||
assert.equal(adapter.promotionAllowed, false);
|
||||
assert.equal(adapter.profileSummary.coordinates, "XYZAC");
|
||||
assert.equal(adapter.profileSummary.jointCount, 5);
|
||||
assert.equal(adapter.profileSummary.mdiCommandCount, 3);
|
||||
assert.equal(adapter.profileSummary.toolCount, 10);
|
||||
assert.equal(adapter.profileSummary.feedbackNetCount, 5);
|
||||
assert.equal(adapter.adapterPoints.pyvcpSchemaId, "xyzac-trt-switchkins-pyvcp");
|
||||
assert.equal(adapter.adapterPoints.halPins.includes("halui.mdi-command-00"), true);
|
||||
|
||||
const readiness = createLinuxCncBoundaryReadiness(adapter);
|
||||
assert.equal(readiness.ready, false);
|
||||
assert.equal(readiness.linuxCncKinematicsReady, false);
|
||||
assert.equal(readiness.promotionAllowed, false);
|
||||
assert.ok(readiness.missing.includes("kinematics runtime"));
|
||||
assert.ok(readiness.missing.includes("interpreter/remap runtime"));
|
||||
|
||||
const runtime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" });
|
||||
const kinematicsAdapter = createLinuxCncBoundaryAdapter({
|
||||
runtime: {
|
||||
kinematicsWasm: runtime.readiness(),
|
||||
interpreterWasm: null,
|
||||
},
|
||||
});
|
||||
const kinematicsReadiness = createLinuxCncBoundaryReadiness(kinematicsAdapter);
|
||||
assert.equal(kinematicsAdapter.runtimeReady, false);
|
||||
assert.equal(kinematicsAdapter.kinematicsRuntimeReady, true);
|
||||
assert.equal(kinematicsAdapter.interpreterRuntimeReady, false);
|
||||
assert.equal(kinematicsAdapter.linuxCncKinematicsReady, true);
|
||||
assert.equal(kinematicsAdapter.promotionAllowed, true);
|
||||
assert.equal(kinematicsAdapter.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.equal(kinematicsAdapter.semanticBoundary, "linuxcnc_kinematics_wasm_runtime_connected");
|
||||
assert.equal(kinematicsReadiness.ready, true);
|
||||
assert.equal(kinematicsReadiness.linuxCncKinematicsReady, true);
|
||||
assert.equal(kinematicsReadiness.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.ok(kinematicsReadiness.missing.includes("interpreter/remap runtime"));
|
||||
|
||||
console.log("profile_boundary_smoke=ok");
|
||||
214
web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
Normal file
214
web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
Normal file
@@ -0,0 +1,214 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
const identityFrame = buildRtcpFrame({
|
||||
axisPose: { x: 43, y: -32.15, z: -11.306, a: 0, b: 0, c: 0 },
|
||||
activeLine: 501,
|
||||
kinsType: "identity",
|
||||
rtcpEnabled: false,
|
||||
});
|
||||
|
||||
assert.equal(identityFrame.apiName, "web-rtcp-5axis-motion-frame");
|
||||
assert.equal(identityFrame.rtcpState, "off");
|
||||
assert.equal(identityFrame.readiness.frameReady, true);
|
||||
assert.equal(identityFrame.readiness.linuxCncKinematicsReady, false);
|
||||
assert.equal(identityFrame.readiness.promotionAllowed, false);
|
||||
assert.equal(identityFrame.semanticBoundary, "fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof");
|
||||
assert.deepEqual(identityFrame.compensation, { x: 0, y: 0, z: 0 });
|
||||
|
||||
const tcpFrame = buildRtcpFrame({
|
||||
axisPose: { x: 43, y: -32.15, z: -11.306, a: 15, b: 0, c: 30 },
|
||||
activeLine: 502,
|
||||
kinsType: "tcp-xyzac",
|
||||
rtcpEnabled: true,
|
||||
});
|
||||
|
||||
assert.equal(tcpFrame.rtcpState, "on");
|
||||
assert.equal(tcpFrame.kinsType, "tcp-xyzac");
|
||||
assert.notEqual(tcpFrame.compensation.z, 0);
|
||||
assert.ok(Number.isFinite(tcpFrame.tcpPose.x));
|
||||
assert.ok(Number.isFinite(tcpFrame.toolAxisVector.z));
|
||||
assert.equal(tcpFrame.jointPose.length, 5);
|
||||
|
||||
const runtime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" });
|
||||
const linuxCncKinematicsResult = runtime.frameForJoints([10, 20, 30, 25, 40]);
|
||||
const linuxCncFrame = buildRtcpFrame({
|
||||
axisPose: { x: 10, y: 20, z: 30, a: 25, b: 0, c: 40 },
|
||||
activeLine: 503,
|
||||
kinsType: "tcp-xyzac",
|
||||
rtcpEnabled: true,
|
||||
linuxCncKinematicsResult,
|
||||
});
|
||||
assert.equal(linuxCncFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(linuxCncFrame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||
assert.equal(linuxCncFrame.readiness.linuxCncKinematicsReady, true);
|
||||
assert.equal(linuxCncFrame.readiness.promotionAllowed, true);
|
||||
assert.equal(linuxCncFrame.readiness.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.equal(linuxCncFrame.kinematicsModuleId, "xyzac-trt");
|
||||
assert.equal(linuxCncFrame.jointPose[3].value, 25);
|
||||
assert.equal(linuxCncFrame.jointPose[4].value, 40);
|
||||
|
||||
const fixtureInitialStore = createSimulationStore();
|
||||
assert.equal(fixtureInitialStore.getState().rtcpState, "off");
|
||||
assert.equal(fixtureInitialStore.getState().machine.powerOn, false);
|
||||
assert.equal(fixtureInitialStore.getState().machine.mode, "manual");
|
||||
assert.equal(
|
||||
fixtureInitialStore.getState().linuxCncBoundaryAdapter.apiName,
|
||||
"web-rtcp-5axis-linuxcnc-boundary-adapter",
|
||||
);
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.panelSummary.schemaId, "xyzac-trt-switchkins-pyvcp");
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.sourceSummary.referenceCount >= 8, true);
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.profileSummary.toolCount, 10);
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.profileSummary.coordinates, "XYZAC");
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.ready, false);
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.promotionAllowed, false);
|
||||
|
||||
const kinematicsStore = createSimulationStore();
|
||||
kinematicsStore.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime });
|
||||
let kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.linuxCncBoundaryAdapter.linuxCncKinematicsReady, true);
|
||||
assert.equal(kinematicsState.linuxCncBoundaryAdapter.interpreterRuntimeReady, false);
|
||||
assert.equal(kinematicsState.linuxCncBoundaryReadiness.ready, true);
|
||||
assert.equal(kinematicsState.linuxCncBoundaryReadiness.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.equal(kinematicsState.linuxCncBoundaryReadiness.missing.includes("interpreter/remap runtime"), true);
|
||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(kinematicsState.rtcpFrame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, true);
|
||||
|
||||
kinematicsStore.dispatch({ type: "SET_RTCP", enabled: true });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.rtcpState, "on");
|
||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(kinematicsState.lastKinematicsResult.moduleId, "xyzac-trt");
|
||||
assert.equal(kinematicsState.dro.tcpX, kinematicsState.rtcpFrame.tcpPose.x);
|
||||
|
||||
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
|
||||
kinematicsStore.dispatch({ type: "STEP" });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.runState, "stepping");
|
||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(kinematicsState.rtcpFrame.readiness.fullLinuxCncProgramExecutionReady, false);
|
||||
|
||||
kinematicsStore.dispatch({ type: "SET_FRAME_SOURCE", sourceMode: "fixture-ui-only" });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "fixture-ui-only");
|
||||
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, false);
|
||||
|
||||
const store = createSimulationStore();
|
||||
store.dispatch({ type: "RUN" });
|
||||
assert.equal(store.getState().activeLine, 501);
|
||||
assert.equal(store.getState().operatorMessage, "run blocked: power or estop state");
|
||||
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
assert.equal(store.getState().machine.powerOn, true);
|
||||
|
||||
store.dispatch({ type: "SET_RTCP", enabled: true });
|
||||
let state = store.getState();
|
||||
assert.equal(state.rtcpState, "on");
|
||||
assert.equal(state.kinsType, "tcp-xyzac");
|
||||
assert.equal(state.rtcpFrame.readiness.linuxCncKinematicsReady, false);
|
||||
assert.notEqual(state.dro.tcpZ, state.dro.z);
|
||||
|
||||
const previousLine = state.activeLine;
|
||||
const previousTcpX = state.tcpPose.x;
|
||||
store.dispatch({ type: "STEP" });
|
||||
state = store.getState();
|
||||
assert.equal(state.activeLine, previousLine + 1);
|
||||
assert.equal(state.runState, "stepping");
|
||||
assert.notEqual(state.tcpPose.x, previousTcpX);
|
||||
|
||||
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 2 });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.mode, "jog");
|
||||
assert.equal(state.runState, "jogging");
|
||||
assert.equal(Math.round(state.axisPose.x), Math.round(state.rtcpFrame.axisPose.x));
|
||||
|
||||
store.dispatch({ type: "RUN_MDI", command: "G0 X1" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.mode, "mdi");
|
||||
assert.equal(state.machine.mdiCommand, "G0 X1");
|
||||
assert.equal(state.runState, "mdi");
|
||||
|
||||
store.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "operator-demo.ngc",
|
||||
content: "G0 X0 Y0\nG1 X10 F100\nM30\n",
|
||||
});
|
||||
state = store.getState();
|
||||
assert.equal(state.activeProgram, "operator-demo.ngc");
|
||||
assert.equal(state.programSource, "operator-file");
|
||||
assert.equal(state.programStartLine, 1);
|
||||
assert.equal(state.activeLine, 1);
|
||||
assert.equal(state.programLines.length, 3);
|
||||
assert.equal(state.machine.mode, "auto");
|
||||
|
||||
store.dispatch({ type: "RUN" });
|
||||
state = store.getState();
|
||||
assert.equal(state.activeLine, 3);
|
||||
assert.equal(state.runState, "complete");
|
||||
|
||||
store.dispatch({ type: "SET_RTCP", enabled: false });
|
||||
state = store.getState();
|
||||
assert.equal(state.rtcpState, "off");
|
||||
assert.equal(state.kinsType, "identity");
|
||||
assert.equal(state.dro.tcpX, state.dro.x);
|
||||
|
||||
store.dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: -10 });
|
||||
state = store.getState();
|
||||
assert.equal(state.feed.feedOverride, 90);
|
||||
assert.equal(state.operatorMessage, "feed override adjusted");
|
||||
|
||||
store.dispatch({ type: "ADJUST_OVERRIDE", target: "rapid", delta: 20 });
|
||||
state = store.getState();
|
||||
assert.equal(state.feed.rapidOverride, 120);
|
||||
|
||||
store.dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: 10 });
|
||||
state = store.getState();
|
||||
assert.equal(state.spindle.override, 110);
|
||||
|
||||
store.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
|
||||
state = store.getState();
|
||||
assert.equal(state.coolant.flood, false);
|
||||
|
||||
store.dispatch({ type: "TOGGLE_COOLANT", kind: "mist" });
|
||||
state = store.getState();
|
||||
assert.equal(state.coolant.mist, true);
|
||||
|
||||
store.dispatch({ type: "SET_VIEW", view: "x" });
|
||||
state = store.getState();
|
||||
assert.equal(state.preview.selectedView, "x");
|
||||
|
||||
store.dispatch({ type: "CLEAR_PREVIEW" });
|
||||
state = store.getState();
|
||||
assert.equal(state.preview.pathPoints, 0);
|
||||
|
||||
store.dispatch({ type: "RELOAD_PROGRAM" });
|
||||
state = store.getState();
|
||||
assert.equal(state.preview.pathPoints, 3);
|
||||
assert.equal(state.activeLine, 1);
|
||||
assert.equal(state.runState, "idle");
|
||||
|
||||
store.dispatch({ type: "TOGGLE_FULLSCREEN" });
|
||||
state = store.getState();
|
||||
assert.equal(state.preview.fullscreen, true);
|
||||
|
||||
store.dispatch({ type: "HOME" });
|
||||
state = store.getState();
|
||||
assert.equal(state.axisPose.x, 43);
|
||||
assert.equal(state.operatorMessage, "machine homed to fixture origin");
|
||||
|
||||
store.dispatch({ type: "ESTOP" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.powerOn, false);
|
||||
assert.equal(state.machine.estopActive, true);
|
||||
assert.equal(state.runState, "estopped");
|
||||
|
||||
store.dispatch({ type: "RESET" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.estopActive, false);
|
||||
assert.equal(state.operatorMessage, "machine reset complete");
|
||||
|
||||
console.log("rtcp_store_smoke=ok");
|
||||
Reference in New Issue
Block a user