接入 LinuxCNC TP 运行反馈

This commit is contained in:
2026-06-21 23:29:56 +08:00
parent 626bcfe8e3
commit 3771b9eafe
44 changed files with 7881 additions and 326 deletions

View File

@@ -6,12 +6,34 @@
</head>
<body>
<pre id="result">gmoccapy_shell_smoke=pending</pre>
<iframe id="app-frame" src="../../app/index.html" title="gmoccapy shell"></iframe>
<iframe id="app-frame" title="gmoccapy shell"></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 waitForInterpreterExecution(win) {
for (let attempt = 0; attempt < 40; attempt += 1) {
const state = win.webRtcp5AxisSimulation.getState();
if (!state.interpreterExecutionPending && state.programExecutionSourceMode === "linuxcnc-interpreter-wasm") {
return state;
}
await wait(25);
}
return win.webRtcp5AxisSimulation.getState();
}
async function waitForMachineFileExecution(win) {
for (let attempt = 0; attempt < 60; attempt += 1) {
const state = win.webRtcp5AxisSimulation.getState();
if (!state.interpreterExecutionPending && state.machineFileExecution) {
return state;
}
await wait(25);
}
return win.webRtcp5AxisSimulation.getState();
}
async function runSmoke() {
await new Promise((resolve, reject) => {
@@ -89,25 +111,84 @@
throw new Error(`forbidden frontend framework dependency detected: ${forbidden.join(", ")}`);
}
const runtimeReadiness = await win.webRtcp5AxisSimulation.kinematicsRuntimeReady;
if (runtimeReadiness.loaded !== true || runtimeReadiness.moduleId !== "xyzac-trt") {
throw new Error(`LinuxCNC kinematics runtime did not load in browser: ${JSON.stringify(runtimeReadiness)}`);
}
if (runtimeReadiness.executionContext !== "worker") {
throw new Error(`LinuxCNC kinematics runtime should run in worker: ${JSON.stringify(runtimeReadiness)}`);
}
const interpreterReadiness = await win.webRtcp5AxisSimulation.interpreterRuntimeReady;
if (interpreterReadiness.loaded !== true || interpreterReadiness.runProgramReady !== true) {
throw new Error(`LinuxCNC interpreter runtime did not load in browser: ${JSON.stringify(interpreterReadiness)}`);
}
if (interpreterReadiness.executionContext !== "worker") {
throw new Error(`LinuxCNC interpreter runtime should run in worker: ${JSON.stringify(interpreterReadiness)}`);
}
if (
interpreterReadiness.plannerRuntimeReady !== true ||
interpreterReadiness.plannerSemanticBoundary !== "linuxcnc_tp_queue_runtime_timing_from_canonical_motion"
) {
throw new Error(`LinuxCNC TP planner runtime did not load in browser worker: ${JSON.stringify(interpreterReadiness)}`);
}
const state = win.webRtcp5AxisSimulation.getState();
if (state.sourceMode !== "fixture-ui-only") {
if (state.sourceMode !== "source-derived-kinematics-wasm") {
throw new Error(`unexpected source mode: ${state.sourceMode}`);
}
if (state.machineProfile !== "xyzac-trt") {
throw new Error(`unexpected profile: ${state.machineProfile}`);
if (state.kinematicsExecutionContext !== "worker") {
throw new Error(`unexpected kinematics execution context: ${state.kinematicsExecutionContext}`);
}
const profileSelector = doc.querySelector('[data-action="select-profile"]');
if (!profileSelector || profileSelector.options.length < 2) {
throw new Error("missing five-axis profile selector");
}
profileSelector.value = "xyzbc-trt";
profileSelector.dispatchEvent(new Event("change", { bubbles: true }));
await wait(500);
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke refreshed xyzbc frame" });
const xyzbcState = win.webRtcp5AxisSimulation.getState();
if (xyzbcState.machineProfile !== "xyzbc-trt" || xyzbcState.profile.traj.coordinates !== "XYZBC") {
throw new Error("profile selector did not switch to XYZBC");
}
if (xyzbcState.kinematicsRuntimeReadiness?.moduleId !== "xyzbc-trt") {
throw new Error(`XYZBC kinematics runtime did not load: ${JSON.stringify(xyzbcState.kinematicsRuntimeReadiness)}`);
}
const restoredProfileSelector = doc.querySelector('[data-action="select-profile"]');
restoredProfileSelector.value = "xyzac-trt";
restoredProfileSelector.dispatchEvent(new Event("change", { bubbles: true }));
await wait(500);
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke restored xyzac frame" });
const restoredProfileState = win.webRtcp5AxisSimulation.getState();
if (restoredProfileState.machineProfile !== "xyzac-trt") {
throw new Error(`unexpected profile: ${restoredProfileState.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 (state.rtcpFrame?.readiness?.linuxCncKinematicsReady !== true) {
throw new Error("browser RTCP frame must use 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")) {
if (!doc.querySelector('[data-rtcp-diagnostic="boundary"]')?.textContent.includes("linuxcnc_kinematics_wasm_c_abi")) {
throw new Error("missing RTCP boundary diagnostic");
}
if (!doc.querySelector('[data-linuxcnc-ini="status"]')?.textContent.includes("xyzac-trt.ini")) {
throw new Error("missing LinuxCNC INI status diagnostic");
}
if (!doc.querySelector('[data-linuxcnc-ini="kins"]')?.textContent.includes("xyzac-trt-kins")) {
throw new Error("missing LinuxCNC INI kinematics diagnostic");
}
if (!doc.querySelector('[data-linuxcnc-ini="limits"]')?.textContent.includes("X[-200,200]")) {
throw new Error("missing LinuxCNC INI axis limits diagnostic");
}
if (!doc.querySelector('[data-linuxcnc-task-policy="boundary"]')?.textContent.includes("linuxcnc_task_state_mode_command_gate")) {
throw new Error("missing LinuxCNC task policy boundary diagnostic");
}
if (!doc.querySelector('[data-linuxcnc-task-policy="source"]')?.textContent.includes("linuxcnc/src/emc/task/emctaskmain.cc")) {
throw new Error("missing LinuxCNC emctaskmain source diagnostic");
}
if (!doc.querySelector('[data-tool-preview="summary"]')?.textContent.includes("T1")) {
throw new Error("missing tool preview summary");
}
@@ -131,11 +212,19 @@
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power on")) {
throw new Error("machine state did not render power on");
}
doc.querySelector('[data-action="HOME"]').click();
await wait(50);
doc.querySelector('[data-action="mode-auto"]').click();
await wait(50);
if (!doc.querySelector('[data-linuxcnc-task-policy="gates"]')?.textContent.includes("auto")) {
throw new Error("LinuxCNC task policy did not expose auto run gate");
}
win.webRtcp5AxisSimulation.dispatch({
type: "LOAD_PROGRAM",
filename: "operator-demo.ngc",
content: [
"G90 G17",
"G0 X0 Y0 Z0",
"G1 X10 F100",
"G1 Y10",
@@ -143,18 +232,154 @@
"G1 Y0",
"G0 Z5",
"M5",
"M30",
"M2",
].join("\n"),
});
await wait(50);
await waitForInterpreterExecution(win);
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 (win.webRtcp5AxisSimulation.getState().programExecutionSourceMode !== "linuxcnc-interpreter-wasm") {
throw new Error("LOAD_PROGRAM did not execute through LinuxCNC interpreter WASM");
}
if (doc.querySelector(".gcode-row.active")?.dataset.programLine !== "1") {
throw new Error("loaded program active row should be line 1");
if (win.webRtcp5AxisSimulation.getState().programExecution?.summary?.motionEventCount < 5) {
throw new Error("LinuxCNC interpreter execution did not produce motion events");
}
if (win.webRtcp5AxisSimulation.getState().programExecution?.summary?.plannerRuntimeReady !== true) {
throw new Error("LinuxCNC TP planner timing was not ready for operator program");
}
if (win.webRtcp5AxisSimulation.getState().programExecutionTiming?.semanticBoundary !== "linuxcnc_tp_queue_runtime_timing_from_canonical_motion") {
throw new Error(`program timing did not use LinuxCNC TP queue runtime: ${JSON.stringify(win.webRtcp5AxisSimulation.getState().programExecutionTiming)}`);
}
if (win.webRtcp5AxisSimulation.getState().programExecutionTiming?.sampleCount < 2) {
throw new Error("LinuxCNC TP runtime feedback samples were not exposed");
}
if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.semanticBoundary !== "linuxcnc_tp_run_cycle_feedback_without_hardware") {
throw new Error(`initial runtime feedback did not use LinuxCNC TP sample: ${JSON.stringify(win.webRtcp5AxisSimulation.getState().programRuntimeFeedback)}`);
}
if (!doc.querySelector('[data-program-execution-source]')?.textContent.includes("linuxcnc-interpreter-wasm")) {
throw new Error("program execution source DOM did not render interpreter source");
}
if (!doc.querySelector('[data-program-execution-summary]')?.textContent.includes("motion")) {
throw new Error("program execution summary DOM did not render motion count");
}
if (!doc.querySelector('[data-program-timing="summary"]')?.textContent.match(/\\d+:\\d/)) {
throw new Error("program timing summary DOM did not render estimated execution time");
}
if (!doc.querySelector('[data-program-timing="segment"]')?.textContent.includes("mm/min")) {
throw new Error("program timing segment DOM did not render segment velocity");
}
if (!doc.querySelector('[data-program-runtime-feedback="source"]')?.textContent.includes("linuxcnc-tp-runtime-sample")) {
throw new Error("program runtime feedback DOM did not render LinuxCNC TP sample source");
}
if (!doc.querySelector('[data-program-runtime-feedback="dtg"]')?.textContent.includes("DTG")) {
throw new Error("program runtime feedback DOM did not render DTG");
}
if (!doc.querySelector('[data-program-switchkins-summary]')?.textContent.includes("0 events")) {
throw new Error("program switchkins summary DOM did not render zero-event state");
}
win.webRtcp5AxisSimulation.dispatch({
type: "LOAD_PROGRAM",
filename: "operator-arc-demo.ngc",
content: [
"G90 G17",
"G0 X1 Y0 Z0",
"G2 X0 Y1 I-1 J0 F60",
"M2",
].join("\n"),
});
await waitForInterpreterExecution(win);
const arcState = win.webRtcp5AxisSimulation.getState();
if (!arcState.programExecution?.summary?.motionTypes?.includes("ARC_FEED")) {
throw new Error("LinuxCNC interpreter did not produce ARC_FEED for browser arc program");
}
if (arcState.programExecution?.summary?.plannerRuntimeReady !== true) {
throw new Error("LinuxCNC TP planner timing was not ready for browser arc program");
}
const arcSegment = arcState.programExecutionTiming?.segments?.find((segment) => segment.type === "ARC_FEED");
if (!arcSegment || arcSegment.durationSeconds <= 1.4) {
throw new Error(`LinuxCNC TP arc timing segment missing or too short: ${JSON.stringify(arcState.programExecutionTiming)}`);
}
const stagedMachineFiles = await win.webRtcp5AxisSimulation.stageMachineFiles();
await wait(50);
if (stagedMachineFiles.save.status !== "saved" || stagedMachineFiles.save.fileCount < 5) {
throw new Error(`machine file staging did not save files: ${JSON.stringify(stagedMachineFiles.save)}`);
}
if (!stagedMachineFiles.save.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc"))) {
throw new Error("machine file staging did not include M428 remap");
}
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
throw new Error("machine file staging status did not render");
}
const sourceSelect = doc.querySelector('[data-action="select-linuxcnc-gcode-source"]');
if (!sourceSelect || sourceSelect.options.length < 4) {
throw new Error("LinuxCNC 5-axis G-code source selector did not render staged demos");
}
const impellerSource = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc";
sourceSelect.value = impellerSource;
sourceSelect.dispatchEvent(new Event("change", { bubbles: true }));
await waitForInterpreterExecution(win);
const linuxCncSourceState = win.webRtcp5AxisSimulation.getState();
if (linuxCncSourceState.programSource !== "linuxcnc-vendored-5axis-gcode") {
throw new Error(`LinuxCNC source program was not loaded: ${linuxCncSourceState.programSource}`);
}
if (!linuxCncSourceState.activeProgram.endsWith("impeller-7bl-xyzac.ngc")) {
throw new Error(`unexpected LinuxCNC source program: ${linuxCncSourceState.activeProgram}`);
}
if (!doc.querySelector('[data-linuxcnc-gcode-source="selected"]')?.textContent.includes("impeller-7bl-xyzac.ngc")) {
throw new Error("selected LinuxCNC G-code source did not render");
}
if (!doc.querySelector('[data-program-source]')?.textContent.includes("linuxcnc-vendored-5axis-gcode")) {
throw new Error("program source DOM did not show LinuxCNC vendored source");
}
win.webRtcp5AxisSimulation.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
const machineFileRunState = await waitForMachineFileExecution(win);
if (machineFileRunState.machineFileExecution?.summary?.machineFileExecutionReady !== true) {
throw new Error(`machine-file backed remap run did not complete: ${JSON.stringify(machineFileRunState.machineFileExecution?.summary)}`);
}
if (!machineFileRunState.machineFileExecution.resultText.includes("fiveaxis_file_reached_exit=1")) {
throw new Error("machine-file backed remap run did not reach exit");
}
if (!machineFileRunState.machineFileExecution.machineFilePlan?.selectedProgramFilename?.includes("impeller-7bl-xyzac.ngc")) {
throw new Error("machine-file backed remap run did not use selected LinuxCNC G-code source");
}
if (!doc.querySelector('[data-machine-file-execution="status"]')?.textContent.includes("ready")) {
throw new Error("machine-file execution status did not render");
}
if (machineFileRunState.fullExecutionBoundary?.machineFileBackedRemapReady !== true) {
throw new Error(`full execution boundary did not record remap readiness: ${JSON.stringify(machineFileRunState.fullExecutionBoundary)}`);
}
if (machineFileRunState.fullExecutionBoundary?.fullLinuxCncProgramExecutionReady !== false) {
throw new Error("full LinuxCNC program execution must remain blocked without native task/planner/HAL");
}
if (!machineFileRunState.fullExecutionBoundary.blockers.some((blocker) => blocker.includes("trajectory planner"))) {
throw new Error("full execution boundary did not expose planner blocker");
}
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("remap ready")) {
throw new Error("full execution boundary status did not render remap readiness");
}
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("full blocked")) {
throw new Error("full execution boundary status did not render full blocked state");
}
if (!doc.querySelector('[data-full-execution-boundary="blockers"]')?.textContent.includes("native LinuxCNC task")) {
throw new Error("full execution blocker diagnostic did not render native task blocker");
}
if (!doc.querySelector('[data-full-execution-boundary="evidence"]')?.textContent.includes("linuxcnc_machine_file_remap_ready_planner_task_hal_blocked")) {
throw new Error("full execution evidence diagnostic did not render boundary semantic");
}
const savedSession = await win.webRtcp5AxisSimulation.saveSession();
await wait(50);
if (savedSession.snapshot.format !== "web-rtcp-5axis-session-snapshot") {
throw new Error("saveSession did not write a five-axis session snapshot");
}
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("saved")) {
throw new Error("session save status did not render");
}
if (doc.querySelector('[data-active-program-line]')?.textContent !== "Current line 2") {
throw new Error("loaded program did not render first LinuxCNC motion line");
}
if (doc.querySelector(".gcode-row.active")?.dataset.programLine !== "2") {
throw new Error("loaded program active row should be first LinuxCNC motion line");
}
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
@@ -162,8 +387,29 @@
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");
if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.sourceMode !== "linuxcnc-tp-runtime-sample") {
throw new Error("RUN did not advance with LinuxCNC TP runtime feedback");
}
if (win.webRtcp5AxisSimulation.getState().feed.currentVelocity <= 0) {
throw new Error("RUN did not expose LinuxCNC TP current velocity");
}
if (Number(doc.querySelector(".gcode-row.active")?.dataset.programLine || 0) < 2) {
throw new Error("RUN did not highlight a LinuxCNC canonical motion line");
}
doc.querySelector('[data-action="PAUSE"]').click();
await wait(50);
if (win.webRtcp5AxisSimulation.getState().runState !== "paused") {
throw new Error("PAUSE action did not update state");
}
doc.querySelector('[data-action="RUN"]').click();
await wait(50);
if (!win.webRtcp5AxisSimulation.getState().operatorMessage.includes("resume paused program")) {
throw new Error("RUN should be blocked while program is paused");
}
doc.querySelector('[data-action="RESUME"]').click();
await wait(50);
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
throw new Error("RESUME action did not restore running state");
}
const tcpButton = doc.querySelector('[data-action="kins-tcp"]');
tcpButton.click();
@@ -181,8 +427,11 @@
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-rtcp-diagnostic="kinematics-ready"]')?.textContent !== "ready") {
throw new Error("RTCP diagnostics must show LinuxCNC kinematics ready");
}
if (doc.querySelector('[data-rtcp-diagnostic="execution-context"]')?.textContent !== "worker") {
throw new Error("RTCP diagnostics must show worker kinematics context");
}
if (!doc.querySelector('[data-linuxcnc-boundary="adapter"]')?.textContent.includes("linuxcnc-boundary-adapter")) {
throw new Error("missing LinuxCNC boundary adapter diagnostic");
@@ -193,12 +442,19 @@
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");
if (!doc.querySelector('[data-linuxcnc-boundary="readiness"]')?.textContent.includes("ready")) {
throw new Error("LinuxCNC boundary readiness should show kinematics ready");
}
if (!doc.querySelector('[data-linuxcnc-boundary="interpreter"]')?.textContent.includes("linuxcnc_interpreter_wasm_canonical_events")) {
throw new Error("missing LinuxCNC interpreter boundary diagnostic");
}
if (doc.querySelector('[data-linuxcnc-boundary="interpreter-context"]')?.textContent !== "worker") {
throw new Error("LinuxCNC interpreter diagnostics must show worker context");
}
canvas = doc.querySelector("[data-five-axis-canvas]");
const tcpPoseBeforeStep = canvas.dataset.threeTcpPose;
win.webRtcp5AxisSimulation.dispatch({ type: "STEP" });
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke refreshed worker frame" });
await wait(50);
if (win.webRtcp5AxisSimulation.getState().activeLine <= rtcpState.activeLine) {
throw new Error("STEP did not advance RTCP frame line");
@@ -208,6 +464,13 @@
throw new Error("Three.js preview did not update TCP pose after STEP");
}
assertCanvasNonblank(canvas, "updated Three.js preview");
doc.querySelector('[data-action="STOP"]').click();
await wait(50);
if (win.webRtcp5AxisSimulation.getState().machine.interpState !== "idle") {
throw new Error("STOP did not clear interpreter state");
}
doc.querySelector('[data-action="mode-manual"]').click();
await wait(50);
doc.querySelector('[data-action="mode-jog"]').click();
await wait(50);
const xBeforeJog = win.webRtcp5AxisSimulation.getState().axisPose.x;
@@ -216,11 +479,47 @@
if (win.webRtcp5AxisSimulation.getState().axisPose.x <= xBeforeJog) {
throw new Error("JOG X+ did not move the axis");
}
await win.webRtcp5AxisSimulation.restoreSession();
await wait(50);
const restoredState = win.webRtcp5AxisSimulation.getState();
if (restoredState.axisPose.x !== xBeforeJog) {
throw new Error("restoreSession did not restore saved axis pose");
}
if (restoredState.programExecutionSourceMode !== "linuxcnc-interpreter-wasm") {
throw new Error("restoreSession did not restore LinuxCNC interpreter execution source");
}
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("restored")) {
throw new Error("session restore status did not render");
}
if (win.webRtcp5AxisSimulation.getState().machine.allHomed !== true) {
doc.querySelector('[data-action="mode-manual"]').click();
await wait(50);
doc.querySelector('[data-action="HOME"]').click();
await wait(50);
}
doc.querySelector('[data-action="mode-mdi"]').click();
const mdiInput = doc.querySelector('[data-action="mdi-command"]');
if (!mdiInput) {
throw new Error("missing MDI command input");
}
mdiInput.value = "G90 X12.5 Y-4 Z1.25 F900";
doc.querySelector('[data-action="mdi-submit"]').click();
await wait(50);
let mdiState = win.webRtcp5AxisSimulation.getState();
if (mdiState.runState !== "mdi") {
throw new Error("MDI action did not update run state");
}
if (mdiState.axisPose.x !== 12.5 || mdiState.axisPose.y !== -4 || mdiState.axisPose.z !== 1.25) {
throw new Error(`MDI input did not move axes: ${JSON.stringify(mdiState.axisPose)}`);
}
if (!doc.querySelector(".mdi-history")?.textContent.includes("G90 X12.5")) {
throw new Error("MDI history did not render executed command");
}
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");
mdiState = win.webRtcp5AxisSimulation.getState();
if (mdiState.runState !== "mdi" || mdiState.machine.mdiCommand !== "G90 X12.5 Y-4 Z1.25 F900") {
throw new Error("bottom MDI button did not execute staged command");
}
doc.querySelector('[data-action="reset"]').click();
await wait(50);
@@ -272,6 +571,8 @@
if (win.webRtcp5AxisSimulation.getState().preview.fullscreen !== true) {
throw new Error("fullscreen button did not update state");
}
doc.querySelector('[data-action="mode-manual"]').click();
await wait(50);
doc.querySelector('[data-action="HOME"]').click();
await wait(50);
const homedState = win.webRtcp5AxisSimulation.getState();

View File

@@ -0,0 +1,77 @@
#!/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
npm --prefix "$ROOT_DIR/web-rtcp-5axis-sim-plan/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 "gmoccapy dist 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-sim-plan/tests/browser/gmoccapy_shell_smoke.html?app=$APP_PATH"
OUT="$TMP_DIR/chromium-gmoccapy-dist.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 dist browser smoke failed" >&2
sed -n '1,260p' "$OUT" >&2
exit 1
fi
echo "gmoccapy_dist_smoke=ok"

View File

@@ -0,0 +1,73 @@
import assert from "node:assert/strict";
import { createFiveAxisSessionPayload, createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
import { createSimulationStore } from "../../app/src/state/store.js";
async function waitForInterpreterExecution(store) {
for (let attempt = 0; attempt < 20; attempt += 1) {
const state = store.getState();
if (!state.interpreterExecutionPending && state.programExecutionSourceMode === "linuxcnc-interpreter-wasm") {
return state;
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
return store.getState();
}
const store = createSimulationStore();
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
store.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime: interpreterRuntime });
store.dispatch({ type: "TOGGLE_POWER" });
store.dispatch({ type: "HOME" });
store.dispatch({ type: "SET_MODE", mode: "auto" });
store.dispatch({ type: "SET_RTCP", enabled: true });
store.dispatch({
type: "LOAD_PROGRAM",
filename: "session-demo.ngc",
content: [
"G90 G17",
"G0 X0 Y0 Z0",
"G1 X4 Y5 A6 C7 F100",
"M2",
].join("\n"),
});
await waitForInterpreterExecution(store);
store.dispatch({ type: "STEP" });
const beforeSave = store.getState();
const payload = createFiveAxisSessionPayload(beforeSave);
assert.equal(payload.apiName, "web-rtcp-5axis-session-payload");
assert.equal(payload.machineProfile, "xyzac-trt");
assert.equal(payload.programExecution.sourceMode, "linuxcnc-interpreter-wasm");
assert.equal(payload.rtcpState, "on");
assert.equal(payload.programRuntimeFeedback.sourceMode, "linuxcnc-tp-runtime-sample");
assert.equal(payload.programRuntimeFeedback.semanticBoundary, "linuxcnc_tp_run_cycle_feedback_without_hardware");
const storage = createMemorySessionStorage();
const saved = await store.saveSession({ storage });
assert.equal(saved.snapshot.format, "web-rtcp-5axis-session-snapshot");
assert.equal(saved.path, "web-rtcp-5axis-sim-plan/sessions/gmoccapy-web-session/web-rtcp-5axis-session.json");
store.dispatch({ type: "SET_RTCP", enabled: false });
store.dispatch({ type: "STOP" });
store.dispatch({ type: "SET_MODE", mode: "manual" });
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 25 });
assert.notEqual(store.getState().rtcpState, beforeSave.rtcpState);
assert.notEqual(store.getState().axisPose.x, beforeSave.axisPose.x);
const restored = await store.restoreSession({ storage });
const afterRestore = store.getState();
assert.equal(restored.path, saved.path);
assert.equal(afterRestore.sessionPersistence.status, "restored");
assert.equal(afterRestore.rtcpState, beforeSave.rtcpState);
assert.equal(afterRestore.kinsType, beforeSave.kinsType);
assert.equal(afterRestore.axisPose.x, beforeSave.axisPose.x);
assert.equal(afterRestore.activeProgram, "session-demo.ngc");
assert.equal(afterRestore.programExecution.sourceMode, "linuxcnc-interpreter-wasm");
assert.equal(afterRestore.programExecution.summary.motionEventCount, beforeSave.programExecution.summary.motionEventCount);
assert.equal(afterRestore.programRuntimeFeedback.semanticBoundary, beforeSave.programRuntimeFeedback.semanticBoundary);
assert.equal(afterRestore.programRuntimeFeedback.axisPose.x, beforeSave.programRuntimeFeedback.axisPose.x);
console.log("five_axis_session_smoke=ok");

View File

@@ -0,0 +1,108 @@
import assert from "node:assert/strict";
import { createFullLinuxCncExecutionBoundary } from "../../app/src/runtime/full-execution-boundary.js";
const blocked = createFullLinuxCncExecutionBoundary({});
assert.equal(blocked.apiName, "web-rtcp-5axis-full-linuxcnc-execution-boundary");
assert.equal(blocked.phase, "blocked");
assert.equal(blocked.promotionAllowed, false);
assert.equal(blocked.fullLinuxCncProgramExecutionReady, false);
assert.equal(blocked.missing.includes("linuxcnc kinematics WASM frame"), true);
assert.equal(blocked.blockers.some((blocker) => blocker.includes("native LinuxCNC task")), true);
const canonicalState = {
machineProfile: "xyzac-trt",
linuxCncBoundaryAdapter: {
linuxCncKinematicsReady: true,
linuxCncInterpreterReady: true,
},
rtcpFrame: {
semanticBoundary: "linuxcnc_kinematics_wasm_c_abi",
readiness: { linuxCncKinematicsReady: true },
},
interpreterRuntimeReadiness: {
loaded: true,
semanticBoundary: "linuxcnc_interpreter_wasm_canonical_events",
},
programExecutionSourceMode: "linuxcnc-interpreter-wasm",
programExecution: {
sourceMode: "linuxcnc-interpreter-wasm",
summary: {
motionEventCount: 3,
canonicalEventCount: 8,
},
},
};
const canonical = createFullLinuxCncExecutionBoundary(canonicalState);
assert.equal(canonical.readyForUiSimulation, true);
assert.equal(canonical.machineFileBackedRemapReady, false);
assert.equal(canonical.semanticBoundary, "linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked");
assert.equal(canonical.satisfied.includes("canonical-motion-events"), true);
assert.equal(canonical.plannerRuntimeReady, false);
assert.equal(canonical.missing.includes("machine-file backed five-axis remap run"), true);
const canonicalWithPlanner = createFullLinuxCncExecutionBoundary({
...canonicalState,
programExecution: {
...canonicalState.programExecution,
plannerTiming: {
plannerRuntimeReady: true,
semanticBoundary: "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
},
summary: {
...canonicalState.programExecution.summary,
plannerRuntimeReady: true,
},
},
});
assert.equal(canonicalWithPlanner.plannerRuntimeReady, true);
assert.equal(canonicalWithPlanner.satisfied.includes("linuxcnc-tp-queue-runtime-timing"), true);
assert.equal(canonicalWithPlanner.nativeTaskReady, false);
assert.equal(canonicalWithPlanner.nativeHalSyncReady, false);
const remap = createFullLinuxCncExecutionBoundary({
...canonicalState,
programExecution: {
...canonicalState.programExecution,
plannerTiming: {
plannerRuntimeReady: true,
semanticBoundary: "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
},
summary: {
...canonicalState.programExecution.summary,
plannerRuntimeReady: true,
},
},
machineFileStaging: {
status: "staged",
fileCount: 12,
},
machineFileExecution: {
sourceMode: "linuxcnc-machine-file-remap-wasm",
summary: {
machineFileExecutionReady: true,
},
resultText: [
"fiveaxis_ini_open=1",
"fiveaxis_remaps_ready=1",
"fiveaxis_file_reached_exit=1",
"fiveaxis_hal_switchkins: rc=0 found=1 value=0",
].join("\n"),
},
});
assert.equal(remap.machineFileBackedRemapReady, true);
assert.equal(remap.remapRuntimeReady, true);
assert.equal(remap.halSwitchkinsEvidenceReady, true);
assert.equal(remap.plannerRuntimeReady, true);
assert.equal(remap.nativeTaskReady, false);
assert.equal(remap.nativeHalSyncReady, false);
assert.equal(remap.fullLinuxCncProgramExecutionReady, false);
assert.equal(remap.promotionAllowed, false);
assert.equal(remap.semanticBoundary, "linuxcnc_machine_file_remap_ready_planner_task_hal_blocked");
assert.equal(remap.satisfied.includes("fiveaxis-remap-machine-file-run"), true);
assert.equal(remap.evidence.machineFileFlags.length, 3);
console.log("full_execution_boundary_smoke=ok");

View File

@@ -0,0 +1,109 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
createLinuxCncInterpSdk,
createLinuxCncKinematicsSdk,
planSimConfigStaging,
} from "../../../wasm-port/runtime/sdk/src/index.js";
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const workspaceRoot = resolve(__dirname, "../../..");
const wasmPortRoot = resolve(workspaceRoot, "wasm-port");
const vendorRoot = resolve(wasmPortRoot, "vendor/linuxcnc");
const trtConfigRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting";
function near(actual, expected, tolerance = 1e-7) {
return Math.abs(actual - expected) <= tolerance;
}
function assertPoseComponent(pose, key, expected, label) {
assert.equal(near(Number(pose[key] ?? 0), expected), true, `${label}.${key}: ${pose[key]} != ${expected}`);
}
async function verifyTrtKinematics(moduleId, joints, expectedAxis) {
const wasmFile = `linuxcnc_${moduleId.replaceAll("-", "_")}_kinematics.wasm`;
const kins = await createLinuxCncKinematicsSdk({
moduleId,
moduleOptions: {
wasmBinary: readFileSync(resolve(wasmPortRoot, "build/wasm/kinematics", wasmFile)),
print() {},
printErr() {},
},
});
assert.equal(kins.apiName, "linuxcnc-kinematics-wasm-sdk");
assert.equal(kins.switchable(), 1);
assert.equal(kins.switchKinematics(0), 0);
const forward = kins.forward(joints);
assert.equal(forward.rc, 0, `${moduleId} forward`);
const inverse = kins.inverse(forward.pose, 5, { seedJoints: joints });
assert.equal(inverse.rc, 0, `${moduleId} inverse`);
inverse.joints.slice(0, 5).forEach((joint, index) => {
assert.equal(near(joint, joints[index]), true, `${moduleId} inverse joint ${index}`);
});
assert.equal(kins.switchKinematics(1), 0);
const identity = kins.forward(joints);
assert.equal(identity.rc, 0, `${moduleId} identity forward`);
assertPoseComponent(identity.pose, "x", joints[0], `${moduleId} identity`);
assertPoseComponent(identity.pose, "y", joints[1], `${moduleId} identity`);
assertPoseComponent(identity.pose, "z", joints[2], `${moduleId} identity`);
assertPoseComponent(identity.pose, expectedAxis, joints[3], `${moduleId} identity`);
assertPoseComponent(identity.pose, "c", joints[4], `${moduleId} identity`);
}
await verifyTrtKinematics("xyzac-trt", [10, 20, 30, 25, 40], "a");
await verifyTrtKinematics("xyzbc-trt", [10, 20, 30, 35, 40], "b");
const manifestText = readFileSync(resolve(wasmPortRoot, "tools/source-manifest.txt"), "utf8");
for (const iniFile of ["xyzac-trt.ini", "xyzbc-trt.ini"]) {
const iniText = readFileSync(resolve(vendorRoot, trtConfigRel, iniFile), "utf8");
const iniConfig = parseLinuxCncIni(iniText, {
path: `${trtConfigRel}/${iniFile}`,
profileId: iniFile.startsWith("xyzbc") ? "xyzbc-trt" : "xyzac-trt",
});
assert.equal(iniConfig.validation.ready, true, `${iniFile} INI ready`);
assert.equal(iniConfig.kinematics.name.endsWith("-trt-kins"), true, `${iniFile} kins`);
assert.deepEqual(iniConfig.halui.mdiCommands, ["M429", "M428", "M430"], `${iniFile} HALUI MDI`);
const plan = planSimConfigStaging({
manifestText,
machineRel: `axis/vismach/5axis/table-rotary-tilting`,
iniFile,
iniText,
});
const staged = plan.files.map((file) => file.sourceRel);
assert.equal(staged.includes(`${trtConfigRel}/${iniFile}`), true, `${iniFile} staged`);
assert.equal(staged.some((file) => file.endsWith("/remap_subs/428remap.ngc")), true, `${iniFile} M428 remap staged`);
assert.equal(staged.some((file) => file.endsWith("/remap_subs/429remap.ngc")), true, `${iniFile} M429 remap staged`);
assert.equal(staged.some((file) => file.endsWith("/remap_subs/430remap.ngc")), true, `${iniFile} M430 remap staged`);
assert.equal(staged.some((file) => file.endsWith(".tbl")), true, `${iniFile} tool table staged`);
}
const interp = await createLinuxCncInterpSdk({
wasmBinary: readFileSync(resolve(wasmPortRoot, "build/wasm/core/linuxcnc_interp.wasm")),
print() {},
printErr() {},
});
const directFiveAxisProgram = [
"G90 G17",
"G0 X0 Y0 Z0 A0 C0",
"G1 X10 Y2 Z-1 A15 C30 F120",
"G1 X0 Y0 Z0 A0 C0 F120",
"M2",
"",
].join("\n");
const output = interp.runProgram(directFiveAxisProgram);
assert.equal(output.includes("canon_event=STRAIGHT_TRAVERSE"), true);
assert.equal(output.includes("canon_event=STRAIGHT_FEED"), true);
assert.equal(output.includes("a=15"), true);
assert.equal(output.includes("c=30"), true);
console.log("full_linuxcnc_5axis_source_node_smoke=ok");

View File

@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import {
applyIniConfigToProfile,
parseLinuxCncIni,
} from "../../app/src/runtime/linuxcnc-ini-runtime.js";
import { xyzacTrtProfile } from "../../app/src/profiles/xyzac-trt.js";
import { xyzbcTrtProfile } from "../../app/src/profiles/xyzbc-trt.js";
const xyzacIniText = await readFile(
new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", import.meta.url),
"utf8",
);
const xyzbcIniText = await readFile(
new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", import.meta.url),
"utf8",
);
const xyzacIni = parseLinuxCncIni(xyzacIniText, {
path: xyzacTrtProfile.iniPath,
profileId: xyzacTrtProfile.id,
});
assert.equal(xyzacIni.apiName, "web-rtcp-5axis-linuxcnc-ini-config");
assert.equal(xyzacIni.validation.ready, true);
assert.equal(xyzacIni.machineName, "sim-xyzac-trt-kins (switchkins)");
assert.equal(xyzacIni.kinematics.name, "xyzac-trt-kins");
assert.equal(xyzacIni.kinematicsParameters.sparm, "identityfirst");
assert.equal(xyzacIni.kinematicsParameters.joints, 5);
assert.equal(xyzacIni.traj.coordinates, "XYZAC");
assert.equal(xyzacIni.axisLimits.A.max, 50);
assert.equal(xyzacIni.jointConfig[3].axis, "A");
assert.equal(xyzacIni.jointConfig[4].max, 36000);
assert.deepEqual(xyzacIni.halui.mdiCommands, ["M429", "M428", "M430"]);
assert.equal(xyzacIni.rs274ngc.remaps.map((remap) => remap.code).join(","), "M428,M429,M430");
assert.equal(xyzacIni.hal.initialSets.some((set) => set.pin === "y-offset" && set.value === 20), true);
const derivedXyzac = applyIniConfigToProfile(xyzacTrtProfile, xyzacIni);
assert.equal(derivedXyzac.machineName, xyzacIni.machineName);
assert.equal(derivedXyzac.traj.coordinates, "XYZAC");
assert.equal(derivedXyzac.axisLimits.X.min, -200);
assert.equal(derivedXyzac.jointConfig.length, 5);
assert.equal(derivedXyzac.linuxCncIniConfig.path.endsWith("xyzac-trt.ini"), true);
const xyzbcIni = parseLinuxCncIni(xyzbcIniText, {
path: xyzbcTrtProfile.iniPath,
profileId: xyzbcTrtProfile.id,
});
assert.equal(xyzbcIni.validation.ready, true);
assert.equal(xyzbcIni.kinematics.name, "xyzbc-trt-kins");
assert.equal(xyzbcIni.kinematicsModuleId, "xyzbc-trt");
assert.equal(xyzbcIni.traj.coordinates, "XYZBC");
assert.equal(xyzbcIni.axisLimits.B.max, 36000);
assert.equal(xyzbcIni.jointConfig[3].axis, "B");
assert.equal(xyzbcIni.kinematicsParameters.switchkinsTypes[1].webKinsType, "tcp-xyzbc");
console.log("linuxcnc_ini_runtime_smoke=ok");

View File

@@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
const runtime = await createLinuxCncInterpreterRuntime();
const readiness = runtime.readiness();
assert.equal(runtime.apiName, "web-rtcp-5axis-linuxcnc-interpreter-runtime");
assert.equal(runtime.loaded, true);
assert.equal(runtime.sourceMode, "linuxcnc-interpreter-wasm");
assert.equal(runtime.semanticBoundary, "linuxcnc_interpreter_wasm_canonical_events");
assert.equal(readiness.loaded, true);
assert.equal(readiness.runProgramReady, true);
assert.equal(readiness.remapRuntimeReady, false);
assert.equal(readiness.plannerRuntimeReady, true);
assert.equal(readiness.plannerSemanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion");
const programText = [
"G90 G17",
"G0 X0 Y0 Z0",
"G1 X10 Y2 F100",
"G1 X12 Y4 A5 C7",
"M2",
].join("\n");
const execution = runtime.runProgram(programText);
assert.equal(execution.apiName, "web-rtcp-5axis-linuxcnc-interpreter-program-execution");
assert.equal(execution.sourceMode, "linuxcnc-interpreter-wasm");
assert.equal(execution.semanticBoundary, "linuxcnc_interpreter_wasm_canonical_events");
assert.equal(execution.summary.ready, true);
assert.equal(execution.summary.motionEventCount >= 3, true);
assert.equal(execution.summary.canonicalEventCount > execution.summary.motionEventCount, true);
assert.equal(execution.summary.motionTypes.includes("STRAIGHT_TRAVERSE"), true);
assert.equal(execution.summary.motionTypes.includes("STRAIGHT_FEED"), true);
assert.equal(execution.summary.finalAxes.x, 12);
assert.equal(execution.summary.finalAxes.y, 4);
assert.equal(execution.summary.finalAxes.a, 5);
assert.equal(execution.summary.finalAxes.c, 7);
assert.equal(execution.motion[2].feedRate, 100);
assert.equal(execution.summary.remapRuntimeReady, false);
assert.equal(execution.summary.plannerRuntimeReady, true);
assert.equal(execution.plannerTiming.semanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion");
assert.equal(execution.plannerTiming.totalSeconds > 0, true);
assert.equal(execution.plannerTiming.motionCount, execution.motion.length);
assert.equal(execution.summary.fullLinuxCncProgramExecutionReady, false);
const switchkinsProgramText = [
"G90 G17",
"M428",
"G0 X0 Y0 Z0 A0 C0",
"G1 X10 Y2 Z-1 A15 C30 F120",
"M429",
"G1 X0 Y0 Z0 A0 C0 F120",
"M2",
].join("\n");
const switchkinsExecution = runtime.runProgram(switchkinsProgramText);
assert.equal(switchkinsExecution.summary.ready, true);
assert.equal(switchkinsExecution.summary.motionEventCount, 3);
assert.equal(switchkinsExecution.summary.switchkinsEventCount, 2);
assert.deepEqual(switchkinsExecution.summary.switchkinsCodes, ["M428", "M429"]);
assert.equal(
switchkinsExecution.summary.switchkinsRemapBoundary,
"linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
);
assert.equal(switchkinsExecution.switchkinsEvents[0].switchkinsType, 1);
assert.equal(switchkinsExecution.switchkinsEvents[1].switchkinsType, 0);
assert.equal(switchkinsExecution.motion[0].kinsType, "tcp");
assert.equal(switchkinsExecution.motion[1].axes.a, 15);
assert.equal(switchkinsExecution.motion[1].axes.c, 30);
assert.equal(switchkinsExecution.motion[1].switchkinsCode, "M428");
assert.equal(switchkinsExecution.motion[2].kinsType, "identity");
assert.equal(switchkinsExecution.motion[2].switchkinsCode, "M429");
assert.equal(switchkinsExecution.summary.remapRuntimeReady, false);
assert.equal(switchkinsExecution.summary.plannerRuntimeReady, true);
assert.equal(switchkinsExecution.plannerTiming.totalSeconds > 0, true);
assert.equal(switchkinsExecution.summary.fullLinuxCncProgramExecutionReady, false);
console.log("linuxcnc_interpreter_runtime_smoke=ok");

View File

@@ -11,8 +11,10 @@ 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.executionContext, "direct");
assert.equal(runtime.wasmFile, "linuxcnc_xyzac_trt_kinematics.wasm");
assert.equal(readiness.loaded, true);
assert.equal(readiness.executionContext, "direct");
assert.equal(readiness.supportedModules.includes("xyzac-trt"), true);
assert.equal(runtime.switchRc, 0);

View File

@@ -0,0 +1,128 @@
import assert from "node:assert/strict";
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
import {
createMachineFileStagingPlan,
selectMachineFileProgram,
stageProfileMachineFiles,
} from "../../app/src/runtime/linuxcnc-machine-file-staging.js";
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
import { createSimulationStore } from "../../app/src/state/store.js";
async function waitForMachineFileExecution(store) {
for (let attempt = 0; attempt < 20; attempt += 1) {
const state = store.getState();
if (!state.interpreterExecutionPending && state.machineFileExecution) {
return state;
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
return store.getState();
}
const profile = getFiveAxisProfile("xyzac-trt");
const storage = createMemorySessionStorage();
const plan = await createMachineFileStagingPlan({ profile });
assert.equal(plan.apiName, "web-rtcp-5axis-machine-file-staging-plan");
assert.equal(plan.profileId, "xyzac-trt");
assert.equal(plan.semanticBoundary, "linuxcnc_sim_config_file_staging_plan_only");
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("xyzac-trt.ini")), true);
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("xyzac-trt.tbl")), true);
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc")), true);
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("remap_subs/429remap.ngc")), true);
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("demos/xyzac_switchkins.ngc")), true);
assert.equal(plan.summary.remapFileCount >= 3, true);
assert.equal(plan.summary.demoFileCount >= 1, true);
const staged = await stageProfileMachineFiles(profile, { storage });
assert.equal(staged.save.apiName, "web-rtcp-5axis-machine-file-staging-save");
assert.equal(staged.save.status, "saved");
assert.equal(staged.save.fileCount, staged.plan.files.length);
assert.equal(staged.save.semanticBoundary, "opfs_machine_file_text_staging_only");
assert.equal(staged.save.summary.kinds.remap >= 3, true);
assert.equal(staged.save.gcodeSources.some((source) => source.filename === "impeller-7bl-xyzac.ngc"), true);
assert.equal(staged.save.gcodeSources.some((source) => source.filename === "boat-xyzac.ngc"), true);
assert.equal(staged.save.files.every((file) => file.opfsPath.startsWith("web-rtcp-5axis-sim-plan/machines/xyzac-trt/")), true);
assert.throws(
() => selectMachineFileProgram(staged.plan, staged.save, "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc"),
/must come from LinuxCNC source demos/,
);
const iniOpfsPath = staged.save.files.find((file) => file.sourceRel.endsWith("xyzac-trt.ini")).opfsPath;
assert.equal(storage.files.get(iniOpfsPath).includes("KINEMATICS = xyzac-trt-kins"), true);
const store = createSimulationStore();
store.dispatch({
type: "ATTACH_INTERPRETER_RUNTIME",
runtime: await createLinuxCncInterpreterRuntime(),
});
const storeStage = await store.stageMachineFiles({ storage });
const state = store.getState();
assert.equal(storeStage.save.fileCount, staged.save.fileCount);
assert.equal(state.machineFileStaging.status, "staged");
assert.equal(state.machineFileStaging.profileId, "xyzac-trt");
assert.equal(state.machineFileStaging.fileCount, staged.save.fileCount);
assert.equal(state.machineFileStaging.save.summary.kinds.remap >= 3, true);
assert.equal(state.machineFileStaging.gcodeSources.length >= 4, true);
assert.equal(state.machineFileStaging.selectedGcodeSourceRel, null);
store.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
assert.equal(
store.getState().operatorMessage,
"machine-file run blocked: select a LinuxCNC source-directory 5-axis G-code program",
);
store.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: "operator-demo.ngc",
});
assert.equal(
store.getState().operatorMessage,
"LinuxCNC G-code source not staged: operator-demo.ngc",
);
store.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
});
await new Promise((resolve) => setTimeout(resolve, 0));
const sourceState = store.getState();
assert.equal(sourceState.programSource, "linuxcnc-vendored-5axis-gcode");
assert.equal(sourceState.activeProgram.endsWith("impeller-7bl-xyzac.ngc"), true);
assert.equal(sourceState.programLines[0].includes("Impeller 5-axis"), true);
assert.equal(sourceState.machineFileStaging.selectedGcodeSourceRel.endsWith("impeller-7bl-xyzac.ngc"), true);
assert.equal(sourceState.machineFileStaging.plan.wasmProgramPath.endsWith("/demos/impeller-7bl-xyzac.ngc"), true);
store.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
const runState = await waitForMachineFileExecution(store);
assert.equal(runState.machineFileExecution.sourceMode, "linuxcnc-machine-file-remap-wasm");
assert.equal(runState.machineFileExecution.semanticBoundary, "linuxcnc_fiveaxis_remap_wasm_machine_file_execution");
assert.equal(runState.machineFileExecution.summary.machineFileExecutionReady, true);
assert.equal(runState.machineFileExecution.summary.remapRuntimeReady, true);
assert.equal(runState.machineFileExecution.summary.plannerRuntimeReady, false);
assert.equal(runState.machineFileExecution.summary.fullLinuxCncProgramExecutionReady, false);
assert.equal(runState.machineFileExecution.resultText.includes("fiveaxis_ini_open=1"), true);
assert.equal(runState.machineFileExecution.resultText.includes("fiveaxis_remaps_ready=1"), true);
assert.equal(runState.machineFileExecution.resultText.includes("fiveaxis_file_reached_exit=1"), true);
assert.equal(runState.machineFileExecution.machineFilePlan.selectedProgramFilename, "impeller-7bl-xyzac.ngc");
assert.equal(runState.machineFileExecution.machineFilePlan.wasmProgramPath.endsWith("/demos/impeller-7bl-xyzac.ngc"), true);
assert.equal(runState.fullExecutionBoundary.apiName, "web-rtcp-5axis-full-linuxcnc-execution-boundary");
assert.equal(runState.fullExecutionBoundary.machineFileBackedRemapReady, true);
assert.equal(runState.fullExecutionBoundary.remapRuntimeReady, true);
assert.equal(runState.fullExecutionBoundary.halSwitchkinsEvidenceReady, true);
assert.equal(runState.fullExecutionBoundary.plannerRuntimeReady, false);
assert.equal(runState.fullExecutionBoundary.nativeTaskReady, false);
assert.equal(runState.fullExecutionBoundary.nativeHalSyncReady, false);
assert.equal(runState.fullExecutionBoundary.fullLinuxCncProgramExecutionReady, false);
assert.equal(runState.fullExecutionBoundary.promotionAllowed, false);
assert.equal(
runState.fullExecutionBoundary.semanticBoundary,
"linuxcnc_machine_file_remap_ready_planner_task_hal_blocked",
);
assert.equal(runState.fullExecutionBoundary.satisfied.includes("fiveaxis-remap-machine-file-run"), true);
assert.equal(runState.fullExecutionBoundary.satisfied.includes("switchkins-hal-bridge-evidence"), true);
assert.equal(runState.fullExecutionBoundary.blockers.some((blocker) => blocker.includes("trajectory planner")), true);
console.log("machine_file_staging_smoke=ok");

View File

@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { xyzacTrtProfile } from "../../app/src/profiles/xyzac-trt.js";
import { getFiveAxisProfile, fiveAxisProfiles } from "../../app/src/profiles/index.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";
@@ -29,6 +30,20 @@ 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);
assert.deepEqual(fiveAxisProfiles.map(({ id }) => id), ["xyzac-trt", "xyzbc-trt"]);
const xyzbcTrtProfile = getFiveAxisProfile("xyzbc-trt");
assert.equal(xyzbcTrtProfile.id, "xyzbc-trt");
assert.equal(xyzbcTrtProfile.iniPath.endsWith("xyzbc-trt.ini"), true);
assert.deepEqual(xyzbcTrtProfile.coordinates, ["X", "Y", "Z", "B", "C"]);
assert.equal(xyzbcTrtProfile.kinematics, "xyzbc-trt-kins");
assert.equal(xyzbcTrtProfile.kinematicsModuleId, "xyzbc-trt");
assert.equal(xyzbcTrtProfile.machineName, "sim-xyzbc-trt-kins (switchkins)");
assert.equal(xyzbcTrtProfile.traj.coordinates, "XYZBC");
assert.equal(xyzbcTrtProfile.axisLimits.B.max, 36000);
assert.equal(xyzbcTrtProfile.hal.halcmd.feedbackNets.some((net) => net.target === "xyzbc-trt-gui.tilt-b"), true);
assert.equal(xyzbcTrtProfile.halPins.includes("xyzbc-trt-kins.x-offset"), true);
assert.equal(xyzbcTrtProfile.panelSchema.id, "xyzbc-trt-switchkins-pyvcp");
const sourceSummary = createProfileSourceReferenceSummary("xyzac-trt");
assert.equal(sourceSummary.referenceCount >= 8, true);
@@ -39,6 +54,11 @@ assert.equal(sourceSummary.semanticBoundary, "profile_source_map_only_not_runtim
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("xyzac-trt.ini")));
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("trtfuncs.c")));
const xyzbcSourceSummary = createProfileSourceReferenceSummary("xyzbc-trt");
assert.equal(xyzbcSourceSummary.referenceCount >= 7, true);
assert.ok(xyzbcSourceSummary.references.some((reference) => reference.path.endsWith("xyzbc-trt.ini")));
assert.ok(xyzbcSourceSummary.references.some((reference) => reference.path.endsWith("xyzbc-trt-kins.c")));
const panelSummary = createPyvcpHalBindingSummary(xyzacTrtPyvcpPanelSchema);
assert.equal(panelSummary.schemaId, "xyzac-trt-switchkins-pyvcp");
assert.equal(panelSummary.controlCount, 5);

View File

@@ -1,8 +1,22 @@
import assert from "node:assert/strict";
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
import { createSimulationStore } from "../../app/src/state/store.js";
import { readFile } from "node:fs/promises";
async function waitForInterpreterExecution(store) {
for (let attempt = 0; attempt < 20; attempt += 1) {
const state = store.getState();
if (!state.interpreterExecutionPending && state.programExecutionSourceMode === "linuxcnc-interpreter-wasm") {
return state;
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
return store.getState();
}
const identityFrame = buildRtcpFrame({
axisPose: { x: 43, y: -32.15, z: -11.306, a: 0, b: 0, c: 0 },
@@ -34,6 +48,7 @@ assert.ok(Number.isFinite(tcpFrame.toolAxisVector.z));
assert.equal(tcpFrame.jointPose.length, 5);
const runtime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" });
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
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 },
@@ -65,6 +80,37 @@ assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.profileSumma
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.profileSummary.coordinates, "XYZAC");
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.ready, false);
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.promotionAllowed, false);
assert.equal(fixtureInitialStore.getState().fullExecutionBoundary.fullLinuxCncProgramExecutionReady, false);
assert.equal(fixtureInitialStore.getState().fullExecutionBoundary.promotionAllowed, false);
assert.equal(fixtureInitialStore.getState().fullExecutionBoundary.phase, "blocked");
assert.equal(fixtureInitialStore.getState().linuxCncTaskPolicy.semanticBoundary, "linuxcnc_task_state_mode_command_gate");
assert.equal(
fixtureInitialStore.getState().linuxCncTaskPolicy.sourceReferences.some((reference) => reference.path === "linuxcnc/src/emc/task/emctaskmain.cc"),
true,
);
assert.equal(fixtureInitialStore.getState().linuxCncTaskPolicy.canRunAuto, false);
const xyzacIniText = await readFile(
new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", import.meta.url),
"utf8",
);
const xyzacIniConfig = parseLinuxCncIni(xyzacIniText, {
path: fixtureInitialStore.getState().profile.iniPath,
profileId: fixtureInitialStore.getState().machineProfile,
});
fixtureInitialStore.dispatch({ type: "ATTACH_INI_CONFIG", iniConfig: xyzacIniConfig });
assert.equal(fixtureInitialStore.getState().iniConfigReadiness.loaded, true);
assert.equal(fixtureInitialStore.getState().iniConfigReadiness.ready, true);
assert.equal(fixtureInitialStore.getState().profile.axisLimits.X.max, 200);
fixtureInitialStore.dispatch({ type: "TOGGLE_POWER" });
fixtureInitialStore.dispatch({ type: "HOME" });
fixtureInitialStore.dispatch({ type: "SET_MODE", mode: "mdi" });
fixtureInitialStore.dispatch({ type: "RUN_MDI", command: "G90 X999 Y-999 Z999 A999 C99999" });
assert.equal(fixtureInitialStore.getState().axisPose.x, 200);
assert.equal(fixtureInitialStore.getState().axisPose.y, -100);
assert.equal(fixtureInitialStore.getState().axisPose.z, 120);
assert.equal(fixtureInitialStore.getState().axisPose.a, 50);
assert.equal(fixtureInitialStore.getState().axisPose.c, 36000);
const kinematicsStore = createSimulationStore();
kinematicsStore.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime });
@@ -74,6 +120,9 @@ assert.equal(kinematicsState.linuxCncBoundaryAdapter.interpreterRuntimeReady, fa
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.fullExecutionBoundary.satisfied.includes("linuxcnc-kinematics-wasm"), true);
assert.equal(kinematicsState.fullExecutionBoundary.missing.includes("linuxcnc interpreter WASM runtime"), true);
assert.equal(kinematicsState.kinematicsExecutionContext, "direct");
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);
@@ -86,27 +135,159 @@ assert.equal(kinematicsState.lastKinematicsResult.moduleId, "xyzac-trt");
assert.equal(kinematicsState.dro.tcpX, kinematicsState.rtcpFrame.tcpPose.x);
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
kinematicsStore.dispatch({ type: "HOME" });
kinematicsStore.dispatch({ type: "SET_MODE", mode: "auto" });
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: "ATTACH_INTERPRETER_RUNTIME", runtime: interpreterRuntime });
kinematicsStore.dispatch({
type: "LOAD_PROGRAM",
filename: "linuxcnc-canonical-demo.ngc",
content: [
"G90 G17",
"G0 X0 Y0 Z0",
"G1 X10 Y2 F100",
"G1 X12 Y4 A5 C7",
"M2",
].join("\n"),
});
kinematicsState = await waitForInterpreterExecution(kinematicsStore);
assert.equal(kinematicsState.linuxCncBoundaryAdapter.linuxCncInterpreterReady, true);
assert.equal(kinematicsState.linuxCncBoundaryReadiness.linuxCncInterpreterReady, true);
assert.equal(kinematicsState.programExecutionSourceMode, "linuxcnc-interpreter-wasm");
assert.equal(kinematicsState.programExecution.summary.motionEventCount >= 3, true);
assert.equal(kinematicsState.programExecutionTiming.motionCount >= 3, true);
assert.equal(kinematicsState.programExecutionTiming.sampleCount >= 3, true);
assert.equal(kinematicsState.programExecutionTiming.totalSeconds > 0, true);
assert.equal(kinematicsState.programExecutionTiming.semanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion");
assert.equal(kinematicsState.programExecution.summary.plannerRuntimeReady, true);
assert.equal(kinematicsState.fullExecutionBoundary.plannerRuntimeReady, true);
assert.equal(kinematicsState.programExecution.summary.fullLinuxCncProgramExecutionReady, false);
assert.equal(kinematicsState.fullExecutionBoundary.readyForUiSimulation, true);
assert.equal(kinematicsState.fullExecutionBoundary.machineFileBackedRemapReady, false);
assert.equal(kinematicsState.fullExecutionBoundary.fullLinuxCncProgramExecutionReady, false);
assert.equal(
kinematicsState.fullExecutionBoundary.semanticBoundary,
"linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked",
);
kinematicsStore.dispatch({ type: "STEP" });
kinematicsState = kinematicsStore.getState();
assert.equal(kinematicsState.programExecutionMotionIndex, 1);
assert.equal(kinematicsState.activeLine, kinematicsState.programExecution.motion[1].line);
assert.equal(kinematicsState.programRuntimeFeedback.sourceMode, "linuxcnc-tp-runtime-sample");
assert.equal(kinematicsState.programRuntimeFeedback.semanticBoundary, "linuxcnc_tp_run_cycle_feedback_without_hardware");
assert.equal(kinematicsState.axisPose.x, kinematicsState.programRuntimeFeedback.axisPose.x);
assert.notEqual(kinematicsState.axisPose.x, kinematicsState.programExecution.motion[1].axes.x);
assert.equal(kinematicsState.programRuntimeFeedback.queueDepth >= 0, true);
assert.equal(kinematicsState.programElapsedSeconds > 0, true);
assert.equal(kinematicsState.programRemainingSeconds >= 0, true);
assert.equal(kinematicsState.feed.currentVelocity > 0, true);
kinematicsStore.dispatch({
type: "LOAD_PROGRAM",
filename: "linuxcnc-switchkins-rtcp-demo.ngc",
content: [
"G90 G17",
"M428",
"G0 X0 Y0 Z0 A0 C0",
"G1 X10 Y2 Z-1 A15 C30 F120",
"M429",
"G1 X0 Y0 Z0 A0 C0 F120",
"M2",
].join("\n"),
});
kinematicsState = await waitForInterpreterExecution(kinematicsStore);
assert.equal(kinematicsState.programExecutionSourceMode, "linuxcnc-interpreter-wasm");
assert.equal(kinematicsState.programExecution.summary.switchkinsEventCount, 2);
assert.equal(kinematicsState.programExecution.motion[0].switchkinsType, 1);
assert.equal(kinematicsState.kinsType, "tcp-xyzac");
assert.equal(kinematicsState.rtcpState, "on");
assert.equal(kinematicsState.rtcpFrame.kinematicsSwitchkinsType, 1);
kinematicsStore.dispatch({ type: "STEP" });
kinematicsState = kinematicsStore.getState();
assert.equal(kinematicsState.programExecutionMotionIndex, 1);
assert.equal(kinematicsState.programRuntimeFeedback.semanticBoundary, "linuxcnc_tp_run_cycle_feedback_without_hardware");
assert.equal(kinematicsState.axisPose.a > 0 && kinematicsState.axisPose.a < 15, true);
assert.equal(kinematicsState.axisPose.c > 0 && kinematicsState.axisPose.c < 30, true);
assert.equal(kinematicsState.programRuntimeFeedback.distanceToGo > 0, true);
assert.equal(kinematicsState.kinsType, "tcp-xyzac");
assert.equal(kinematicsState.rtcpState, "on");
assert.equal(kinematicsState.rtcpFrame.kinematicsSwitchkinsType, 1);
const switchkinsStepSampleIndex = kinematicsState.programExecutionSampleIndex;
kinematicsStore.dispatch({ type: "RESUME" });
kinematicsStore.dispatch({ type: "RUN" });
kinematicsState = kinematicsStore.getState();
assert.equal(kinematicsState.programExecutionSampleIndex > switchkinsStepSampleIndex, true);
assert.equal(kinematicsState.programExecutionMotionIndex, 1);
assert.equal(kinematicsState.programRuntimeFeedback.semanticBoundary, "linuxcnc_tp_run_cycle_feedback_without_hardware");
assert.equal(kinematicsState.programRuntimeFeedback.distanceToGo > 0, true);
assert.equal(kinematicsState.kinsType, "tcp-xyzac");
assert.equal(kinematicsState.rtcpState, "on");
assert.equal(kinematicsState.rtcpFrame.kinematicsSwitchkinsType, 1);
kinematicsStore.dispatch({ type: "PAUSE" });
kinematicsState = kinematicsStore.getState();
assert.equal(kinematicsState.runState, "paused");
assert.equal(kinematicsState.machine.interpState, "paused");
assert.equal(kinematicsState.machine.taskPaused, true);
kinematicsStore.dispatch({ type: "RUN" });
kinematicsState = kinematicsStore.getState();
assert.equal(kinematicsState.operatorMessage, "run blocked: resume paused program first");
assert.equal(kinematicsState.runState, "paused");
kinematicsStore.dispatch({ type: "RESUME" });
kinematicsState = kinematicsStore.getState();
assert.equal(kinematicsState.runState, "running");
assert.equal(kinematicsState.machine.interpState, "reading");
assert.equal(kinematicsState.machine.taskPaused, false);
kinematicsStore.dispatch({ type: "STOP" });
kinematicsState = kinematicsStore.getState();
assert.equal(kinematicsState.runState, "stopped");
assert.equal(kinematicsState.machine.interpState, "idle");
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();
let state;
assert.equal(store.getState().availableProfiles.length, 2);
store.dispatch({ type: "SET_PROFILE", profileId: "xyzbc-trt" });
assert.equal(store.getState().machineProfile, "xyzbc-trt");
assert.equal(store.getState().profile.traj.coordinates, "XYZBC");
const xyzbcRuntime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzbc-trt" });
store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime: xyzbcRuntime });
store.dispatch({ type: "SET_RTCP", enabled: true });
state = store.getState();
assert.equal(state.kinsType, "tcp-xyzbc");
assert.equal(state.rtcpFrame.kinematicsModuleId, "xyzbc-trt");
assert.equal(state.rtcpFrame.jointPose[3].axis, "B");
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
store.dispatch({ type: "RUN" });
assert.equal(store.getState().activeLine, 501);
assert.equal(store.getState().operatorMessage, "run blocked: power or estop state");
assert.equal(store.getState().operatorMessage, "run blocked: machine must be on");
store.dispatch({ type: "TOGGLE_POWER" });
assert.equal(store.getState().machine.powerOn, true);
store.dispatch({ type: "HOME" });
assert.equal(store.getState().machine.allHomed, true);
store.dispatch({ type: "SET_MODE", mode: "auto" });
assert.equal(store.getState().linuxCncTaskPolicy.canRunAuto, true);
assert.equal(store.getState().linuxCncTaskPolicy.canExecuteMdi, false);
store.dispatch({ type: "SET_RTCP", enabled: true });
let state = store.getState();
state = store.getState();
assert.equal(state.rtcpState, "on");
assert.equal(state.kinsType, "tcp-xyzac");
assert.equal(state.rtcpFrame.readiness.linuxCncKinematicsReady, false);
@@ -120,24 +301,55 @@ assert.equal(state.activeLine, previousLine + 1);
assert.equal(state.runState, "stepping");
assert.notEqual(state.tcpPose.x, previousTcpX);
store.dispatch({ type: "STOP" });
store.dispatch({ type: "SET_MODE", mode: "manual" });
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 2 });
state = store.getState();
assert.equal(state.machine.mode, "jog");
assert.equal(state.machine.mode, "manual");
assert.equal(state.runState, "jogging");
assert.equal(Math.round(state.axisPose.x), Math.round(state.rtcpFrame.axisPose.x));
store.dispatch({ type: "SET_MODE", mode: "mdi" });
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");
assert.equal(state.axisPose.x, 1);
assert.equal(state.programSource, "operator-mdi");
assert.equal(state.programLines[0], "G0 X1");
assert.equal(state.mdiHistory[0], "G0 X1");
const mdiRelativeBase = store.getState().axisPose;
store.dispatch({ type: "RUN_MDI", command: "G91 X2 Y-3 F1200 M3 S2400 M8" });
state = store.getState();
assert.equal(state.machine.mdiDistanceMode, "relative");
assert.equal(state.axisPose.x, mdiRelativeBase.x + 2);
assert.equal(state.axisPose.y, mdiRelativeBase.y - 3);
assert.equal(state.feed.feedRate, 1200);
assert.equal(state.spindle.enabled, true);
assert.equal(state.spindle.rpm, 2400);
assert.equal(state.coolant.flood, true);
store.dispatch({ type: "RUN_MDI", command: "M428" });
state = store.getState();
assert.equal(state.kinsType, "tcp-xyzac");
assert.equal(state.rtcpState, "on");
store.dispatch({ type: "RUN_MDI", command: "M429 M9 M5" });
state = store.getState();
assert.equal(state.kinsType, "identity");
assert.equal(state.rtcpState, "off");
assert.equal(state.coolant.flood, false);
assert.equal(state.coolant.mist, false);
assert.equal(state.spindle.enabled, false);
store.dispatch({
type: "LOAD_PROGRAM",
filename: "operator-demo.ngc",
content: "G0 X0 Y0\nG1 X10 F100\nM30\n",
});
state = store.getState();
state = await waitForInterpreterExecution(store);
assert.equal(state.activeProgram, "operator-demo.ngc");
assert.equal(state.programSource, "operator-file");
assert.equal(state.programStartLine, 1);
@@ -169,13 +381,15 @@ store.dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: 10 });
state = store.getState();
assert.equal(state.spindle.override, 110);
const floodBeforeToggle = store.getState().coolant.flood;
store.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
state = store.getState();
assert.equal(state.coolant.flood, false);
assert.equal(state.coolant.flood, !floodBeforeToggle);
const mistBeforeToggle = store.getState().coolant.mist;
store.dispatch({ type: "TOGGLE_COOLANT", kind: "mist" });
state = store.getState();
assert.equal(state.coolant.mist, true);
assert.equal(state.coolant.mist, !mistBeforeToggle);
store.dispatch({ type: "SET_VIEW", view: "x" });
state = store.getState();
@@ -195,6 +409,7 @@ store.dispatch({ type: "TOGGLE_FULLSCREEN" });
state = store.getState();
assert.equal(state.preview.fullscreen, true);
store.dispatch({ type: "SET_MODE", mode: "manual" });
store.dispatch({ type: "HOME" });
state = store.getState();
assert.equal(state.axisPose.x, 43);
@@ -209,6 +424,8 @@ 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");
assert.equal(state.machine.powerOn, false);
assert.equal(state.machine.taskState, "estop-reset");
assert.equal(state.operatorMessage, "estop reset; machine off");
console.log("rtcp_store_smoke=ok");