完善 xyzbc-trt 刀具偏置闭环
This commit is contained in:
78
web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/verify_xyzbc_trt_browser.sh
Executable file
78
web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/verify_xyzbc_trt_browser.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
PROJECT_DIR="$ROOT_DIR/web-rtcp-5axis-xyzbc-trt-sim-plan"
|
||||
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
|
||||
|
||||
npm --prefix "$PROJECT_DIR/app" run build >/dev/null
|
||||
|
||||
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 "xyzbc-trt browser smoke HTTP server did not start" >&2
|
||||
cat "$SERVER_LOG" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORT="$(cat "$PORT_FILE")"
|
||||
APP_PATH="../../app/dist/index.html"
|
||||
URL="http://127.0.0.1:$PORT/web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/xyzbc_trt_browser_smoke.html?app=$APP_PATH"
|
||||
OUT="$TMP_DIR/chromium-xyzbc-trt.out"
|
||||
|
||||
"$CHROMIUM" \
|
||||
--headless=new \
|
||||
--disable-gpu \
|
||||
--no-sandbox \
|
||||
--user-data-dir="$CHROME_PROFILE" \
|
||||
--virtual-time-budget=15000 \
|
||||
--dump-dom \
|
||||
"$URL" >"$OUT" 2>&1
|
||||
|
||||
if ! grep -Fq "xyzbc_trt_browser_smoke=ok" "$OUT"; then
|
||||
echo "xyzbc-trt browser smoke failed" >&2
|
||||
sed -n '1,260p' "$OUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "xyzbc_trt_browser_smoke=ok"
|
||||
@@ -0,0 +1,180 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>xyzbc-trt browser smoke</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="result">xyzbc_trt_browser_smoke=pending</pre>
|
||||
<iframe id="app-frame" title="xyzbc-trt app"></iframe>
|
||||
<script type="module">
|
||||
const result = document.querySelector("#result");
|
||||
const frame = document.querySelector("#app-frame");
|
||||
const appSrc = new URLSearchParams(window.location.search).get("app") || "../../app/index.html";
|
||||
frame.src = appSrc;
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
const win = frame.contentWindow;
|
||||
const doc = frame.contentDocument;
|
||||
const runtimeErrors = [];
|
||||
win.addEventListener("error", (event) => {
|
||||
runtimeErrors.push(event.message || String(event.error || "window error"));
|
||||
});
|
||||
win.addEventListener("unhandledrejection", (event) => {
|
||||
runtimeErrors.push(event.reason?.message || String(event.reason || "unhandled rejection"));
|
||||
});
|
||||
|
||||
await waitFor(() => win.webRtcp5AxisSimulation, "public simulation API");
|
||||
const api = win.webRtcp5AxisSimulation;
|
||||
const [kinematics, interpreter, taskHal] = await Promise.all([
|
||||
api.kinematicsRuntimeReady,
|
||||
api.interpreterRuntimeReady,
|
||||
api.taskHalRuntimeReady,
|
||||
]);
|
||||
|
||||
assertRuntime(kinematics, {
|
||||
name: "kinematics",
|
||||
moduleId: "xyzbc-trt",
|
||||
required: ["loaded"],
|
||||
});
|
||||
if (kinematics.executionContext !== "worker") {
|
||||
throw new Error(`kinematics runtime should run in worker: ${JSON.stringify(kinematics)}`);
|
||||
}
|
||||
assertRuntime(interpreter, {
|
||||
name: "interpreter",
|
||||
required: ["loaded", "runProgramReady", "plannerRuntimeReady"],
|
||||
});
|
||||
if (interpreter.executionContext !== "worker") {
|
||||
throw new Error(`interpreter runtime should run in worker: ${JSON.stringify(interpreter)}`);
|
||||
}
|
||||
assertRuntime(taskHal, {
|
||||
name: "task/HAL",
|
||||
required: ["loaded", "taskRuntimeReady", "motionRuntimeReady", "halRuntimeReady"],
|
||||
});
|
||||
if (taskHal.executionContext !== "worker") {
|
||||
throw new Error(`task/HAL runtime should run in worker: ${JSON.stringify(taskHal)}`);
|
||||
}
|
||||
|
||||
await api.machineFileSeedReady;
|
||||
await waitFor(() => {
|
||||
const state = api.getState();
|
||||
return !state.interpreterExecutionPending
|
||||
&& state.programExecution?.sourceMode === "linuxcnc-interpreter-wasm"
|
||||
&& state.machineFileStaging?.status === "staged";
|
||||
}, "default xyzbc program preview");
|
||||
|
||||
const state = api.getState();
|
||||
if (state.machineProfile !== "xyzbc-trt" || state.profile?.traj?.coordinates !== "XYZBC") {
|
||||
throw new Error(`default profile mismatch: ${state.machineProfile}/${state.profile?.traj?.coordinates}`);
|
||||
}
|
||||
if (state.profile?.kinematicsModuleId !== "xyzbc-trt") {
|
||||
throw new Error(`profile kinematics module mismatch: ${state.profile?.kinematicsModuleId}`);
|
||||
}
|
||||
if (state.machineFileStaging?.profileId !== "xyzbc-trt") {
|
||||
throw new Error(`staging profile mismatch: ${state.machineFileStaging?.profileId}`);
|
||||
}
|
||||
if (!state.machineFileStaging?.gcodeSources?.some((source) => source.filename === "xyzbc_switchkins.ngc")) {
|
||||
throw new Error("staged sources missing xyzbc_switchkins.ngc");
|
||||
}
|
||||
if (!state.machineFileStaging?.gcodeSources?.some((source) => source.filename === "boat-xyzbc.ngc")) {
|
||||
throw new Error("staged sources missing boat-xyzbc.ngc");
|
||||
}
|
||||
if (state.programExecution?.summary?.motionEventCount < 1) {
|
||||
throw new Error(`default program did not produce canonical motion: ${JSON.stringify(state.programExecution?.summary)}`);
|
||||
}
|
||||
|
||||
const canvas = await waitFor(() => doc.querySelector("[data-five-axis-canvas]"), "preview canvas");
|
||||
await waitFor(() => canvas.dataset.threeReady === "true" && Number(canvas.dataset.threePathPoints || 0) > 0, "Three.js dataset");
|
||||
if (
|
||||
canvas.dataset.threeRenderer !== "webgl" ||
|
||||
canvas.dataset.threeFrameApi !== "web-rtcp-5axis-motion-frame" ||
|
||||
canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" ||
|
||||
canvas.dataset.threeMachineReferenceModel !== "webgl-five-axis-reference" ||
|
||||
canvas.dataset.threePreviewScope !== "machine-reference-and-toolpath" ||
|
||||
canvas.dataset.threeProgramPreviewSource !== "linuxcnc-interpreter-wasm" ||
|
||||
canvas.dataset.threeVismachPinDrivenModel !== "web_threejs_vismach_equivalent_driven_by_xyzbc_trt_hal_pins"
|
||||
) {
|
||||
throw new Error(`unexpected canvas dataset: ${JSON.stringify(canvas.dataset)}`);
|
||||
}
|
||||
if (Number(canvas.dataset.threeSceneObjects || 0) < 12) {
|
||||
throw new Error(`scene object count too low: ${canvas.dataset.threeSceneObjects}`);
|
||||
}
|
||||
const vismachPins = JSON.parse(canvas.dataset.threeVismachPins || "{}");
|
||||
for (const pin of ["table-x", "saddle-y", "spindle-z", "tilt-b", "rotate-c", "tool-offset", "x-offset", "z-offset"]) {
|
||||
if (!Number.isFinite(Number(vismachPins[pin]))) {
|
||||
throw new Error(`missing Vismach pin ${pin}: ${JSON.stringify(vismachPins)}`);
|
||||
}
|
||||
}
|
||||
const vismachTransforms = JSON.parse(canvas.dataset.threeVismachTransforms || "{}");
|
||||
if (
|
||||
vismachTransforms.table?.sourcePins?.[0] !== "table-x" ||
|
||||
vismachTransforms.saddle?.sourcePins?.[0] !== "saddle-y" ||
|
||||
vismachTransforms.spindle?.sourcePins?.[0] !== "spindle-z" ||
|
||||
vismachTransforms.tilt?.sourcePins?.[0] !== "tilt-b" ||
|
||||
vismachTransforms.rotary?.sourcePins?.[0] !== "rotate-c" ||
|
||||
!vismachTransforms.tool?.sourcePins?.includes("tool-offset")
|
||||
) {
|
||||
throw new Error(`Vismach transform source pins mismatch: ${JSON.stringify(vismachTransforms)}`);
|
||||
}
|
||||
assertCanvasNonblank(canvas);
|
||||
|
||||
if (runtimeErrors.length > 0) {
|
||||
throw new Error(`runtime errors: ${runtimeErrors.join(" | ")}`);
|
||||
}
|
||||
result.textContent = "xyzbc_trt_browser_smoke=ok";
|
||||
}
|
||||
|
||||
function assertRuntime(readiness, { name, moduleId = null, required }) {
|
||||
for (const key of required) {
|
||||
if (readiness?.[key] !== true) {
|
||||
throw new Error(`${name} runtime missing ${key}: ${JSON.stringify(readiness)}`);
|
||||
}
|
||||
}
|
||||
if (moduleId && readiness?.moduleId !== moduleId) {
|
||||
throw new Error(`${name} runtime module mismatch: ${JSON.stringify(readiness)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitFor(predicate, label) {
|
||||
for (let attempt = 0; attempt < 240; attempt += 1) {
|
||||
const value = predicate();
|
||||
if (value) return value;
|
||||
await wait(50);
|
||||
}
|
||||
throw new Error(`timed out waiting for ${label}`);
|
||||
}
|
||||
|
||||
function assertCanvasNonblank(canvas) {
|
||||
const context = canvas.getContext("webgl2") || canvas.getContext("webgl") || canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("preview canvas has no readable context");
|
||||
}
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw new Error(`preview canvas has invalid size ${width}x${height}`);
|
||||
}
|
||||
if (context.readPixels) {
|
||||
const pixels = new Uint8Array(4);
|
||||
context.readPixels(Math.floor(width / 2), Math.floor(height / 2), 1, 1, context.RGBA, context.UNSIGNED_BYTE, pixels);
|
||||
if (pixels.some((value) => value !== 0)) return;
|
||||
}
|
||||
const imageData = canvas.toDataURL("image/png");
|
||||
if (!imageData || imageData.length < 2000) {
|
||||
throw new Error("preview canvas appears blank");
|
||||
}
|
||||
}
|
||||
|
||||
runSmoke().catch((error) => {
|
||||
result.textContent = `xyzbc_trt_browser_smoke=fail ${error.stack || error.message || error}`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,6 +17,11 @@ import {
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
|
||||
import { buildVismachModelState, XYZBC_TRT_VISMACH_PINS } from "../../app/src/runtime/vismach-model-state.js";
|
||||
import {
|
||||
applyToolCommandSequence,
|
||||
createToolRuntimeState,
|
||||
} from "../../app/src/runtime/tool-db-simulation.js";
|
||||
|
||||
const profile = getFiveAxisProfile();
|
||||
|
||||
@@ -30,6 +35,19 @@ assert.equal(profile.machineFileStaging.defaultProgramFilename, "xyzbc_switchkin
|
||||
assert.equal(profile.panelSchema.id, "xyzbc-trt-switchkins-pyvcp");
|
||||
assert.ok(profile.halPins.includes("xyzbc-trt-kins.x-offset"));
|
||||
assert.ok(profile.halPins.includes("motion.switchkins-type"));
|
||||
assert.deepEqual(profile.hal.halcmd.feedbackNets.map((net) => net.signal), [
|
||||
"table-x",
|
||||
"saddle-y",
|
||||
"spindle-z",
|
||||
"tilt-b",
|
||||
"rotate-c",
|
||||
]);
|
||||
assert.deepEqual(profile.hal.halcmd.offsetNets.map((net) => net.target), [
|
||||
"xyzbc-trt-kins.tool-offset",
|
||||
"xyzbc-trt-gui.tool-offset",
|
||||
"xyzbc-trt-gui.x-offset",
|
||||
"xyzbc-trt-gui.z-offset",
|
||||
]);
|
||||
assert.deepEqual(profile.samplePrograms, [
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
|
||||
@@ -80,6 +98,7 @@ assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/h
|
||||
assert.ok(staged.save.gcodeSources.some((source) => source.filename === "xyzbc_switchkins.ngc"));
|
||||
assert.ok(staged.save.gcodeSources.some((source) => source.filename === "boat-xyzbc.ngc"));
|
||||
assert.equal(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc.var") && file.kind === "parameters"), true);
|
||||
assert.equal(staged.save.files.find((file) => file.sourceRel.endsWith("xyzbc-trt.tbl")).text.includes("T2 P2 Z10 D8"), true);
|
||||
|
||||
const selectedPlan = selectMachineFileProgram(
|
||||
staged.plan,
|
||||
@@ -145,6 +164,58 @@ assert.equal(state.sessionName, "xyzbc-trt-web-session");
|
||||
assert.equal(state.kinematicsRuntimeReadiness, null);
|
||||
|
||||
const storeStage = await store.stageMachineFiles({ storage: createMemorySessionStorage() });
|
||||
const toolOffsetDb = applyToolCommandSequence(store.getState().toolDbSimulation, [
|
||||
{ code: "T", toolNumber: 2 },
|
||||
{ code: "M6" },
|
||||
{ code: "G43", h: 2 },
|
||||
]);
|
||||
const toolRuntimeState = createToolRuntimeState(toolOffsetDb, {
|
||||
fallbackToolLength: state.toolPreview.length,
|
||||
});
|
||||
assert.equal(toolRuntimeState.ready, true);
|
||||
assert.equal(toolRuntimeState.activeOffsetApplied, true);
|
||||
assert.equal(toolRuntimeState.activeToolNumber, 2);
|
||||
assert.equal(toolRuntimeState.activePocket, 2);
|
||||
assert.equal(toolRuntimeState.currentTool.offset.z, 10);
|
||||
assert.equal(toolRuntimeState.currentTool.diameter, 8);
|
||||
assert.equal(toolRuntimeState.kinematics.toolOffsetZ, 10);
|
||||
assert.equal(toolRuntimeState.kinematics.halPin, "motion.tooloffset.z");
|
||||
assert.deepEqual(toolRuntimeState.kinematics.downstreamPins, [
|
||||
"xyzbc-trt-kins.tool-offset",
|
||||
"xyzbc-trt-gui.tool-offset",
|
||||
]);
|
||||
assert.equal(toolRuntimeState.pathTool.id, 2);
|
||||
assert.equal(toolRuntimeState.pathTool.pocket, 2);
|
||||
assert.equal(toolRuntimeState.pathTool.length, 10);
|
||||
assert.equal(toolRuntimeState.pathTool.diameter, 8);
|
||||
assert.equal(toolRuntimeState.vismach.toolOffset, 10);
|
||||
|
||||
const vismachState = buildVismachModelState({
|
||||
...state,
|
||||
axisPose: { x: 12.5, y: -6.25, z: 3.75, b: 35, c: -90 },
|
||||
toolPreview: { ...state.toolPreview, length: 84.019 },
|
||||
toolRuntimeState,
|
||||
});
|
||||
assert.deepEqual(Object.keys(vismachState.pins), XYZBC_TRT_VISMACH_PINS);
|
||||
assert.equal(vismachState.pins["table-x"], 12.5);
|
||||
assert.equal(vismachState.pins["saddle-y"], -6.25);
|
||||
assert.equal(vismachState.pins["spindle-z"], 3.75);
|
||||
assert.equal(vismachState.pins["tilt-b"], 35);
|
||||
assert.equal(vismachState.pins["rotate-c"], -90);
|
||||
assert.equal(vismachState.pins["x-offset"], -20);
|
||||
assert.equal(vismachState.pins["z-offset"], -15);
|
||||
assert.equal(vismachState.pins["tool-offset"], 10);
|
||||
assert.deepEqual(vismachState.transforms.table.sourcePins, ["table-x"]);
|
||||
assert.deepEqual(vismachState.transforms.saddle.sourcePins, ["saddle-y"]);
|
||||
assert.deepEqual(vismachState.transforms.spindle.sourcePins, ["spindle-z"]);
|
||||
assert.deepEqual(vismachState.transforms.tilt.sourcePins, ["tilt-b"]);
|
||||
assert.deepEqual(vismachState.transforms.rotary.sourcePins, ["rotate-c"]);
|
||||
assert.deepEqual(vismachState.transforms.tool.sourcePins, ["tool-offset", "x-offset", "z-offset"]);
|
||||
assert.equal(vismachState.transforms.tool.translate.x, -20);
|
||||
assert.equal(vismachState.transforms.tool.translate.z, -25);
|
||||
assert.equal(vismachState.halNets.length, 9);
|
||||
assert.equal(vismachState.semanticBoundary, "web_threejs_vismach_equivalent_driven_by_xyzbc_trt_hal_pins");
|
||||
|
||||
const stagedState = store.getState();
|
||||
assert.equal(storeStage.save.profileId, "xyzbc-trt");
|
||||
assert.equal(stagedState.machineFileStaging.profileId, "xyzbc-trt");
|
||||
|
||||
Reference in New Issue
Block a user