提交当前项目改动
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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,77 @@
|
||||
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");
|
||||
assert.equal(saved.storageMode, "memory");
|
||||
assert.equal(store.getState().sessionPersistence.storageMode, "memory");
|
||||
|
||||
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(restored.storageMode, "memory");
|
||||
assert.equal(afterRestore.sessionPersistence.status, "restored");
|
||||
assert.equal(afterRestore.sessionPersistence.storageMode, "memory");
|
||||
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");
|
||||
@@ -0,0 +1,229 @@
|
||||
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.missing.includes("tool DB Web/WASM simulation process"), true);
|
||||
assert.equal(blocked.missing.includes("controlled user-M Web/WASM simulation process"), true);
|
||||
assert.equal(blocked.blockers.some((blocker) => blocker.includes("LinuxCNC task runtime")), 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);
|
||||
|
||||
const gmoccapyMachineFile = 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: 18,
|
||||
},
|
||||
machineFileExecution: {
|
||||
sourceMode: "linuxcnc-machine-file-remap-wasm",
|
||||
summary: {
|
||||
machineFileExecutionReady: true,
|
||||
remapRuntimeReady: false,
|
||||
},
|
||||
resultText: [
|
||||
"fiveaxis_parse_remap_1=5",
|
||||
"fiveaxis_parse_remap_2=5",
|
||||
"fiveaxis_remaps_ready=0",
|
||||
"fiveaxis_file_reached_exit=1",
|
||||
"fiveaxis_linuxcnc_remap_file_execute=0",
|
||||
].join("\n"),
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(gmoccapyMachineFile.machineFileBackedRemapReady, true);
|
||||
assert.equal(gmoccapyMachineFile.remapRuntimeReady, false);
|
||||
assert.equal(gmoccapyMachineFile.semanticBoundary, "linuxcnc_machine_file_remap_ready_planner_task_hal_blocked");
|
||||
assert.equal(gmoccapyMachineFile.satisfied.includes("fiveaxis-remap-machine-file-run"), true);
|
||||
assert.equal(gmoccapyMachineFile.evidence.machineFileFlags.length, 1);
|
||||
|
||||
const taskHalPromoted = createFullLinuxCncExecutionBoundary({
|
||||
...canonicalState,
|
||||
programExecution: remap.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: remap.evidence ? {
|
||||
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=1",
|
||||
].join("\n"),
|
||||
} : null,
|
||||
taskHalRuntimeReadiness: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
},
|
||||
taskHalStatus: {
|
||||
summary: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
taskHalComparisonReady: true,
|
||||
},
|
||||
ui: {
|
||||
taskCycle: 2,
|
||||
servoCycle: 20,
|
||||
motionQueueDepth: 0,
|
||||
halChangedPinCount: 4,
|
||||
},
|
||||
},
|
||||
toolDbReadiness: {
|
||||
toolDbProcessReady: true,
|
||||
toolDbProcessScope: "web_simulation_only",
|
||||
hostToolDbProcessReady: false,
|
||||
toolCount: 10,
|
||||
},
|
||||
controlledUserMReadiness: {
|
||||
externalUserMProcessReady: true,
|
||||
externalUserMProcessScope: "web_simulation_only",
|
||||
hostExternalUserMProcessReady: false,
|
||||
arbitraryUserMExecution: false,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(taskHalPromoted.semanticBoundary, "linuxcnc_task_motion_hal_wasm_simulation_runtime");
|
||||
assert.equal(taskHalPromoted.nativeTaskReady, true);
|
||||
assert.equal(taskHalPromoted.nativeHalSyncReady, true);
|
||||
assert.equal(taskHalPromoted.fullLinuxCncProgramExecutionReady, true);
|
||||
assert.equal(taskHalPromoted.promotionAllowed, true);
|
||||
assert.equal(taskHalPromoted.hardwareDrive, false);
|
||||
assert.equal(taskHalPromoted.hostRealtimeKernel, false);
|
||||
assert.equal(taskHalPromoted.externalUserMProcessReady, true);
|
||||
assert.equal(taskHalPromoted.externalUserMProcessScope, "web_simulation_only");
|
||||
assert.equal(taskHalPromoted.toolDbProcessReady, true);
|
||||
assert.equal(taskHalPromoted.toolDbProcessScope, "web_simulation_only");
|
||||
assert.equal(taskHalPromoted.hostExternalUserMProcessReady, false);
|
||||
assert.equal(taskHalPromoted.hostToolDbProcessReady, false);
|
||||
assert.equal(taskHalPromoted.arbitraryUserMExecution, false);
|
||||
assert.equal(taskHalPromoted.satisfied.includes("tool-db-web-simulation"), true);
|
||||
assert.equal(taskHalPromoted.satisfied.includes("controlled-user-m-web-simulation"), true);
|
||||
assert.equal(taskHalPromoted.evidence.taskCycle, 2);
|
||||
|
||||
console.log("full_execution_boundary_smoke=ok");
|
||||
@@ -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");
|
||||
@@ -0,0 +1,152 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
createGmoccapyCommunicationSummary,
|
||||
gmoccapyCommunicationModel,
|
||||
} from "../../app/src/runtime/gmoccapy-communication-model.js";
|
||||
|
||||
const model = gmoccapyCommunicationModel;
|
||||
|
||||
assert.equal(model.apiName, "web-rtcp-5axis-gmoccapy-communication-model");
|
||||
assert.equal(model.profileId, "gmoccapy-xyzab");
|
||||
assert.equal(model.nativeConfigPath.endsWith("gmoccapy_XYZAB.ini"), true);
|
||||
assert.equal(model.cycleTimeMs, 100);
|
||||
assert.equal(model.semanticBoundary, "native_gmoccapy_nml_hal_reference_web_store_runtime_mapping");
|
||||
|
||||
assert.deepEqual(model.nativePaths.command.path, ["gmoccapy", "linuxcnc.command()", "NML emcCommand", "milltask"]);
|
||||
assert.equal(model.nativePaths.status.path.includes("linuxcnc.stat()"), true);
|
||||
assert.equal(model.nativePaths.status.path.includes("linuxcnc.error_channel()"), true);
|
||||
assert.equal(model.nativePaths.hal.path.includes("HAL shared memory"), true);
|
||||
assert.equal(model.nativePaths.hardwareButtons.path.includes("_button_pin_changed"), true);
|
||||
assert.equal(model.nativePaths.halPins.path.includes("hal_glib.GPin value_changed"), true);
|
||||
assert.equal(model.nativePaths.remap.path.includes("Python remap prolog/ngc/epilog"), true);
|
||||
assert.equal(model.nativePaths.filePage.path.includes("IconFileSelection1"), true);
|
||||
assert.equal(model.nativePaths.macroPage.path.includes("_on_btn_macro_pressed"), true);
|
||||
assert.equal(model.nativePaths.toolEditorPage.path.includes("tooledit1.reload()"), true);
|
||||
assert.equal(model.nativePaths.nativePages.path.includes("Web implementation matrix"), true);
|
||||
assert.deepEqual(model.webPath.path, ["button", "store.dispatch", "linuxcnc-task-policy", "runtime/status snapshot", "UI/Three.js"]);
|
||||
|
||||
assert.equal(model.startupStages.length, 9);
|
||||
assert.equal(model.startupStages[0].id, "parse-ini");
|
||||
assert.equal(model.startupStages.at(-1).id, "postgui");
|
||||
assert.equal(model.startupStages.at(-1).nativeStep.includes("halcomp.ready()"), true);
|
||||
|
||||
const actionIds = model.actionMappings.map((mapping) => mapping.actionId);
|
||||
for (const actionId of [
|
||||
"estop",
|
||||
"estop-reset",
|
||||
"power-on",
|
||||
"mode-manual",
|
||||
"mode-mdi",
|
||||
"mode-auto",
|
||||
"jog",
|
||||
"home",
|
||||
"mdi",
|
||||
"auto-run",
|
||||
"auto-pause",
|
||||
"auto-resume",
|
||||
"abort",
|
||||
"spindle",
|
||||
"coolant",
|
||||
"hardware-button",
|
||||
"hal-pin-input",
|
||||
"hal-settings-unlock",
|
||||
"hal-jog-pin",
|
||||
"hal-tool-measurement",
|
||||
"hal-user-message",
|
||||
"hal-warning-confirm",
|
||||
"file-page",
|
||||
"macro-page",
|
||||
"tool-editor-page",
|
||||
"native-page-diagnostic",
|
||||
]) {
|
||||
assert.equal(actionIds.includes(actionId), true, `${actionId} missing`);
|
||||
}
|
||||
|
||||
const runMapping = model.actionMappings.find((mapping) => mapping.actionId === "auto-run");
|
||||
assert.equal(runMapping.nativeCommand, "command.auto(AUTO_RUN, start_line)");
|
||||
assert.equal(runMapping.webAction, "RUN");
|
||||
assert.equal(runMapping.gate.includes("INI loaded"), true);
|
||||
assert.equal(runMapping.gate.includes("runtime ready"), true);
|
||||
|
||||
const jogMapping = model.actionMappings.find((mapping) => mapping.actionId === "jog");
|
||||
assert.equal(jogMapping.nativeCommand.includes("JOG_CONTINUOUS"), true);
|
||||
assert.equal(jogMapping.gate, "machine on, manual mode, not running");
|
||||
|
||||
const spindleMapping = model.actionMappings.find((mapping) => mapping.actionId === "spindle");
|
||||
assert.equal(spindleMapping.nativeStatus.includes("spindle.0.forward"), true);
|
||||
assert.equal(spindleMapping.gate.includes("restricted during interpreter"), true);
|
||||
|
||||
const hardwareButtonMapping = model.actionMappings.find((mapping) => mapping.actionId === "hardware-button");
|
||||
assert.equal(hardwareButtonMapping.nativeCommand.includes("_button_pin_changed"), true);
|
||||
assert.equal(hardwareButtonMapping.gate.includes("rising-edge"), true);
|
||||
assert.equal(hardwareButtonMapping.gate.includes("insensitive"), true);
|
||||
|
||||
const halPinMapping = model.actionMappings.find((mapping) => mapping.actionId === "hal-pin-input");
|
||||
assert.equal(halPinMapping.webAction, "GMOCAPY_HAL_PIN");
|
||||
assert.equal(halPinMapping.nativeCommand.includes("_optional_blocks"), true);
|
||||
assert.equal(halPinMapping.nativeCommand.includes("_on_counts_changed"), true);
|
||||
assert.equal(halPinMapping.nativeCommand.includes("_del_message_changed"), true);
|
||||
assert.equal(halPinMapping.gate.includes("counts require count-enable"), true);
|
||||
assert.equal(halPinMapping.gate.includes("direct-value requires analog-enable"), true);
|
||||
assert.equal(halPinMapping.gate.includes("delete-message pins are rising-edge only"), true);
|
||||
|
||||
const settingsUnlockMapping = model.actionMappings.find((mapping) => mapping.actionId === "hal-settings-unlock");
|
||||
assert.equal(settingsUnlockMapping.nativeCommand, "_on_unlock_settings_changed");
|
||||
assert.equal(settingsUnlockMapping.gate.includes("unlock_way"), true);
|
||||
assert.equal(settingsUnlockMapping.gate.includes("HAL unlock"), true);
|
||||
|
||||
const halJogMapping = model.actionMappings.find((mapping) => mapping.actionId === "hal-jog-pin");
|
||||
assert.equal(halJogMapping.webAction, "GMOCAPY_HAL_PIN jog.axis/jog-inc");
|
||||
assert.equal(halJogMapping.nativeCommand.includes("_on_pin_jog_changed"), true);
|
||||
assert.equal(halJogMapping.nativeCommand.includes("_on_pin_incr_changed"), true);
|
||||
assert.equal(halJogMapping.gate.includes("press/release level"), true);
|
||||
assert.equal(halJogMapping.gate.includes("increment pins are rising-edge only"), true);
|
||||
|
||||
const toolMeasurementMapping = model.actionMappings.find((mapping) => mapping.actionId === "hal-tool-measurement");
|
||||
assert.equal(toolMeasurementMapping.nativeCommand.includes("_check_toolmeasurement"), true);
|
||||
assert.equal(toolMeasurementMapping.nativeStatus.includes("probeheight"), true);
|
||||
assert.equal(toolMeasurementMapping.gate.includes("no [TOOLSENSOR]"), true);
|
||||
|
||||
const userMessageMapping = model.actionMappings.find((mapping) => mapping.actionId === "hal-user-message");
|
||||
assert.equal(userMessageMapping.nativeCommand.includes("_init_user_messages"), true);
|
||||
assert.equal(userMessageMapping.nativeStatus.includes("messages.<pinname>"), true);
|
||||
assert.equal(userMessageMapping.gate.includes("no MESSAGE_*"), true);
|
||||
|
||||
const warningConfirmMapping = model.actionMappings.find((mapping) => mapping.actionId === "hal-warning-confirm");
|
||||
assert.equal(warningConfirmMapping.nativeCommand.includes("confirm_pin"), true);
|
||||
assert.equal(warningConfirmMapping.gate, "level-polled while warning dialog is active");
|
||||
|
||||
const filePageMapping = model.actionMappings.find((mapping) => mapping.actionId === "file-page");
|
||||
assert.equal(filePageMapping.nativeCommand.includes("IconFileSelection1"), true);
|
||||
assert.equal(filePageMapping.gate.includes("native GTK file chooser"), true);
|
||||
|
||||
const macroPageMapping = model.actionMappings.find((mapping) => mapping.actionId === "macro-page");
|
||||
assert.equal(macroPageMapping.webAction, "GMOCAPY_RUN_MACRO");
|
||||
assert.equal(macroPageMapping.nativeCommand.includes("command.mdi"), true);
|
||||
assert.equal(macroPageMapping.gate.includes("same as RUN_MDI"), true);
|
||||
|
||||
const toolEditorPageMapping = model.actionMappings.find((mapping) => mapping.actionId === "tool-editor-page");
|
||||
assert.equal(toolEditorPageMapping.nativeCommand.includes("_show_tooledit_tab"), true);
|
||||
assert.equal(toolEditorPageMapping.gate.includes("writeback is disabled"), true);
|
||||
|
||||
const nativePageMapping = model.actionMappings.find((mapping) => mapping.actionId === "native-page-diagnostic");
|
||||
assert.equal(nativePageMapping.webAction, "GMOCAPY_NATIVE_PAGE");
|
||||
assert.equal(nativePageMapping.gate.includes("do not emit fake GTK"), true);
|
||||
|
||||
const summary = createGmoccapyCommunicationSummary(model);
|
||||
assert.equal(summary.apiName, "web-rtcp-5axis-gmoccapy-communication-model-summary");
|
||||
assert.equal(summary.profileId, "gmoccapy-xyzab");
|
||||
assert.equal(summary.nativePathCount, 10);
|
||||
assert.equal(summary.startupStageCount, 9);
|
||||
assert.equal(summary.actionCount, 26);
|
||||
assert.equal(summary.pagePathCount, 4);
|
||||
assert.equal(summary.postguiAfterHalcompReady, true);
|
||||
assert.equal(summary.hasNativeNmlCommandPath, true);
|
||||
assert.equal(summary.hasNativeHalPath, true);
|
||||
assert.equal(summary.hasNativeHardwareButtonPath, true);
|
||||
assert.equal(summary.hasNativeHalPinInputPath, true);
|
||||
assert.equal(summary.hasWebStorePath, true);
|
||||
assert.equal(summary.semanticBoundary, model.semanticBoundary);
|
||||
|
||||
console.log("gmoccapy_communication_model_smoke=ok");
|
||||
@@ -0,0 +1,232 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
createGmoccapyHalSummary,
|
||||
gmoccapyHalModel,
|
||||
resolveGmoccapyHardwareButton,
|
||||
} from "../../app/src/runtime/gmoccapy-hal-model.js";
|
||||
|
||||
const model = gmoccapyHalModel;
|
||||
|
||||
assert.equal(model.apiName, "web-rtcp-5axis-gmoccapy-hal-model");
|
||||
assert.equal(model.profileId, "gmoccapy-xyzab");
|
||||
assert.equal(model.nativeConfigPath.endsWith("gmoccapy_XYZAB.ini"), true);
|
||||
assert.equal(model.semanticBoundary, "gmoccapy_hal_pin_postgui_reference_web_diagnostic_only");
|
||||
|
||||
assert.equal(model.halFiles.length, 4);
|
||||
assert.equal(model.halFiles.some((file) => file.path.endsWith("core_sim_XYZAB.hal") && file.stage === "HALFILE"), true);
|
||||
assert.equal(model.halFiles.some((file) => file.path.endsWith("spindle_sim.hal") && file.stage === "HALFILE"), true);
|
||||
assert.equal(model.halFiles.some((file) => file.path.endsWith("simulated_home.hal") && file.stage === "HALFILE"), true);
|
||||
assert.equal(model.halFiles.some((file) => file.path.endsWith("gmoccapy_postgui.hal") && file.requiresHalcompReady), true);
|
||||
|
||||
const groups = new Map(model.nativePins.map((group) => [group.group, group]));
|
||||
for (const groupName of [
|
||||
"hard-buttons",
|
||||
"jog",
|
||||
"override",
|
||||
"operator-inputs",
|
||||
"settings",
|
||||
"tool",
|
||||
"program",
|
||||
"message",
|
||||
"spindle-postgui",
|
||||
]) {
|
||||
assert.equal(groups.has(groupName), true, `${groupName} missing`);
|
||||
}
|
||||
|
||||
assert.equal(groups.get("hard-buttons").pins.includes("gmoccapy.h-button.button-0"), true);
|
||||
assert.equal(groups.get("hard-buttons").pins.includes("gmoccapy.v-button.button-6"), true);
|
||||
assert.equal(groups.get("jog").pins.includes("gmoccapy.jog.axis.jog-b-minus"), true);
|
||||
assert.equal(groups.get("jog").pins.includes("gmoccapy.jog.jog-inc-5"), true);
|
||||
assert.equal(groups.get("jog").pins.includes("gmoccapy.jog.turtle-jog"), true);
|
||||
assert.equal(groups.get("override").pins.includes("gmoccapy.rapid.rapid-override.direct-value"), true);
|
||||
assert.equal(groups.get("override").pins.includes("gmoccapy.feed.feed-override.count-enable"), true);
|
||||
assert.equal(groups.get("override").pins.includes("gmoccapy.spindle.reset-spindle-override"), true);
|
||||
assert.equal(groups.get("operator-inputs").pins.includes("gmoccapy.ignore-limits"), true);
|
||||
assert.equal(groups.get("operator-inputs").pins.includes("gmoccapy.optional-stop"), true);
|
||||
assert.equal(groups.get("operator-inputs").pins.includes("gmoccapy.blockdelete"), true);
|
||||
assert.equal(groups.get("settings").pins.includes("gmoccapy.unlock-settings"), true);
|
||||
assert.equal(groups.get("tool").pins.includes("gmoccapy.toolchange-changed"), true);
|
||||
assert.equal(groups.get("tool").pins.includes("gmoccapy.probeheight"), true);
|
||||
assert.equal(groups.get("tool").pins.includes("gmoccapy.blockheight"), true);
|
||||
assert.equal(groups.get("tool").pins.includes("gmoccapy.toolmeasurement"), true);
|
||||
assert.equal(groups.get("program").pins.includes("gmoccapy.program.current-line"), true);
|
||||
assert.equal(groups.get("message").pins.includes("gmoccapy.error"), true);
|
||||
assert.equal(groups.get("spindle-postgui").pins.includes("gmoccapy.spindle_at_speed_led"), true);
|
||||
|
||||
assert.equal(model.coreNets.length, 10);
|
||||
assert.equal(model.coreNets.some((net) => net.signal === "Xpos" && net.target === "joint.0.motor-pos-fb"), true);
|
||||
assert.equal(model.coreNets.some((net) => net.signal === "Bpos" && net.target === "joint.4.motor-pos-fb"), true);
|
||||
assert.equal(model.coreNets.some((net) => net.signal === "estop-loop"), true);
|
||||
assert.equal(model.coreNets.some((net) => net.signal === "tool-change-loop"), true);
|
||||
|
||||
assert.equal(model.spindleNets.length, 4);
|
||||
assert.equal(model.spindleNets.some((net) => net.signal === "spindle-speed-cmd"), true);
|
||||
assert.equal(model.spindleNets.some((net) => net.signal === "spindle-at-speed"), true);
|
||||
|
||||
assert.equal(model.postguiNets.length, 5);
|
||||
assert.equal(model.postguiNets.every((net) => net.requiresHalcompReady), true);
|
||||
assert.equal(model.postguiNets.some((net) => net.target === "gmoccapy.spindle_feedback_bar"), true);
|
||||
assert.equal(model.postguiNets.some((net) => net.target === "gmoccapy.tooloffset-x"), true);
|
||||
assert.equal(model.postguiNets.some((net) => net.simulationLoop && net.signal === "tool-change-loop"), true);
|
||||
|
||||
assert.equal(model.hardwareButtons.source.methods.includes("_button_pin_changed"), true);
|
||||
assert.equal(model.hardwareButtons.behavior.edge, "rising-edge-only");
|
||||
assert.equal(model.hardwareButtons.behavior.insensitiveTarget, "ignored");
|
||||
assert.deepEqual(model.hardwareButtons.verticalMain.map((button) => button.nativeWidget), [
|
||||
"tbtn_estop",
|
||||
"tbtn_on",
|
||||
"rbt_manual",
|
||||
"rbt_mdi",
|
||||
"rbt_auto",
|
||||
"tbtn_user_tabs",
|
||||
"tbtn_setup",
|
||||
]);
|
||||
assert.deepEqual(model.hardwareButtons.verticalMain.map((button) => button.pin), [
|
||||
"gmoccapy.v-button.button-0",
|
||||
"gmoccapy.v-button.button-1",
|
||||
"gmoccapy.v-button.button-2",
|
||||
"gmoccapy.v-button.button-3",
|
||||
"gmoccapy.v-button.button-4",
|
||||
"gmoccapy.v-button.button-5",
|
||||
"gmoccapy.v-button.button-6",
|
||||
]);
|
||||
assert.equal(model.hardwareButtons.bottomMain.find((button) => button.index === 0).nativeWidget, "btn_homing");
|
||||
assert.equal(model.hardwareButtons.bottomMain.find((button) => button.index === 0).webDispatch.type, "HOME");
|
||||
assert.equal(resolveGmoccapyHardwareButton("gmoccapy.v-button.button-4", model).webDispatch.mode, "auto");
|
||||
assert.equal(resolveGmoccapyHardwareButton({ location: "bottom", index: 0 }, model).webDispatch.type, "HOME");
|
||||
assert.equal(resolveGmoccapyHardwareButton({ location: "right", index: 6 }, model).webDispatch.pageId, "setup");
|
||||
assert.equal(resolveGmoccapyHardwareButton("gmoccapy.h-button.button-9", model), null);
|
||||
assert.equal(resolveGmoccapyHardwareButton({ location: "right", index: 5 }, model).webDispatch.type, "GMOCAPY_NATIVE_PAGE");
|
||||
assert.equal(resolveGmoccapyHardwareButton({ location: "right", index: 5 }, model).webDispatch.pageId, "user-tabs");
|
||||
assert.equal(resolveGmoccapyHardwareButton({ location: "bottom", index: 3 }, model).webDispatch.pageId, "tool-editor");
|
||||
|
||||
assert.equal(model.nativePages.filePage.nativeWidget, "IconFileSelection1");
|
||||
assert.equal(model.nativePages.filePage.boundary, "native_GtkIconFileSelection_not_embedded_in_browser");
|
||||
assert.equal(model.nativePages.macroPage.macros.length, 5);
|
||||
assert.equal(model.nativePages.macroPage.macros.find((macro) => macro.name === "increment").args.length, 2);
|
||||
assert.equal(model.nativePages.macroPage.boundary.includes("MDI"), true);
|
||||
assert.equal(model.nativePages.toolEditorPage.editableInWeb, false);
|
||||
assert.equal(model.nativePages.toolEditorPage.toolCount, 17);
|
||||
assert.equal(model.nativePages.implementationMatrix.some((page) => page.pageId === "setup" && page.implementation === "diagnostic-only"), true);
|
||||
assert.equal(model.nativePages.implementationMatrix.some((page) => page.pageId === "edit" && page.implementation === "native-only"), true);
|
||||
assert.equal(model.nativePages.behavior.hardButtonsDoNotFakeNativePages, true);
|
||||
assert.equal(model.nativePages.behavior.toolEditorKeepsIocontrolLoopback, true);
|
||||
|
||||
assert.equal(model.halPinActions.source.methods.includes("_optional_blocks"), true);
|
||||
assert.equal(model.halPinActions.source.methods.includes("_blockdelete"), true);
|
||||
assert.equal(model.halPinActions.source.methods.includes("_on_pin_jog_changed"), true);
|
||||
assert.equal(model.halPinActions.source.methods.includes("_on_pin_incr_changed"), true);
|
||||
assert.equal(model.halPinActions.source.methods.includes("_del_message_changed"), true);
|
||||
assert.equal(model.halPinActions.source.methods.includes("_on_unlock_settings_changed"), true);
|
||||
assert.equal(model.halPinActions.source.methods.includes("_check_toolmeasurement"), true);
|
||||
assert.equal(model.halPinActions.source.methods.includes("_init_user_messages"), true);
|
||||
assert.equal(model.halPinActions.jogPins.axes.length, 5);
|
||||
assert.equal(model.halPinActions.jogPins.axes.find((entry) => entry.axis === "b").minus, "gmoccapy.jog.axis.jog-b-minus");
|
||||
assert.equal(model.halPinActions.jogPins.increments.length, 6);
|
||||
assert.equal(model.halPinActions.jogPins.increments[0].distance, 0);
|
||||
assert.equal(model.halPinActions.jogPins.increments[5].pin, "gmoccapy.jog.jog-inc-5");
|
||||
assert.equal(model.halPinActions.jogPins.increments[5].distance, 31.3563);
|
||||
assert.equal(model.halPinActions.jogPins.turtlePin, "gmoccapy.jog.turtle-jog");
|
||||
assert.equal(model.halPinActions.operatorPins.find((pin) => pin.pin === "gmoccapy.optional-stop").webState, "gmoccapyGui.optionalBlocks");
|
||||
assert.equal(model.halPinActions.operatorPins.find((pin) => pin.pin === "gmoccapy.blockdelete").webState, "gmoccapyGui.optionalStop");
|
||||
assert.equal(model.halPinActions.settingsPins.find((pin) => pin.pin === "gmoccapy.unlock-settings").prefDefault, "unlock_way=use");
|
||||
assert.equal(model.halPinActions.overridePins.find((pin) => pin.target === "feed").countEnable, "gmoccapy.feed.feed-override.count-enable");
|
||||
assert.equal(model.halPinActions.overridePins.find((pin) => pin.target === "spindle").reset, "gmoccapy.spindle.reset-spindle-override");
|
||||
assert.equal(model.halPinActions.overridePins.find((pin) => pin.target === "jogVelocity").scalePref, "scale_jog_vel=140.4");
|
||||
assert.equal(model.halPinActions.behavior.optionalStopPinCrossesToBlockDelete, true);
|
||||
assert.equal(model.halPinActions.behavior.blockdeletePinCrossesToOptionalStop, true);
|
||||
assert.equal(model.halPinActions.behavior.unlockSettingsLevelDriven, true);
|
||||
assert.equal(model.halPinActions.behavior.jogAxisPinsPressRelease, true);
|
||||
assert.equal(model.halPinActions.behavior.jogPinsUseTaskPolicyGate, true);
|
||||
assert.equal(model.halPinActions.behavior.jogIncrementPinsRisingEdgeOnly, true);
|
||||
assert.equal(model.halPinActions.behavior.jogIncrementZeroIsContinuous, true);
|
||||
assert.equal(model.halPinActions.behavior.turtleJogLevelDriven, true);
|
||||
assert.equal(model.halPinActions.behavior.countInputRequiresEnable, true);
|
||||
assert.equal(model.halPinActions.behavior.directValueRequiresAnalogEnable, true);
|
||||
assert.equal(model.halPinActions.behavior.resetPinsRisingEdgeOnly, true);
|
||||
assert.equal(model.halPinActions.messagePins.find((pin) => pin.pin === "gmoccapy.delete-message").nativeCallback, "_del_message_changed");
|
||||
assert.equal(model.halPinActions.messagePins.find((pin) => pin.pin === "gmoccapy.warning-confirm").nativeCallback, "dialogs.warning_dialog periodic confirm_pin poll");
|
||||
assert.equal(model.halPinActions.behavior.deleteMessageRisingEdgeOnly, true);
|
||||
assert.equal(model.halPinActions.behavior.warningConfirmLevelPolled, true);
|
||||
assert.equal(model.halPinActions.toolMeasurementPins.toolsensorConfigured, false);
|
||||
assert.equal(model.halPinActions.toolMeasurementPins.pins.length, 5);
|
||||
assert.equal(model.halPinActions.toolMeasurementPins.pins.find((pin) => pin.pin === "gmoccapy.toolmeasurement").direction, "HAL_OUT");
|
||||
assert.equal(model.halPinActions.userMessages.configured, false);
|
||||
assert.equal(model.halPinActions.userMessages.dynamicPinPatterns.includes("gmoccapy.messages.<pinname>-response"), true);
|
||||
assert.equal(model.halPinActions.behavior.toolMeasurementPinsAreHalOut, true);
|
||||
assert.equal(model.halPinActions.behavior.toolMeasurementDisabledWithoutToolsensor, true);
|
||||
assert.equal(model.halPinActions.behavior.userMessagesDynamicIniOnly, true);
|
||||
|
||||
assert.equal(model.toolChange.strategy, "iocontrol-loopback");
|
||||
assert.equal(model.toolChange.manualGmoccapyPinsConnected, false);
|
||||
assert.deepEqual(model.toolChange.remapCodes, ["M6", "M61"]);
|
||||
assert.equal(model.toolChange.postguiUnlinks.map((entry) => entry.pin).join(","), "iocontrol.0.tool-change,iocontrol.0.tool-changed,iocontrol.0.tool-prep-number");
|
||||
assert.equal(model.toolChange.commentedManualGuiNets.some((net) => net.target === "gmoccapy.toolchange-change"), true);
|
||||
assert.equal(model.toolChange.commentedManualGuiNets.some((net) => net.source === "gmoccapy.toolchange-changed"), true);
|
||||
assert.equal(model.toolChange.activeLoop.signal, "tool-change-loop");
|
||||
assert.equal(model.toolChange.activeLoop.stage, "POSTGUI_HALFILE");
|
||||
assert.equal(model.toolChange.semanticBoundary, "gmoccapy_xyzab_iocontrol_tool_change_loop_no_manual_gui_dialog");
|
||||
|
||||
const summary = createGmoccapyHalSummary(model);
|
||||
assert.equal(summary.apiName, "web-rtcp-5axis-gmoccapy-hal-model-summary");
|
||||
assert.equal(summary.profileId, "gmoccapy-xyzab");
|
||||
assert.equal(summary.halFileCount, 4);
|
||||
assert.equal(summary.nativePinGroupCount, 9);
|
||||
assert.equal(summary.nativePinCount >= 58, true);
|
||||
assert.equal(summary.coreNetCount, 10);
|
||||
assert.equal(summary.spindleNetCount, 4);
|
||||
assert.equal(summary.postguiNetCount, 5);
|
||||
assert.equal(summary.postguiRequiresHalcompReady, true);
|
||||
assert.equal(summary.hasToolChangeSimulationLoop, true);
|
||||
assert.equal(summary.toolChangeStrategy, "iocontrol-loopback");
|
||||
assert.equal(summary.manualToolChangeGuiConnected, false);
|
||||
assert.equal(summary.postguiToolUnlinkCount, 3);
|
||||
assert.equal(summary.commentedManualToolChangeNetCount, 3);
|
||||
assert.equal(summary.hardwareButtonVerticalCount, 7);
|
||||
assert.equal(summary.hardwareButtonBottomMainCount, 4);
|
||||
assert.equal(summary.mappedHardwareButtonCount, 6);
|
||||
assert.equal(summary.hardwareButtonEdge, "rising-edge-only");
|
||||
assert.equal(summary.hardwareButtonInsensitiveTarget, "ignored");
|
||||
assert.equal(summary.operatorInputPinCount, 3);
|
||||
assert.equal(summary.settingsInputPinCount, 1);
|
||||
assert.equal(summary.overridePinTargetCount, 4);
|
||||
assert.equal(summary.jogAxisPinCount, 10);
|
||||
assert.equal(summary.jogIncrementPinCount, 6);
|
||||
assert.equal(summary.messageInputPinCount, 3);
|
||||
assert.equal(summary.toolMeasurementPinCount, 5);
|
||||
assert.equal(summary.toolMeasurementEnabled, false);
|
||||
assert.equal(summary.toolsensorConfigured, false);
|
||||
assert.equal(summary.userMessagePinCount, 0);
|
||||
assert.equal(summary.userMessagesConfigured, false);
|
||||
assert.equal(summary.optionalStopPinCrossesToBlockDelete, true);
|
||||
assert.equal(summary.blockdeletePinCrossesToOptionalStop, true);
|
||||
assert.equal(summary.unlockSettingsLevelDriven, true);
|
||||
assert.equal(summary.jogAxisPinsPressRelease, true);
|
||||
assert.equal(summary.jogPinsUseTaskPolicyGate, true);
|
||||
assert.equal(summary.jogIncrementPinsRisingEdgeOnly, true);
|
||||
assert.equal(summary.jogIncrementZeroIsContinuous, true);
|
||||
assert.equal(summary.turtleJogLevelDriven, true);
|
||||
assert.equal(summary.countInputRequiresEnable, true);
|
||||
assert.equal(summary.directValueRequiresAnalogEnable, true);
|
||||
assert.equal(summary.resetPinsRisingEdgeOnly, true);
|
||||
assert.equal(summary.deleteMessageRisingEdgeOnly, true);
|
||||
assert.equal(summary.warningConfirmLevelPolled, true);
|
||||
assert.equal(summary.toolMeasurementPinsAreHalOut, true);
|
||||
assert.equal(summary.toolMeasurementDisabledWithoutToolsensor, true);
|
||||
assert.equal(summary.userMessagesDynamicIniOnly, true);
|
||||
assert.equal(summary.nativePageCount, 12);
|
||||
assert.equal(summary.implementedNativePageCount, 1);
|
||||
assert.equal(summary.partialNativePageCount, 4);
|
||||
assert.equal(summary.diagnosticOnlyNativePageCount, 5);
|
||||
assert.equal(summary.nativeOnlyPageCount, 2);
|
||||
assert.equal(summary.filePageImplementation, "partial");
|
||||
assert.equal(summary.macroCount, 5);
|
||||
assert.equal(summary.macroButtonsUseMdiGate, true);
|
||||
assert.equal(summary.toolEditorImplementation, "diagnostic-only");
|
||||
assert.equal(summary.toolEditorWritebackDisabled, true);
|
||||
assert.equal(summary.hardButtonsDoNotFakeNativePages, true);
|
||||
assert.equal(summary.unimplementedPagesDiagnosticOnly, true);
|
||||
assert.equal(summary.semanticBoundary, model.semanticBoundary);
|
||||
|
||||
console.log("gmoccapy_hal_model_smoke=ok");
|
||||
@@ -0,0 +1,84 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
const repoRoot = new URL("../../..", import.meta.url).pathname.replace(/\/$/, "");
|
||||
const manifestPath = join(repoRoot, "web-rtcp-5axis-sim-plan/app/src/ui-reference/gmoccapy-button-icons.json");
|
||||
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
||||
|
||||
assert.equal(manifest.schema, "web-rtcp-5axis-gmoccapy-button-icons-v1");
|
||||
assert.equal(manifest.semanticBoundary, "gmoccapy_button_icon_asset_reference_only_not_control_semantics");
|
||||
assert.equal(manifest.generatedFrom.csv, "work/working3/gmoccapy_button_icons/button_icon_inventory.csv");
|
||||
assert.equal(manifest.rowCount, 211);
|
||||
assert.equal(manifest.entries.length, 211);
|
||||
assert.equal(manifest.copiedIconCount, 112);
|
||||
|
||||
const entriesByButton = new Map();
|
||||
for (const entry of manifest.entries) {
|
||||
if (!entriesByButton.has(entry.buttonId)) entriesByButton.set(entry.buttonId, []);
|
||||
entriesByButton.get(entry.buttonId).push(entry);
|
||||
if (entry.projectAsset) {
|
||||
await access(join(repoRoot, "web-rtcp-5axis-sim-plan", entry.projectAsset.replace(/^app\//, "app/")));
|
||||
}
|
||||
}
|
||||
|
||||
const coreButtons = [
|
||||
"tbtn_estop",
|
||||
"tbtn_on",
|
||||
"rbt_manual",
|
||||
"rbt_mdi",
|
||||
"rbt_auto",
|
||||
"tbtn_setup",
|
||||
"tbtn_user_tabs",
|
||||
"btn_homing",
|
||||
"btn_tool",
|
||||
"btn_touch",
|
||||
"btn_load",
|
||||
"btn_reload",
|
||||
"btn_run",
|
||||
"btn_stop",
|
||||
"tbtn_pause",
|
||||
"btn_step",
|
||||
"ref_all",
|
||||
"home_axis_x",
|
||||
"home_axis_y",
|
||||
"home_axis_z",
|
||||
"home_axis_a",
|
||||
"home_axis_b",
|
||||
"touch_x",
|
||||
"touch_y",
|
||||
"touch_z",
|
||||
"touch_a",
|
||||
"touch_b",
|
||||
"rbt_forward",
|
||||
"rbt_reverse",
|
||||
"rbt_stop",
|
||||
"tbtn_flood",
|
||||
"tbtn_mist",
|
||||
"btn_zoom_in",
|
||||
"btn_zoom_out",
|
||||
"rbt_view_x",
|
||||
"rbt_view_y",
|
||||
"rbt_view_z",
|
||||
"rbt_view_p",
|
||||
"tbtn_view_tool_path",
|
||||
"tbtn_view_dimension",
|
||||
];
|
||||
|
||||
for (const buttonId of coreButtons) {
|
||||
const entries = entriesByButton.get(buttonId) || [];
|
||||
assert.equal(entries.length > 0, true, `${buttonId} missing from manifest`);
|
||||
assert.equal(entries.some((entry) => entry.projectAsset || entry.sourceType === "none"), true, `${buttonId} lacks asset/fallback`);
|
||||
}
|
||||
|
||||
assert.equal(entriesByButton.get("rbt_auto").some((entry) => entry.stateOrVariant === "active" && entry.iconName === "mode_auto_active"), true);
|
||||
assert.equal(entriesByButton.get("rbt_auto").some((entry) => entry.iconName === "mode_auto_inactive"), true);
|
||||
assert.equal(entriesByButton.get("tbtn_estop").some((entry) => entry.iconName === "main_switch_on"), true);
|
||||
assert.equal(entriesByButton.get("tbtn_estop").some((entry) => entry.iconName === "main_switch_off"), true);
|
||||
assert.equal(entriesByButton.get("tbtn_flood").some((entry) => entry.iconName === "coolant_flood_active"), true);
|
||||
assert.equal(entriesByButton.get("tbtn_mist").some((entry) => entry.iconName === "coolant_mist_inactive"), true);
|
||||
|
||||
const copiedAssets = new Set(manifest.entries.map((entry) => entry.projectAsset).filter(Boolean));
|
||||
assert.equal(copiedAssets.size, 112);
|
||||
|
||||
console.log("gmoccapy_icon_manifest_smoke=ok");
|
||||
@@ -0,0 +1,63 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
GMOCAPY_ICON_REGISTRY_BOUNDARY,
|
||||
getGmoccapyIcon,
|
||||
listGmoccapyIconRegistryEntries,
|
||||
} from "../../app/src/ui/gmoccapy-icon-registry.js";
|
||||
|
||||
const repoRoot = new URL("../../..", import.meta.url).pathname.replace(/\/$/, "");
|
||||
const manifestPath = join(repoRoot, "web-rtcp-5axis-sim-plan/app/src/ui-reference/gmoccapy-button-icons.json");
|
||||
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
||||
const manifestIconNames = new Set(manifest.entries.map((entry) => entry.iconName).filter(Boolean));
|
||||
const registryEntries = listGmoccapyIconRegistryEntries();
|
||||
|
||||
assert.equal(GMOCAPY_ICON_REGISTRY_BOUNDARY, "gmoccapy_icon_registry_from_working3_manifest_core_ui_subset");
|
||||
assert.equal(registryEntries.length >= 40, true);
|
||||
|
||||
const required = [
|
||||
["tbtn_estop", "active", "main_switch_on"],
|
||||
["tbtn_estop", "inactive", "main_switch_off"],
|
||||
["tbtn_on", "active", "power_on"],
|
||||
["rbt_manual", "active", "mode_manual_active"],
|
||||
["rbt_mdi", "active", "mode_mdi_active"],
|
||||
["rbt_auto", "active", "mode_auto_active"],
|
||||
["btn_load", "default", "open_file"],
|
||||
["btn_reload", "default", "refresh"],
|
||||
["btn_run", "default", "play"],
|
||||
["btn_stop", "default", "stop"],
|
||||
["tbtn_pause", "active", "pause_active"],
|
||||
["btn_step", "default", "step"],
|
||||
["btn_homing", "default", "ref_all"],
|
||||
["btn_tool", "default", "hsk_mill_tool"],
|
||||
["btn_touch", "default", "touch_off"],
|
||||
["rbt_forward", "active", "spindle_right_on"],
|
||||
["rbt_reverse", "active", "spindle_left_on"],
|
||||
["rbt_stop", "active", "spindle_stop_on"],
|
||||
["tbtn_flood", "active", "coolant_flood_active"],
|
||||
["tbtn_mist", "inactive", "coolant_mist_inactive"],
|
||||
["rbt_view_x", "default", "tool_axis_x"],
|
||||
["rbt_view_y", "default", "tool_axis_y"],
|
||||
["rbt_view_z", "default", "tool_axis_z"],
|
||||
["rbt_view_p", "default", "tool_axis_p"],
|
||||
["tbtn_view_tool_path", "default", "toolpath"],
|
||||
["tbtn_view_dimension", "default", "dimensions"],
|
||||
];
|
||||
|
||||
for (const [buttonId, variant, expectedIconName] of required) {
|
||||
const icon = getGmoccapyIcon(buttonId, variant);
|
||||
assert.equal(icon.iconName, expectedIconName, `${buttonId}/${variant}`);
|
||||
assert.equal(icon.fallback, false, `${buttonId}/${variant} should use an asset`);
|
||||
assert.equal(icon.semanticBoundary, GMOCAPY_ICON_REGISTRY_BOUNDARY);
|
||||
assert.equal(manifestIconNames.has(icon.iconName), true, `${icon.iconName} missing from manifest`);
|
||||
assert.match(icon.path, /\/assets\/gmoccapy-icons\//);
|
||||
await access(new URL(icon.path));
|
||||
}
|
||||
|
||||
const fallback = getGmoccapyIcon("unknown_button", "active");
|
||||
assert.equal(fallback.iconName, "text_fallback");
|
||||
assert.equal(fallback.fallback, true);
|
||||
|
||||
console.log("gmoccapy_icon_registry_smoke=ok");
|
||||
@@ -0,0 +1,164 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
function sidebarEntry(state, id) {
|
||||
const entry = state.rightSidebarEntrances.find((candidate) => candidate.id === id);
|
||||
assert.ok(entry, `missing right sidebar entry ${id}`);
|
||||
return entry;
|
||||
}
|
||||
|
||||
async function waitForValidatedProgram(store) {
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
const state = store.getState();
|
||||
if (!state.interpreterExecutionPending && state.programValidation?.ready) {
|
||||
return state;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
return store.getState();
|
||||
}
|
||||
|
||||
async function loadIniConfigFromVendoredSource(profile) {
|
||||
const text = await readFile(
|
||||
new URL(`../../../wasm-port/vendor/linuxcnc/${profile.iniPath}`, import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
return parseLinuxCncIni(text, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
}
|
||||
|
||||
const store = createSimulationStore();
|
||||
let state = store.getState();
|
||||
assert.equal(state.rightSidebarEntrances.length, 9);
|
||||
assert.deepEqual(
|
||||
state.rightSidebarEntrances.map((entry) => entry.label),
|
||||
["E-STOP", "POWER", "RESET", "AUTO", "MANUAL", "JOG", "MDI", "IDENTITY", "TCP"],
|
||||
);
|
||||
assert.equal(sidebarEntry(state, "power").allowed, true);
|
||||
assert.equal(sidebarEntry(state, "auto").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "manual").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "jog").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "mdi").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "identity").active, true);
|
||||
assert.equal(sidebarEntry(state, "identity").status, "active-blocked");
|
||||
assert.equal(sidebarEntry(state, "tcp").allowed, false);
|
||||
|
||||
store.dispatch({ type: "ESTOP" });
|
||||
state = store.getState();
|
||||
assert.equal(sidebarEntry(state, "estop").status, "emergency");
|
||||
assert.equal(sidebarEntry(state, "power").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "power").operatorMessage, "power on blocked: reset estop first");
|
||||
|
||||
store.dispatch({ type: "RESET" });
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.taskState, "on");
|
||||
assert.equal(sidebarEntry(state, "power").active, true);
|
||||
assert.equal(sidebarEntry(state, "manual").active, true);
|
||||
assert.equal(sidebarEntry(state, "jog").active, false);
|
||||
assert.equal(sidebarEntry(state, "auto").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "auto").operatorMessage, "mode blocked: home machine before AUTO");
|
||||
|
||||
store.dispatch({ type: "SET_MODE", mode: "jog" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.mode, "manual");
|
||||
assert.equal(state.machine.manualPanel, "jog");
|
||||
assert.equal(sidebarEntry(state, "manual").active, false);
|
||||
assert.equal(sidebarEntry(state, "jog").active, true);
|
||||
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
state = store.getState();
|
||||
assert.equal(sidebarEntry(state, "auto").active, true);
|
||||
assert.equal(sidebarEntry(state, "mdi").allowed, true);
|
||||
assert.equal(sidebarEntry(state, "tcp").allowed, true);
|
||||
|
||||
store.dispatch({ type: "SET_KINS_TYPE", kinsType: "tcp-xyzac" });
|
||||
state = store.getState();
|
||||
assert.equal(state.kinsType, "tcp-xyzac");
|
||||
assert.equal(state.rtcpState, "on");
|
||||
assert.equal(sidebarEntry(state, "tcp").active, true);
|
||||
store.dispatch({ type: "SET_KINS_TYPE", kinsType: "identity" });
|
||||
assert.equal(store.getState().kinsType, "identity");
|
||||
|
||||
store.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "sidebar-running-gate.ngc",
|
||||
content: [
|
||||
"G0 X0 Y0 Z0",
|
||||
"G1 X1 F100",
|
||||
"G1 X2",
|
||||
"G1 X3",
|
||||
"G1 X4",
|
||||
"G1 X5",
|
||||
"G1 X6",
|
||||
"G1 X7",
|
||||
"G1 X8",
|
||||
"G1 X9",
|
||||
"M2",
|
||||
].join("\n"),
|
||||
});
|
||||
store.dispatch({ type: "RUN" });
|
||||
state = store.getState();
|
||||
assert.equal(state.runState, "running");
|
||||
assert.equal(sidebarEntry(state, "manual").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "tcp").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "tcp").operatorMessage, "kinematics blocked: interpreter must be idle");
|
||||
|
||||
store.dispatch({ type: "STOP" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||
|
||||
const profile = getFiveAxisProfile("xyzac-trt");
|
||||
const iniConfig = await loadIniConfigFromVendoredSource(profile);
|
||||
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
store.dispatch({
|
||||
type: "ATTACH_INTERPRETER_RUNTIME",
|
||||
runtime: await createLinuxCncInterpreterRuntime(),
|
||||
});
|
||||
await store.stageMachineFiles({ storage: createMemorySessionStorage() });
|
||||
state = store.getState();
|
||||
assert.equal(state.machineProject.profileId, "xyzac-trt");
|
||||
assert.equal(state.machineProject.projectRoot, "web-rtcp-5axis-sim-plan/machines/xyzac-trt");
|
||||
assert.equal(state.machineProject.ini.filename, "xyzac-trt.ini");
|
||||
assert.equal(state.machineProject.ini.sourceMatchesProfile, true);
|
||||
assert.equal(state.machineProject.ini.textMatchesLoadedIni, true);
|
||||
assert.equal(state.machineProject.configFileCount > 0, true);
|
||||
assert.equal(state.machineProject.gcodeFileCount, 16);
|
||||
assert.equal(state.machineProject.gcodeFiles.some((file) => file.filename === "impeller-7bl-xyzac.ngc"), true);
|
||||
|
||||
store.dispatch({
|
||||
type: "LOAD_LINUXCNC_GCODE_SOURCE",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
});
|
||||
state = await waitForValidatedProgram(store);
|
||||
assert.equal(state.programValidation.ready, true);
|
||||
assert.equal(state.programValidation.sourceGuard, "linuxcnc_vendored_5axis_gcode_source_file");
|
||||
assert.equal(state.programValidation.previewSource, "linuxcnc_interpreter_canonical_motion");
|
||||
assert.equal(state.programValidation.motionEventCount > 0, true);
|
||||
assert.equal(state.programValidation.plannerSampleCount > 0, true);
|
||||
assert.equal(state.programValidation.realtimeAxisValues.x, state.axisPose.x);
|
||||
assert.equal(state.machineProject.selectedProgram.filename, "impeller-7bl-xyzac.ngc");
|
||||
|
||||
const xyzbcStore = createSimulationStore();
|
||||
xyzbcStore.dispatch({ type: "SET_PROFILE", profileId: "xyzbc-trt" });
|
||||
const xyzbcProfile = getFiveAxisProfile("xyzbc-trt");
|
||||
xyzbcStore.dispatch({
|
||||
type: "ATTACH_INI_CONFIG",
|
||||
profileId: xyzbcProfile.id,
|
||||
iniConfig: await loadIniConfigFromVendoredSource(xyzbcProfile),
|
||||
});
|
||||
await xyzbcStore.stageMachineFiles({ storage: createMemorySessionStorage() });
|
||||
const xyzbcState = xyzbcStore.getState();
|
||||
assert.equal(xyzbcState.machineProject.profileId, "xyzbc-trt");
|
||||
assert.equal(xyzbcState.machineProject.ini.filename, "xyzbc-trt.ini");
|
||||
assert.equal(xyzbcState.machineProject.gcodeFiles.some((file) => file.filename === "boat-xyzbc.ngc"), true);
|
||||
|
||||
console.log("gmoccapy_trt_project_sidebar_smoke=ok");
|
||||
@@ -0,0 +1,372 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore, validateRunPreconditions } from "../../app/src/state/store.js";
|
||||
import { gateLinuxCncTaskAction } from "../../app/src/state/linuxcnc-task-policy.js";
|
||||
|
||||
const profile = getFiveAxisProfile("gmoccapy-xyzab");
|
||||
const store = createSimulationStore();
|
||||
store.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
|
||||
assert.equal(store.getState().profile.id, profile.id);
|
||||
assert.equal(store.getState().machine.noForceHoming, false);
|
||||
|
||||
let gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: machine must be on");
|
||||
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "manual" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "mode blocked: machine must be on before MANUAL");
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "mdi" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "mode blocked: machine must be on before MDI");
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "auto" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "mode blocked: machine must be on before AUTO");
|
||||
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "manual" });
|
||||
assert.equal(gate.allowed, true);
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
|
||||
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "auto" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "mode blocked: home machine before AUTO");
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
assert.equal(store.getState().machine.mode, "manual");
|
||||
assert.equal(store.getState().operatorMessage, "mode blocked: home machine before AUTO");
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
|
||||
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "mdi" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "mode blocked: home machine before MDI");
|
||||
|
||||
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||
store.dispatch({ type: "HOME" });
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "auto" });
|
||||
assert.equal(gate.allowed, true);
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: LinuxCNC INI not loaded");
|
||||
|
||||
store.dispatch({
|
||||
type: "ATTACH_INI_CONFIG",
|
||||
profileId: "gmoccapy-xyzab",
|
||||
iniConfig: {
|
||||
profileId: "gmoccapy-xyzab",
|
||||
path: profile.iniPath,
|
||||
sourceText: "[TRAJ]\nCOORDINATES = X Y Z A B\nNO_FORCE_HOMING = 0\n[KINS]\nKINEMATICS = trivkins coordinates=xyzab\n",
|
||||
validation: { ready: true },
|
||||
traj: { coordinates: "XYZAB", noForceHoming: false },
|
||||
kinematics: { name: "trivkins", sparm: "coordinates=xyzab" },
|
||||
kinematicsParameters: profile.kinematicsParameters,
|
||||
kinematicsModuleId: profile.kinematicsModuleId,
|
||||
axisLimits: profile.axisLimits,
|
||||
jointConfig: profile.jointConfig,
|
||||
display: profile.display,
|
||||
rs274ngc: profile.rs274ngc,
|
||||
hal: {
|
||||
...profile.hal,
|
||||
halcmd: profile.hal.halcmd,
|
||||
initialSets: [],
|
||||
},
|
||||
halui: profile.halui,
|
||||
emcmot: { servoPeriodNs: 1000000 },
|
||||
emcio: {},
|
||||
task: { cycleTimeSeconds: 0.001 },
|
||||
},
|
||||
});
|
||||
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: LinuxCNC machine files not staged");
|
||||
|
||||
store.dispatch({
|
||||
type: "MACHINE_FILE_STAGING_COMPLETE",
|
||||
plan: {
|
||||
profileId: "gmoccapy-xyzab",
|
||||
selectedProgramSourceRel: null,
|
||||
},
|
||||
save: {
|
||||
fileCount: 2,
|
||||
files: [
|
||||
{
|
||||
sourceRel: "configs/sim/gmoccapy/gmoccapy_XYZAB.ini",
|
||||
wasmPath: "/work/sim/gmoccapy/gmoccapy_XYZAB.ini",
|
||||
kind: "ini",
|
||||
text: "",
|
||||
bytes: 0,
|
||||
},
|
||||
],
|
||||
summary: { gcodeFileCount: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: no machine-file G-code opened for task/HAL session");
|
||||
|
||||
store.dispatch({
|
||||
type: "MACHINE_FILE_STAGING_COMPLETE",
|
||||
plan: {
|
||||
profileId: "gmoccapy-xyzab",
|
||||
selectedProgramSourceRel: "configs/sim/gmoccapy/demos/xyzab-demo.ngc",
|
||||
},
|
||||
save: {
|
||||
fileCount: 2,
|
||||
files: [
|
||||
{
|
||||
sourceRel: "configs/sim/gmoccapy/demos/xyzab-demo.ngc",
|
||||
wasmPath: "/work/sim/gmoccapy/demos/xyzab-demo.ngc",
|
||||
kind: "demo",
|
||||
text: "G0 X0\nM2\n",
|
||||
bytes: 9,
|
||||
},
|
||||
],
|
||||
summary: { gcodeFileCount: 1 },
|
||||
},
|
||||
selectedGcodeSourceRel: "configs/sim/gmoccapy/demos/xyzab-demo.ngc",
|
||||
});
|
||||
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: task/HAL runtime not ready");
|
||||
|
||||
store.dispatch({
|
||||
type: "ATTACH_TASK_HAL_RUNTIME",
|
||||
runtime: {
|
||||
loaded: true,
|
||||
readiness() {
|
||||
return {
|
||||
loaded: true,
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, true);
|
||||
assert.equal(gate.status.canRunAutoStrict, true);
|
||||
assert.equal(gate.status.profileId, "gmoccapy-xyzab");
|
||||
|
||||
const runPreconditions = validateRunPreconditions(store.getState(), {
|
||||
requireTaskHalRuntime: false,
|
||||
requireTaskHalSession: false,
|
||||
});
|
||||
assert.equal(runPreconditions.ok, false);
|
||||
assert.equal(runPreconditions.operatorMessage, "run blocked: unsupported five-axis profile gmoccapy-xyzab");
|
||||
|
||||
const manualStore = createSimulationStore();
|
||||
manualStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
manualStore.dispatch({ type: "TOGGLE_POWER" });
|
||||
gate = gateLinuxCncTaskAction(manualStore.getState(), { type: "JOG", axis: "x", direction: 1 });
|
||||
assert.equal(gate.allowed, true);
|
||||
gate = gateLinuxCncTaskAction(manualStore.getState(), { type: "HOME" });
|
||||
assert.equal(gate.allowed, true);
|
||||
|
||||
manualStore.dispatch({ type: "HOME" });
|
||||
manualStore.dispatch({ type: "SET_MODE", mode: "mdi" });
|
||||
gate = gateLinuxCncTaskAction(manualStore.getState(), { type: "RUN_MDI" });
|
||||
assert.equal(gate.allowed, true);
|
||||
|
||||
const controlStore = createSimulationStore();
|
||||
controlStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
controlStore.dispatch({ type: "ESTOP" });
|
||||
gate = gateLinuxCncTaskAction(controlStore.getState(), { type: "TOGGLE_COOLANT", kind: "flood" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "coolant blocked: machine must be on");
|
||||
gate = gateLinuxCncTaskAction(controlStore.getState(), { type: "SET_SPINDLE_DIRECTION", direction: "forward" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "spindle blocked: machine must be on");
|
||||
|
||||
controlStore.dispatch({ type: "RESET" });
|
||||
controlStore.dispatch({ type: "TOGGLE_POWER" });
|
||||
controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" });
|
||||
assert.equal(controlStore.getState().spindle.enabled, true);
|
||||
assert.equal(controlStore.getState().spindle.direction, "forward");
|
||||
controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" });
|
||||
assert.equal(controlStore.getState().spindle.enabled, false);
|
||||
assert.equal(controlStore.getState().spindle.direction, "stop");
|
||||
|
||||
controlStore.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
|
||||
assert.equal(controlStore.getState().coolant.flood, true);
|
||||
|
||||
const hardButtonStore = createSimulationStore();
|
||||
hardButtonStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-1", value: false });
|
||||
assert.equal(hardButtonStore.getState().machine.powerOn, false);
|
||||
assert.equal(hardButtonStore.getState().operatorMessage, "gmoccapy hardware button falling edge ignored");
|
||||
hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-1", value: true });
|
||||
assert.equal(hardButtonStore.getState().machine.powerOn, true);
|
||||
hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-4", value: true });
|
||||
assert.equal(hardButtonStore.getState().machine.mode, "manual");
|
||||
assert.equal(hardButtonStore.getState().operatorMessage, "mode blocked: home machine before AUTO");
|
||||
hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", location: "bottom", index: 0, value: true });
|
||||
assert.equal(hardButtonStore.getState().machine.allHomed, true);
|
||||
hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-4", value: true });
|
||||
assert.equal(hardButtonStore.getState().machine.mode, "auto");
|
||||
hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-6", value: true });
|
||||
assert.equal(hardButtonStore.getState().gmoccapyGui.activeNativePage, "setup");
|
||||
assert.equal(hardButtonStore.getState().operatorMessage, "gmoccapy native page diagnostic-only: setup");
|
||||
hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.h-button.button-9", value: true });
|
||||
assert.equal(hardButtonStore.getState().operatorMessage, "gmoccapy hardware button unmapped: gmoccapy.h-button.button-9");
|
||||
|
||||
const halPinStore = createSimulationStore();
|
||||
halPinStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.ignore-limits", value: true });
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.ignoreLimits, true);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL ignore-limits requested");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.optional-stop", value: true });
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.optionalBlocks, true);
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.optionalStop, false);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL optional-stop -> block delete on");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.blockdelete", value: true });
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.optionalStop, true);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL blockdelete -> optional stop on");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.unlock-settings", value: false });
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.settingsUnlockPin, false);
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.setupSensitive, true);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL unlock-settings ignored: unlock_way is use");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.unlock-settings", value: false, halUnlockMode: true });
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.settingsUnlockMode, "hal");
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.setupSensitive, false);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL unlock-settings disabled setup");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.unlock-settings", value: true });
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.settingsUnlockPin, true);
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.setupSensitive, true);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL unlock-settings enabled setup");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.blockheight", value: 12.5 });
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.blockHeight, 12.5);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL tool measurement output recorded; XYZAB has no [TOOLSENSOR]");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.toolmeasurement", value: true });
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.toolMeasurement, true);
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.messages.test", value: true });
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL user message pin unmapped: gmoccapy_XYZAB.ini defines no MESSAGE_* entries");
|
||||
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.feed.feed-override.counts", value: 12 });
|
||||
assert.equal(halPinStore.getState().feed.feedOverride, 100);
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.feedOverrideCounts, 12);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL feed counts synchronized");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.feed.feed-override.count-enable", value: true });
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.feed.feed-override.counts", value: 17 });
|
||||
assert.equal(halPinStore.getState().feed.feedOverride, 105);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL feed counts adjusted");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.rapid.rapid-override.analog-enable", value: true });
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.rapid.rapid-override.direct-value", value: 0.25 });
|
||||
assert.equal(halPinStore.getState().feed.rapidOverride, 50);
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.spindle-override.direct-value", value: 0.5 });
|
||||
assert.equal(halPinStore.getState().spindle.override, 100);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL spindle direct-value ignored: analog disabled");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.spindle-override.analog-enable", value: true });
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.spindle-override.direct-value", value: 0.5 });
|
||||
assert.equal(halPinStore.getState().spindle.override, 75);
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.reset-spindle-override", value: false });
|
||||
assert.equal(halPinStore.getState().spindle.override, 75);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL spindle reset falling edge ignored");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.reset-spindle-override", value: true });
|
||||
assert.equal(halPinStore.getState().spindle.override, 100);
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL spindle reset to 100");
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-velocity.count-enable", value: true });
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-velocity.counts", value: 2 });
|
||||
assert.equal(halPinStore.getState().gmoccapyGui.jogVelocity, 380.8);
|
||||
halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.unknown", value: true });
|
||||
assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL pin unmapped: gmoccapy.unknown");
|
||||
|
||||
const halJogStore = createSimulationStore();
|
||||
halJogStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-x-plus", value: true });
|
||||
assert.equal(halJogStore.getState().runState, "idle");
|
||||
assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog blocked: jog blocked: machine must be on");
|
||||
halJogStore.dispatch({ type: "TOGGLE_POWER" });
|
||||
halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-x-plus", value: true });
|
||||
assert.equal(halJogStore.getState().runState, "jogging");
|
||||
assert.equal(halJogStore.getState().axisPose.x, 43);
|
||||
assert.equal(halJogStore.getState().machine.jogIncrement, 0);
|
||||
assert.equal(halJogStore.getState().gmoccapyGui.activeJogPin, "gmoccapy.jog.axis.jog-x-plus");
|
||||
assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog X+ continuous");
|
||||
halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-x-plus", value: false });
|
||||
assert.equal(halJogStore.getState().runState, "idle");
|
||||
assert.equal(halJogStore.getState().gmoccapyGui.activeJogPin, null);
|
||||
assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog X+ released");
|
||||
halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-inc-2", value: false });
|
||||
assert.equal(halJogStore.getState().machine.jogIncrement, 0);
|
||||
assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog increment falling edge ignored");
|
||||
halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-inc-2", value: true });
|
||||
assert.equal(halJogStore.getState().machine.jogIncrement, 0.1);
|
||||
assert.equal(halJogStore.getState().gmoccapyGui.jogIncrementIndex, 2);
|
||||
assert.equal(halJogStore.getState().gmoccapyGui.jogIncrementOutput, 0.1);
|
||||
halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-x-plus", value: true });
|
||||
assert.equal(halJogStore.getState().axisPose.x, 43.1);
|
||||
assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog X+ 0.1");
|
||||
halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-inc-5", value: true });
|
||||
assert.equal(halJogStore.getState().machine.jogIncrement, 31.3563);
|
||||
assert.equal(halJogStore.getState().gmoccapyGui.jogIncrementLabel, "1.2345 in");
|
||||
halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.turtle-jog", value: true });
|
||||
assert.equal(halJogStore.getState().gmoccapyGui.turtleJog, true);
|
||||
assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL turtle jog on");
|
||||
|
||||
const halMessageStore = createSimulationStore();
|
||||
halMessageStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.delete-message", value: false });
|
||||
assert.equal(halMessageStore.getState().gmoccapyGui.deletedMessageCount, 0);
|
||||
assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL delete-message falling edge ignored");
|
||||
halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-y-minus", value: true });
|
||||
assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL jog blocked: jog blocked: machine must be on");
|
||||
halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.delete-message", value: true });
|
||||
assert.equal(halMessageStore.getState().gmoccapyGui.error, false);
|
||||
assert.equal(halMessageStore.getState().gmoccapyGui.deletedMessageCount, 1);
|
||||
assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL delete-message cleared alert");
|
||||
halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.warning-confirm", value: true });
|
||||
assert.equal(halMessageStore.getState().gmoccapyGui.warningConfirm, true);
|
||||
assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL warning-confirm asserted");
|
||||
halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.warning-confirm", value: false });
|
||||
assert.equal(halMessageStore.getState().gmoccapyGui.warningConfirm, false);
|
||||
assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL warning-confirm cleared");
|
||||
|
||||
const nativePageStore = createSimulationStore();
|
||||
nativePageStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
nativePageStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-6", value: true });
|
||||
assert.equal(nativePageStore.getState().gmoccapyGui.activeNativePage, "setup");
|
||||
assert.equal(nativePageStore.getState().gmoccapyGui.nativePageMode, "diagnostic-only");
|
||||
assert.equal(nativePageStore.getState().operatorMessage, "gmoccapy native page diagnostic-only: setup");
|
||||
nativePageStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.h-button.button-3", value: true });
|
||||
assert.equal(nativePageStore.getState().gmoccapyGui.activeNativePage, "tool-editor");
|
||||
assert.equal(nativePageStore.getState().operatorMessage, "gmoccapy native page diagnostic-only: tool-editor");
|
||||
nativePageStore.dispatch({ type: "GMOCAPY_PAGE_ACTION", pageId: "file-load", actionId: "open" });
|
||||
assert.equal(nativePageStore.getState().gmoccapyGui.filePageStatus, "open");
|
||||
assert.equal(nativePageStore.getState().operatorMessage.includes("native Gtk chooser diagnostic"), true);
|
||||
nativePageStore.dispatch({ type: "GMOCAPY_TOOL_EDITOR_ACTION", actionId: "save" });
|
||||
assert.equal(nativePageStore.getState().gmoccapyGui.toolEditorStatus, "writeback-blocked");
|
||||
assert.equal(nativePageStore.getState().operatorMessage.includes("does not write tool.tbl"), true);
|
||||
|
||||
const macroBlockedStore = createSimulationStore();
|
||||
macroBlockedStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
macroBlockedStore.dispatch({ type: "GMOCAPY_RUN_MACRO", name: "halo_world" });
|
||||
assert.equal(macroBlockedStore.getState().gmoccapyGui.macroLastName, "halo_world");
|
||||
assert.equal(macroBlockedStore.getState().operatorMessage.includes("gmoccapy macro blocked"), true);
|
||||
|
||||
const macroReadyStore = createSimulationStore();
|
||||
macroReadyStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
macroReadyStore.dispatch({ type: "TOGGLE_POWER" });
|
||||
macroReadyStore.dispatch({ type: "HOME" });
|
||||
macroReadyStore.dispatch({ type: "SET_MODE", mode: "mdi" });
|
||||
macroReadyStore.dispatch({
|
||||
type: "GMOCAPY_RUN_MACRO",
|
||||
name: "increment",
|
||||
args: { xinc: 1.25, yinc: 2.5 },
|
||||
});
|
||||
assert.equal(macroReadyStore.getState().gmoccapyGui.activeNativePage, "mdi-macros");
|
||||
assert.equal(macroReadyStore.getState().gmoccapyGui.macroLastCommand, "O<increment> call [1.25] [2.5]");
|
||||
assert.equal(macroReadyStore.getState().machine.mdiCommand, "O<INCREMENT> CALL [1.25] [2.5]");
|
||||
assert.equal(macroReadyStore.getState().operatorMessage, "gmoccapy macro MDI O<increment> call [1.25] [2.5]");
|
||||
|
||||
console.log("gmoccapy_xyzab_gates_smoke=ok");
|
||||
@@ -0,0 +1,179 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getFiveAxisProfile, fiveAxisProfiles } from "../../app/src/profiles/index.js";
|
||||
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
|
||||
import { createLinuxCncBoundaryAdapter, createLinuxCncBoundaryReadiness } from "../../app/src/runtime/linuxcnc-boundary-adapter.js";
|
||||
import { createProfileSourceReferenceSummary } from "../../app/src/profiles/source-reference-map.js";
|
||||
import { createSimulationStore, validateRunPreconditions } from "../../app/src/state/store.js";
|
||||
|
||||
const profile = getFiveAxisProfile("gmoccapy-xyzab");
|
||||
|
||||
assert.equal(profile.id, "gmoccapy-xyzab");
|
||||
assert.equal(profile.title, "gmoccapy XYZAB trivkins reference");
|
||||
assert.equal(profile.iniPath, "linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini");
|
||||
assert.deepEqual(profile.coordinates, ["X", "Y", "Z", "A", "B"]);
|
||||
assert.equal(profile.traj.coordinates, "XYZAB");
|
||||
assert.equal(profile.joints.length, 5);
|
||||
assert.equal(profile.jointConfig.length, 5);
|
||||
assert.equal(profile.kinematics, "trivkins coordinates=xyzab");
|
||||
assert.equal(profile.kinematicsModuleId, "gmoccapy-xyzab-trivkins-reference");
|
||||
assert.equal(profile.tcpCapable, false);
|
||||
assert.equal(profile.rtcpProof, false);
|
||||
assert.equal(profile.linuxCncKinematicsReady, false);
|
||||
assert.equal(profile.promotionAllowed, false);
|
||||
assert.equal(profile.semanticBoundary, "gmoccapy_xyzab_trivkins_reference_only_not_rtcp_proof");
|
||||
assert.equal(profile.homing.noForceHoming, false);
|
||||
assert.equal(profile.homing.noForceHomingIniValue, 0);
|
||||
assert.equal(profile.hal.postguiRequiresHalcompReady, true);
|
||||
assert.deepEqual(profile.hal.halFiles, ["core_sim_XYZAB.hal", "spindle_sim.hal", "simulated_home.hal"]);
|
||||
assert.deepEqual(profile.hal.postguiHalFiles, ["gmoccapy_postgui.hal"]);
|
||||
assert.equal(profile.nativeRuntime.task, "milltask");
|
||||
assert.equal(profile.nativeRuntime.emcmot, "motmod");
|
||||
assert.equal(profile.nativeRuntime.halui, "halui");
|
||||
assert.equal(profile.nativeRuntime.displayCycleTimeMs, 100);
|
||||
assert.equal(profile.nativeRuntime.servoPeriodNs, 1000000);
|
||||
assert.equal(profile.axisLimits.X.max, 600);
|
||||
assert.equal(profile.axisLimits.Y.max, 400);
|
||||
assert.equal(profile.axisLimits.Z.min, -400);
|
||||
assert.equal(profile.axisLimits.B.maxVelocity, 90);
|
||||
assert.equal(profile.jointConfig[4].axis, "B");
|
||||
assert.equal(profile.jointConfig[4].homeOffset, 45);
|
||||
assert.equal(profile.hal.halcmd.feedbackNets.length, 5);
|
||||
assert.equal(profile.hal.halcmd.offsetNets.some((net) => net.target === "gmoccapy.tooloffset-z"), true);
|
||||
assert.equal(profile.hal.halcmd.simulationLoops.some((net) => net.signal === "tool-change-loop"), true);
|
||||
assert.equal(profile.hal.halcmd.toolChange.strategy, "iocontrol-loopback");
|
||||
assert.equal(profile.hal.halcmd.toolChange.manualGmoccapyPinsConnected, false);
|
||||
assert.deepEqual(profile.hal.halcmd.toolChange.postguiUnlinks, [
|
||||
"iocontrol.0.tool-change",
|
||||
"iocontrol.0.tool-changed",
|
||||
"iocontrol.0.tool-prep-number",
|
||||
]);
|
||||
assert.equal(profile.hal.halcmd.toolChange.commentedManualGuiNets.length, 3);
|
||||
assert.equal(profile.hal.halcmd.toolChange.activeLoop.signal, "tool-change-loop");
|
||||
assert.equal(profile.halPins.includes("gmoccapy.program.current-line"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.spindle_at_speed_led"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.jog.axis.jog-x-plus"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.jog.axis.jog-y-minus"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.jog.axis.jog-b-minus"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.jog.jog-inc-0"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.jog.jog-inc-5"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.jog.jog-increment"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.jog.turtle-jog"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.ignore-limits"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.optional-stop"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.blockdelete"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.unlock-settings"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.feed.feed-override.count-enable"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.feed.reset-feed-override"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.spindle.reset-spindle-override"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.jog.jog-velocity.direct-value"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.delete-message"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.warning-confirm"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.probeheight"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.blockheight"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.toolmeasurement"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.searchvel"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.probevel"), true);
|
||||
assert.equal(profile.halPins.includes("gmoccapy.toolchange-confirm"), true);
|
||||
assert.equal(profile.toolsensor.configured, false);
|
||||
assert.equal(profile.toolsensor.useToolMeasurementPref, false);
|
||||
assert.equal(profile.toolsensor.blockHeightPref, 0);
|
||||
assert.equal(profile.toolsensor.reason.includes("no [TOOLSENSOR]"), true);
|
||||
assert.equal(profile.userMessages.configured, false);
|
||||
assert.equal(profile.userMessages.count, 0);
|
||||
assert.equal(profile.userMessages.reason.includes("no [DISPLAY] MESSAGE_*"), true);
|
||||
assert.equal(profile.nativePages.filePage.configured, true);
|
||||
assert.equal(profile.nativePages.filePage.programPrefix, "../../nc_files/");
|
||||
assert.equal(profile.nativePages.filePage.webImplementation, "partial");
|
||||
assert.equal(profile.nativePages.macroPage.configured, true);
|
||||
assert.equal(profile.nativePages.macroPage.count, 5);
|
||||
assert.equal(profile.nativePages.macroPage.macros.find((macro) => macro.name === "increment").args.length, 2);
|
||||
assert.equal(profile.nativePages.macroPage.macros.find((macro) => macro.name === "go_to_position").command, "O<go_to_position> call [X-pos] [Y-pos] [Z-pos]");
|
||||
assert.equal(profile.nativePages.toolEditor.configured, true);
|
||||
assert.equal(profile.nativePages.toolEditor.writebackEnabled, false);
|
||||
assert.equal(profile.nativePages.toolEditor.toolCount, 17);
|
||||
assert.equal(profile.nativePages.implementationMatrixBoundary.includes("diagnostic-only"), true);
|
||||
assert.equal(profile.toolTable.toolCount, 17);
|
||||
assert.equal(profile.macros.includes("go_to_position X-pos Y-pos Z-pos"), true);
|
||||
assert.equal(profile.panelSchema.id, "gmoccapy-xyzab-hal-component");
|
||||
assert.equal(profile.panelSchema.boundary, "gmoccapy_hal_component_reference_only");
|
||||
assert.equal(profile.panelSchema.groups.length, 2);
|
||||
const taskModeControls = profile.panelSchema.groups.find((group) => group.id === "task-mode-actions").controls;
|
||||
assert.deepEqual(taskModeControls.map((control) => [control.id, control.halpin]), [
|
||||
["tbtn-estop", "gmoccapy.v-button.button-0"],
|
||||
["tbtn-on", "gmoccapy.v-button.button-1"],
|
||||
["rbt-manual", "gmoccapy.v-button.button-2"],
|
||||
["rbt-mdi", "gmoccapy.v-button.button-3"],
|
||||
["rbt-auto", "gmoccapy.v-button.button-4"],
|
||||
]);
|
||||
|
||||
assert.deepEqual(fiveAxisProfiles.map(({ id }) => id), [
|
||||
"xyzac-trt",
|
||||
"xyzbc-trt",
|
||||
"gmoccapy-xyzac-trt",
|
||||
"gmoccapy-xyzab",
|
||||
]);
|
||||
|
||||
const sourceSummary = createProfileSourceReferenceSummary("gmoccapy-xyzab");
|
||||
assert.equal(sourceSummary.profileId, "gmoccapy-xyzab");
|
||||
assert.equal(sourceSummary.referenceCount >= 10, true);
|
||||
assert.equal(sourceSummary.kinds.includes("ini"), true);
|
||||
assert.equal(sourceSummary.kinds.includes("halfile"), true);
|
||||
assert.equal(sourceSummary.kinds.includes("postgui_hal"), true);
|
||||
assert.equal(sourceSummary.kinds.includes("icon_manifest"), true);
|
||||
assert.equal(sourceSummary.kinds.includes("python_gui"), 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("gmoccapy_XYZAB.ini")));
|
||||
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy_postgui.hal")));
|
||||
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy-button-icons.json")));
|
||||
|
||||
const adapter = createLinuxCncBoundaryAdapter({ profile });
|
||||
assert.equal(adapter.profileId, "gmoccapy-xyzab");
|
||||
assert.equal(adapter.profileSummary.coordinates, "XYZAB");
|
||||
assert.equal(adapter.profileSummary.jointCount, 5);
|
||||
assert.equal(adapter.profileSummary.mdiCommandCount, 0);
|
||||
assert.equal(adapter.profileSummary.remapCount, 2);
|
||||
assert.equal(adapter.profileSummary.toolCount, 17);
|
||||
assert.equal(adapter.profileSummary.feedbackNetCount, 5);
|
||||
assert.equal(adapter.profileSummary.halFileCount, 4);
|
||||
assert.equal(adapter.promotionAllowed, false);
|
||||
assert.equal(adapter.linuxCncKinematicsReady, false);
|
||||
|
||||
const readiness = createLinuxCncBoundaryReadiness(adapter);
|
||||
assert.equal(readiness.ready, false);
|
||||
assert.equal(readiness.promotionAllowed, false);
|
||||
assert.equal(readiness.missing.includes("kinematics runtime"), true);
|
||||
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: { x: 10, y: 20, z: -5, a: 30, b: 45, c: 99 },
|
||||
activeLine: 10,
|
||||
profile,
|
||||
});
|
||||
assert.equal(frame.profileId, "gmoccapy-xyzab");
|
||||
assert.deepEqual(frame.jointPose.map((joint) => joint.axis), ["X", "Y", "Z", "A", "B"]);
|
||||
assert.equal(frame.jointPose[4].value, 45);
|
||||
assert.equal(frame.readiness.linuxCncKinematicsReady, false);
|
||||
assert.equal(frame.readiness.promotionAllowed, false);
|
||||
|
||||
const store = createSimulationStore();
|
||||
store.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
|
||||
assert.equal(store.getState().machineProfile, "gmoccapy-xyzab");
|
||||
assert.equal(store.getState().profile.tcpCapable, false);
|
||||
assert.equal(store.getState().rtcpState, "off");
|
||||
store.dispatch({ type: "SET_KINS_TYPE", kinsType: "tcp-xyzac" });
|
||||
assert.equal(store.getState().kinsType, "identity");
|
||||
assert.equal(store.getState().rtcpState, "off");
|
||||
assert.equal(store.getState().operatorMessage, "kinematics tcp-xyzac blocked: gmoccapy-xyzab is not TCP capable");
|
||||
store.dispatch({ type: "SET_RTCP", enabled: true });
|
||||
assert.equal(store.getState().rtcpState, "off");
|
||||
assert.equal(store.getState().operatorMessage, "RTCP blocked: gmoccapy-xyzab is trivkins coordinates=xyzab reference only");
|
||||
|
||||
const preconditions = validateRunPreconditions(store.getState(), {
|
||||
requireTaskHalRuntime: false,
|
||||
requireTaskHalSession: false,
|
||||
});
|
||||
assert.equal(preconditions.ok, false);
|
||||
assert.equal(preconditions.operatorMessage, "run blocked: unsupported five-axis profile gmoccapy-xyzab");
|
||||
|
||||
console.log("gmoccapy_xyzab_profile_smoke=ok");
|
||||
@@ -0,0 +1,211 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
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 {
|
||||
selectMachineFileProgram,
|
||||
stageProfileMachineFiles,
|
||||
} from "../../app/src/runtime/linuxcnc-machine-file-staging.js";
|
||||
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
|
||||
import { createProfileSourceReferenceSummary } from "../../app/src/profiles/source-reference-map.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore, validateRunPreconditions } from "../../app/src/state/store.js";
|
||||
|
||||
const GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX = "configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/";
|
||||
const GMOCAPY_TRT_EXAMPLE_FILES = [
|
||||
"boat-xyzac.ngc",
|
||||
"boat-xyzbc.ngc",
|
||||
"impeller-7bl-xyzac.ngc",
|
||||
"test-xyzac.ngc",
|
||||
"test-xyzbc.ngc",
|
||||
];
|
||||
const DIRECT_PROGRAM_EXAMPLES = [
|
||||
"impeller-7bl-xyzac.ngc",
|
||||
"boat-xyzac.ngc",
|
||||
"boat-xyzbc.ngc",
|
||||
];
|
||||
const NGC_GUI_SUBROUTINE_EXAMPLES = [
|
||||
{ filename: "test-xyzac.ngc", subroutine: "test-xyzac", rotaryAxis: "A" },
|
||||
{ filename: "test-xyzbc.ngc", subroutine: "test-xyzbc", rotaryAxis: "B" },
|
||||
];
|
||||
|
||||
const profile = getFiveAxisProfile("gmoccapy-xyzac-trt");
|
||||
const iniText = await readFile(
|
||||
new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const postguiText = await readFile(
|
||||
new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/postgui.hal", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.equal(profile.display.display, "gmoccapy");
|
||||
assert.equal(profile.display.openFile, "./examples/impeller-7bl-xyzac.ngc");
|
||||
assert.equal(profile.kinematics, "xyzac-trt-kins");
|
||||
assert.equal(profile.kinematicsModuleId, "xyzac-trt");
|
||||
assert.equal(profile.kinematicsParameters.fixedTrtDefault, true);
|
||||
assert.equal(profile.kinematicsParameters.switchkinsTypes[0].webKinsType, "tcp-xyzac");
|
||||
assert.equal(profile.rtcpProof, true);
|
||||
assert.equal(profile.machineFileStaging.machineRel, "gmoccapy/non_trivial_kinematics/table-rotary-tilting");
|
||||
assert.equal(profile.machineFileStaging.demoDirectory, "examples");
|
||||
assert.equal(profile.hal.halcmd.offsetNets.some((net) => net.target === "gmoccapy.tooloffset-z"), true);
|
||||
assert.equal(profile.hal.halcmd.toolChange.manualGmoccapyPinsConnected, true);
|
||||
|
||||
const sourceSummary = createProfileSourceReferenceSummary(profile.id);
|
||||
assert.equal(sourceSummary.referenceCount >= 10, true);
|
||||
assert.equal(sourceSummary.kinds.includes("ini"), true);
|
||||
assert.equal(sourceSummary.kinds.includes("gcode_demo"), true);
|
||||
assert.equal(sourceSummary.kinds.includes("gcode_ngcgui_subroutine"), true);
|
||||
assert.equal(sourceSummary.kinds.includes("remap_subroutine"), true);
|
||||
assert.equal(sourceSummary.sourceRequiredCount >= 2, true);
|
||||
for (const filename of GMOCAPY_TRT_EXAMPLE_FILES) {
|
||||
assert.equal(
|
||||
sourceSummary.references.some((reference) => reference.path === `${GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX}${filename}`),
|
||||
true,
|
||||
`${filename} source reference`,
|
||||
);
|
||||
}
|
||||
|
||||
const iniConfig = parseLinuxCncIni(iniText, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
assert.equal(iniConfig.validation.ready, true);
|
||||
assert.equal(iniConfig.machineName, "sim-xyzac-trt");
|
||||
assert.equal(iniConfig.display.display, "gmoccapy");
|
||||
assert.equal(iniConfig.traj.coordinates, "XYZAC");
|
||||
assert.equal(iniConfig.kinematics.name, "xyzac-trt-kins");
|
||||
assert.equal(iniConfig.kinematicsParameters.sparm, null);
|
||||
assert.deepEqual(iniConfig.rs274ngc.remaps.map((remap) => remap.code), ["M6", "M61"]);
|
||||
assert.equal(iniConfig.rs274ngc.subroutinePath, "./examples:../../macros");
|
||||
assert.equal(iniConfig.hal.postguiHalFiles.includes("postgui.hal"), true);
|
||||
assert.equal(iniConfig.hal.halcmd.some((line) => line.includes("xyzac-trt-gui")), true);
|
||||
assert.equal(iniConfig.hal.halcmd.some((line) => line.includes("xyzac-trt-kins.tool-offset")), true);
|
||||
assert.equal(iniConfig.hal.halcmd.some((line) => line.includes("sets :y-offset 20")), true);
|
||||
assert.equal(iniConfig.emcmot.servoPeriodNs, 1000000);
|
||||
assert.equal(iniConfig.task.cycleTimeSeconds, 0.010);
|
||||
assert.equal(postguiText.includes("gmoccapy.spindle_feedback_bar"), true);
|
||||
assert.equal(postguiText.includes("gmoccapy.toolchange-change"), true);
|
||||
assert.equal(postguiText.includes("gmoccapy.tooloffset-z"), true);
|
||||
|
||||
const staged = await stageProfileMachineFiles(profile, {
|
||||
storage: createMemorySessionStorage(),
|
||||
});
|
||||
assert.equal(staged.plan.profileId, profile.id);
|
||||
assert.equal(staged.plan.machineRel, "gmoccapy/non_trivial_kinematics/table-rotary-tilting");
|
||||
assert.equal(staged.plan.demoDirectory, "examples");
|
||||
assert.equal(staged.save.files.some((file) => file.sourceRel === profile.iniPath), true);
|
||||
assert.equal(staged.save.files.some((file) => file.sourceRel === profile.toolTablePath), true);
|
||||
assert.equal(staged.save.files.some((file) => file.sourceRel.endsWith("macros/change_g43.ngc")), true);
|
||||
assert.equal(staged.save.files.some((file) => file.sourceRel.endsWith("macros/settool_g43.ngc")), true);
|
||||
assert.deepEqual(staged.save.gcodeSources.map((source) => source.filename), GMOCAPY_TRT_EXAMPLE_FILES);
|
||||
assert.equal(staged.save.gcodeSources.every((source) => source.sourceRel.startsWith(GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX)), true);
|
||||
assert.equal(staged.save.gcodeSources.every((source) => source.semanticBoundary === "linuxcnc_vendored_5axis_gcode_source_file"), true);
|
||||
assert.equal(staged.save.gcodeFiles.some((file) => file.filename === "test-xyzac.ngc"), true);
|
||||
assert.equal(staged.save.gcodeFiles.some((file) => file.filename === "test-xyzbc.ngc"), true);
|
||||
|
||||
const selectedPlan = selectMachineFileProgram(
|
||||
staged.plan,
|
||||
staged.save,
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc",
|
||||
);
|
||||
assert.equal(selectedPlan.selectedProgramFilename, "impeller-7bl-xyzac.ngc");
|
||||
assert.equal(selectedPlan.wasmProgramPath.endsWith("/examples/impeller-7bl-xyzac.ngc"), true);
|
||||
|
||||
const runtime = await createLinuxCncInterpreterRuntime();
|
||||
for (const filename of DIRECT_PROGRAM_EXAMPLES) {
|
||||
const source = staged.save.files.find((file) => file.sourceRel.endsWith(`/examples/${filename}`));
|
||||
assert.ok(source, `${filename} staged`);
|
||||
const execution = runtime.runProgram(source.text);
|
||||
assert.equal(execution.sourceMode, "linuxcnc-interpreter-wasm", `${filename} source mode`);
|
||||
assert.equal(execution.summary.ready, true, `${filename} ready`);
|
||||
assert.equal(execution.summary.motionEventCount > 0, true, `${filename} motion`);
|
||||
assert.equal(execution.plannerTiming.samples.length > 0, true, `${filename} planner samples`);
|
||||
}
|
||||
|
||||
const boatMachineFilePlan = selectMachineFileProgram(
|
||||
staged.plan,
|
||||
staged.save,
|
||||
`${GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX}boat-xyzac.ngc`,
|
||||
);
|
||||
const boatMachineFileExecution = runtime.runMachineFileProgram({
|
||||
plan: boatMachineFilePlan,
|
||||
files: staged.save.files,
|
||||
executionMode: "fiveAxisRemap",
|
||||
});
|
||||
const boatRapidAfterToolChange = boatMachineFileExecution.motion.find((event) => (
|
||||
event.type === "STRAIGHT_TRAVERSE" && event.line === 12
|
||||
));
|
||||
assert.equal(boatMachineFileExecution.sourceMode, "linuxcnc-machine-file-remap-wasm");
|
||||
assert.equal(boatMachineFileExecution.summary.machineFileExecutionReady, true);
|
||||
assert.equal(boatMachineFileExecution.summary.motionEventCount, 1833);
|
||||
assert.equal(boatMachineFileExecution.plannerTiming.samples.length > 0, true);
|
||||
assert.ok(boatRapidAfterToolChange, "boat-xyzac line 12 rapid move");
|
||||
assert.equal(boatRapidAfterToolChange.axes.x, -49.65);
|
||||
assert.equal(boatRapidAfterToolChange.axes.y, -23.015);
|
||||
assert.equal(boatRapidAfterToolChange.axes.z, 5);
|
||||
|
||||
for (const { filename, subroutine, rotaryAxis } of NGC_GUI_SUBROUTINE_EXAMPLES) {
|
||||
const source = staged.save.files.find((file) => file.sourceRel.endsWith(`/examples/${filename}`));
|
||||
assert.ok(source, `${filename} staged`);
|
||||
assert.equal(source.text.includes(`o<${subroutine}> sub`), true, `${filename} subroutine open`);
|
||||
assert.equal(source.text.includes(`o<${subroutine}> endsub`), true, `${filename} subroutine close`);
|
||||
assert.equal(source.text.includes("T#<tool1> M6"), true, `${filename} tool-change semantics`);
|
||||
assert.equal(source.text.includes("G43 H#<tool2> Z0"), true, `${filename} tool-offset semantics`);
|
||||
assert.equal(new RegExp(`G00 ${rotaryAxis}#<${rotaryAxis.toLowerCase()}_angle> C#<c_angle>`).test(source.text), true, `${filename} rotary-axis semantics`);
|
||||
const execution = runtime.runProgram(source.text);
|
||||
assert.equal(execution.sourceMode, "linuxcnc-interpreter-wasm", `${filename} source mode`);
|
||||
assert.equal(execution.summary.ready, false, `${filename} ngcgui subroutine is not a standalone main program`);
|
||||
assert.equal(execution.summary.motionEventCount, 0, `${filename} direct subroutine execution has no motion`);
|
||||
}
|
||||
|
||||
const kinematics = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt", switchkinsType: 0 });
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: { x: 16.339, y: -25.409, z: 33.353, a: -71.841, c: -35.93 },
|
||||
activeLine: 5,
|
||||
kinsType: "tcp-xyzac",
|
||||
rtcpEnabled: true,
|
||||
profile,
|
||||
linuxCncKinematicsResult: kinematics.frameForJoints([16.339, -25.409, 33.353, -71.841, -35.93]),
|
||||
});
|
||||
assert.equal(frame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(frame.readiness.linuxCncKinematicsReady, true);
|
||||
assert.equal(frame.rtcpState, "on");
|
||||
assert.equal(frame.profileId, "gmoccapy-xyzac-trt");
|
||||
|
||||
const store = createSimulationStore();
|
||||
store.dispatch({ type: "SET_PROFILE", profileId: profile.id });
|
||||
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime: kinematics });
|
||||
await store.refreshKinematicsFrame();
|
||||
store.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime });
|
||||
await store.stageMachineFiles({ storage: createMemorySessionStorage() });
|
||||
store.dispatch({
|
||||
type: "LOAD_LINUXCNC_GCODE_SOURCE",
|
||||
sourceRel: "configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc",
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const loadedState = store.getState();
|
||||
assert.equal(loadedState.machineProfile, profile.id);
|
||||
assert.equal(loadedState.display?.display, undefined);
|
||||
assert.equal(loadedState.kinsType, "tcp-xyzac");
|
||||
assert.equal(loadedState.rtcpState, "on");
|
||||
assert.equal(loadedState.machineProject.gcodeDirectory.endsWith("/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples"), true);
|
||||
assert.equal(loadedState.machineProject.selectedProgram.filename, "impeller-7bl-xyzac.ngc");
|
||||
assert.equal(loadedState.programValidation.sourceGuard, "linuxcnc_vendored_5axis_gcode_source_file");
|
||||
assert.equal(loadedState.programExecution?.summary?.motionEventCount > 0, true);
|
||||
assert.equal(loadedState.toolDbReadiness.toolCount, 10);
|
||||
|
||||
const preconditions = validateRunPreconditions(loadedState, {
|
||||
requireTaskHalRuntime: false,
|
||||
requireTaskHalSession: false,
|
||||
});
|
||||
assert.equal(preconditions.ok, true);
|
||||
assert.equal(preconditions.profileId, "gmoccapy-xyzac-trt");
|
||||
assert.equal(preconditions.selectedGcodeSourceRel.endsWith("/examples/impeller-7bl-xyzac.ngc"), true);
|
||||
|
||||
console.log("gmoccapy_xyzac_trt_parity_smoke=ok");
|
||||
@@ -0,0 +1,117 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { buildProgramExecutionTiming } from "../../app/src/runtime/execution-timing.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import {
|
||||
buildTaskHalProgramMotionPlan,
|
||||
wrapTaskHalSdk,
|
||||
} from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = resolve(__dirname, "../../..");
|
||||
const sourceDir = resolve(rootDir, "web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source");
|
||||
const iniPath = resolve(sourceDir, "xyzac-trt.ini");
|
||||
const gcodePath = resolve(sourceDir, "impeller-7bl-xyzac.ngc");
|
||||
const wasmPath = resolve(rootDir, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm");
|
||||
|
||||
const iniText = readFileSync(iniPath, "utf8");
|
||||
const gcodeText = readFileSync(gcodePath, "utf8");
|
||||
const profile = {
|
||||
...getFiveAxisProfile("xyzac-trt"),
|
||||
linuxCncIniConfig: parseLinuxCncIni(iniText, {
|
||||
path: "working_run/test_linuxcnc_source/xyzac-trt.ini",
|
||||
profileId: "xyzac-trt",
|
||||
}),
|
||||
};
|
||||
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
|
||||
const execution = interpreterRuntime.runProgram(gcodeText);
|
||||
const timing = buildProgramExecutionTiming({
|
||||
motion: execution.motion,
|
||||
profile,
|
||||
feedOverride: 100,
|
||||
rapidOverride: 100,
|
||||
defaultFeedRate: 100,
|
||||
});
|
||||
const feedSegments = timing.segments.filter((segment) => segment.motionClass === "feed");
|
||||
const f159 = feedSegments.find((segment) => segment.feedRate === 159);
|
||||
const f636 = feedSegments.find((segment) => segment.feedRate === 636);
|
||||
|
||||
assert.equal(execution.summary.ready, true);
|
||||
assert.equal(execution.motion.length > 1000, true);
|
||||
assert.equal(execution.motion.some((event) => event.feedMode === "inverse-time"), true);
|
||||
assert.equal(feedSegments.length > 1000, true);
|
||||
assert.equal(f159?.feedMode, "inverse-time");
|
||||
assert.equal(f636?.feedMode, "inverse-time");
|
||||
assertNear(f159.durationSeconds, 60 / 159, "F159 inverse-time duration");
|
||||
assertNear(f636.durationSeconds, 60 / 636, "F636 inverse-time duration");
|
||||
assert.equal(f636.durationSeconds < f159.durationSeconds, true);
|
||||
assert.equal(f159.velocityMmPerMin > 0, true);
|
||||
assert.equal(f636.velocityMmPerMin > 0, true);
|
||||
|
||||
const taskHal = wrapTaskHalSdk(await createLinuxCncTaskHalSdk({
|
||||
wasmBinary: readFileSync(wasmPath),
|
||||
print() {},
|
||||
printErr() {},
|
||||
}));
|
||||
const programPath = "/work/sim/xyzac-trt/impeller-7bl-xyzac.ngc";
|
||||
taskHal.initSession({
|
||||
profileId: "xyzac-trt",
|
||||
iniPath: "/work/sim/xyzac-trt/xyzac-trt.ini",
|
||||
iniText,
|
||||
programPath,
|
||||
});
|
||||
taskHal.stageFiles([{ path: programPath, text: gcodeText }]);
|
||||
taskHal.openProgram(programPath);
|
||||
taskHal.loadProgramMotionPlan(buildTaskHalProgramMotionPlan({
|
||||
programPath,
|
||||
motion: execution.motion,
|
||||
timing,
|
||||
linearUnits: timing.linearUnits,
|
||||
}));
|
||||
taskHal.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" });
|
||||
taskHal.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" });
|
||||
taskHal.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 });
|
||||
|
||||
const samples = [];
|
||||
for (let index = 0; index < 200; index += 1) {
|
||||
taskHal.runCycles({ taskPeriodNs: 100000000, servoPeriodNs: 1000000, taskCycles: 1 });
|
||||
const status = taskHal.readStatus();
|
||||
samples.push({
|
||||
index,
|
||||
activeLine: status.ui.activeLine,
|
||||
axisPose: status.ui.axisPose,
|
||||
currentVelocity: status.ui.currentVelocity,
|
||||
taskCycle: status.ui.taskCycle,
|
||||
});
|
||||
}
|
||||
|
||||
const movedSamples = samples.filter((sample) => sample.currentVelocity > 0);
|
||||
const distinctVelocities = [...new Set(movedSamples.map((sample) => Math.round(sample.currentVelocity * 1000) / 1000))];
|
||||
|
||||
assert.equal(movedSamples.length > 20, true);
|
||||
assert.equal(distinctVelocities.length > 3, true);
|
||||
assert.equal(distinctVelocities.includes(3600), false);
|
||||
assert.equal(samples.at(0).activeLine >= 7, true);
|
||||
assert.equal(samples.at(-1).activeLine > samples.at(0).activeLine, true);
|
||||
assert.equal(samples.some((sample) => sample.activeLine === f159.line), true);
|
||||
assert.equal(samples.some((sample) => sample.activeLine === f636.line), true);
|
||||
assert.equal(samples.some((sample) => Math.abs(sample.currentVelocity - f159.velocityMmPerMin) < 0.001), true);
|
||||
assert.equal(samples.some((sample) => Math.abs(sample.currentVelocity - f636.velocityMmPerMin) < 0.001), true);
|
||||
|
||||
console.log(`impeller_motion_count=${execution.motion.length}`);
|
||||
console.log(`impeller_feed_segments=${feedSegments.length}`);
|
||||
console.log(`impeller_f159_duration_seconds=${f159.durationSeconds}`);
|
||||
console.log(`impeller_f636_duration_seconds=${f636.durationSeconds}`);
|
||||
console.log(`impeller_task_hal_distinct_velocities=${distinctVelocities.slice(0, 10).join(",")}`);
|
||||
console.log("impeller_feed_task_hal_run=ok");
|
||||
|
||||
function assertNear(actual, expected, label, tolerance = 1e-9) {
|
||||
assert.equal(Math.abs(Number(actual) - Number(expected)) <= tolerance, true, `${label}: ${actual} != ${expected}`);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
linearUnitsToMetersFactor,
|
||||
linearUnitsToMillimetersFactor,
|
||||
linearValueToMeters,
|
||||
linearValueToMillimeters,
|
||||
normalizeLinearUnits,
|
||||
resolveStateLinearUnits,
|
||||
} from "../../app/src/runtime/linear-units.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { buildProgramExecutionTiming } from "../../app/src/runtime/execution-timing.js";
|
||||
import { axesToSceneMeters } from "../../app/src/visualization/five-axis-scene.js";
|
||||
|
||||
function assertNear(actual, expected, label) {
|
||||
assert.equal(Math.abs(actual - expected) < 1e-9, true, `${label}: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
|
||||
assert.equal(normalizeLinearUnits("MM"), "mm");
|
||||
assert.equal(normalizeLinearUnits("inch"), "inch");
|
||||
assert.equal(normalizeLinearUnits("meters"), "m");
|
||||
assertNear(linearUnitsToMetersFactor("mm"), 0.001, "mm to m factor");
|
||||
assertNear(linearUnitsToMetersFactor("inch"), 0.0254, "inch to m factor");
|
||||
assertNear(linearUnitsToMetersFactor("m"), 1, "m to m factor");
|
||||
assertNear(linearUnitsToMillimetersFactor("inch"), 25.4, "inch to mm factor");
|
||||
assertNear(linearValueToMeters(25.4, "mm"), 0.0254, "25.4 mm to meters");
|
||||
assertNear(linearValueToMeters(1, "inch"), 0.0254, "1 inch to meters");
|
||||
assertNear(linearValueToMillimeters(0.5, "m"), 500, "0.5 m to millimeters");
|
||||
|
||||
const mmState = { profile: { traj: { linearUnits: "mm" } } };
|
||||
const inchState = { profile: { traj: { linearUnits: "inch" } } };
|
||||
const meterState = { profile: { traj: { linearUnits: "m" } } };
|
||||
const iniState = {
|
||||
linuxCncIniConfig: { traj: { linearUnits: "inch" } },
|
||||
profile: { traj: { linearUnits: "mm" } },
|
||||
};
|
||||
|
||||
assert.equal(resolveStateLinearUnits(iniState), "inch");
|
||||
assert.deepEqual(axesToSceneMeters({ x: 25.4, y: -10, z: 2000 }, mmState), {
|
||||
x: 0.0254,
|
||||
y: -0.01,
|
||||
z: 2,
|
||||
});
|
||||
assert.deepEqual(axesToSceneMeters({ x: 1, y: -0.5, z: 2 }, inchState), {
|
||||
x: 0.0254,
|
||||
y: -0.0127,
|
||||
z: 0.0508,
|
||||
});
|
||||
assert.deepEqual(axesToSceneMeters({ x: 0.1, y: -0.2, z: 0.3 }, meterState), {
|
||||
x: 0.1,
|
||||
y: -0.2,
|
||||
z: 0.3,
|
||||
});
|
||||
|
||||
const inchTiming = buildProgramExecutionTiming({
|
||||
profile: {
|
||||
traj: {
|
||||
linearUnits: "inch",
|
||||
maxLinearVelocity: 2,
|
||||
defaultLinearVelocity: 1,
|
||||
},
|
||||
},
|
||||
defaultFeedRate: 1,
|
||||
motion: [
|
||||
{ type: "STRAIGHT_FEED", line: 1, axes: { x: 0, y: 0, z: 0 } },
|
||||
{ type: "STRAIGHT_FEED", line: 2, axes: { x: 1, y: 0, z: 0 } },
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(inchTiming.linearUnits, "inch");
|
||||
assertNear(inchTiming.segments[1].linearDistanceMm, 25.4, "inch segment distance in mm");
|
||||
assertNear(inchTiming.segments[1].velocityMmPerMin, 25.4, "inch feed velocity in mm/min");
|
||||
|
||||
const mixedUnitsTiming = buildProgramExecutionTiming({
|
||||
profile: {
|
||||
traj: {
|
||||
linearUnits: "mm",
|
||||
maxLinearVelocity: 100,
|
||||
defaultLinearVelocity: 10,
|
||||
},
|
||||
},
|
||||
motion: [
|
||||
{ type: "STRAIGHT_FEED", line: 1, axes: { x: 0, y: 0, z: 0 }, linearUnits: "inch", feedRate: 10 },
|
||||
{ type: "STRAIGHT_FEED", line: 2, axes: { x: 1, y: 0, z: 0 }, linearUnits: "inch", feedRate: 10 },
|
||||
{ type: "STRAIGHT_FEED", line: 3, axes: { x: 25.4, y: 0, z: 0 }, linearUnits: "mm", feedRate: 100 },
|
||||
],
|
||||
});
|
||||
assertNear(mixedUnitsTiming.segments[1].linearDistanceMm, 25.4, "mixed units inch segment distance");
|
||||
assertNear(mixedUnitsTiming.segments[2].linearDistanceMm, 0, "mixed units unchanged position distance");
|
||||
|
||||
const inverseTimeTiming = buildProgramExecutionTiming({
|
||||
profile: {
|
||||
traj: {
|
||||
linearUnits: "mm",
|
||||
maxLinearVelocity: 100,
|
||||
defaultLinearVelocity: 10,
|
||||
},
|
||||
},
|
||||
motion: [
|
||||
{ type: "STRAIGHT_FEED", line: 1, axes: { x: 0, y: 0, z: 0 }, linearUnits: "mm", feedMode: "inverse-time", feedRate: 120 },
|
||||
{ type: "STRAIGHT_FEED", line: 2, axes: { x: 10, y: 0, z: 0 }, linearUnits: "mm", feedMode: "inverse-time", feedRate: 120 },
|
||||
],
|
||||
});
|
||||
assertNear(inverseTimeTiming.segments[1].durationSeconds, 0.5, "G93 inverse-time F120 duration");
|
||||
assertNear(inverseTimeTiming.segments[1].velocityMmPerMin, 1200, "G93 inverse-time velocity");
|
||||
|
||||
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
|
||||
const inchExecution = interpreterRuntime.runProgram("G20 G90\nG1 X1 F10\nM2");
|
||||
const metricExecution = interpreterRuntime.runProgram("G21 G90\nG1 X25.4 F100\nM2");
|
||||
const inverseExecution = interpreterRuntime.runProgram("G21 G90 G93\nG1 X1 F120\nM2");
|
||||
|
||||
assert.equal(inchExecution.motion[0].linearUnits, "inch");
|
||||
assert.equal(metricExecution.motion[0].linearUnits, "mm");
|
||||
assert.equal(inverseExecution.motion[0].feedMode, "inverse-time");
|
||||
assertNear(
|
||||
axesToSceneMeters(inchExecution.motion[0].axes, mmState, inchExecution.motion[0].linearUnits).x,
|
||||
0.0254,
|
||||
"G20 interpreter motion uses inch scene conversion",
|
||||
);
|
||||
assertNear(
|
||||
axesToSceneMeters(metricExecution.motion[0].axes, mmState, metricExecution.motion[0].linearUnits).x,
|
||||
0.0254,
|
||||
"G21 interpreter motion uses millimeter scene conversion",
|
||||
);
|
||||
|
||||
console.log("linear_unit_conversion_smoke=ok");
|
||||
@@ -0,0 +1,84 @@
|
||||
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.rs274ngc.halPinVars, true);
|
||||
assert.equal(xyzacIni.rs274ngc.parameterFile, "xyzac.var");
|
||||
assert.equal(xyzacIni.hal.halFiles.includes("LIB:basic_sim.tcl"), true);
|
||||
assert.equal(xyzacIni.hal.postguiHalFiles.includes("switchkins_postgui.hal"), true);
|
||||
assert.equal(xyzacIni.hal.halcmd.some((line) => line.includes("motion.analog-out-03") && line.includes("motion.switchkins-type")), true);
|
||||
assert.equal(xyzacIni.emcmot.module, "motmod");
|
||||
assert.equal(xyzacIni.emcmot.servoPeriodNs, 1000000);
|
||||
assert.equal(xyzacIni.task.module, "milltask");
|
||||
assert.equal(xyzacIni.task.cycleTimeSeconds, 0.01);
|
||||
assert.equal(xyzacIni.emcio.toolTable, "xyzac-trt.tbl");
|
||||
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.rs274ngc.parameterFile, "xyzbc.var");
|
||||
assert.equal(xyzbcIni.emcio.toolTable, "xyzbc-trt.tbl");
|
||||
assert.equal(xyzbcIni.axisLimits.B.max, 36000);
|
||||
assert.equal(xyzbcIni.jointConfig[3].axis, "B");
|
||||
assert.equal(xyzbcIni.kinematicsParameters.switchkinsTypes[1].webKinsType, "tcp-xyzbc");
|
||||
|
||||
const missingHalPinVarsIni = parseLinuxCncIni(xyzacIniText.replace(/^\s*HAL_PIN_VARS\s*=\s*1$/m, ""), {
|
||||
path: xyzacTrtProfile.iniPath,
|
||||
profileId: xyzacTrtProfile.id,
|
||||
});
|
||||
assert.equal(missingHalPinVarsIni.validation.ready, false);
|
||||
assert.equal(missingHalPinVarsIni.validation.missing.includes("RS274NGC.HAL_PIN_VARS=1"), true);
|
||||
|
||||
const missingTaskIni = parseLinuxCncIni(xyzacIniText.replace(/\n\[TASK\][\s\S]*?(?=\n\[EMCIO\])/m, "\n"), {
|
||||
path: xyzacTrtProfile.iniPath,
|
||||
profileId: xyzacTrtProfile.id,
|
||||
});
|
||||
assert.equal(missingTaskIni.validation.ready, false);
|
||||
assert.equal(missingTaskIni.validation.missing.includes("[TASK]"), true);
|
||||
assert.equal(missingTaskIni.validation.missing.includes("TASK.CYCLE_TIME"), true);
|
||||
|
||||
console.log("linuxcnc_ini_runtime_smoke=ok");
|
||||
@@ -0,0 +1,121 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
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 g18ArcProgramText = [
|
||||
"G90 G18",
|
||||
"G0 X1 Z0 Y0",
|
||||
"G3 X0 Z1 I-1 K0 Y2 F80",
|
||||
"M2",
|
||||
].join("\n");
|
||||
const g18ArcExecution = runtime.runProgram(g18ArcProgramText);
|
||||
const g18ArcMotion = g18ArcExecution.motion[1];
|
||||
|
||||
assert.equal(g18ArcExecution.summary.ready, true);
|
||||
assert.equal(g18ArcExecution.summary.motionEventCount, 2);
|
||||
assert.equal(g18ArcExecution.summary.motionTypes.includes("ARC_FEED"), true);
|
||||
assert.equal(g18ArcExecution.summary.finalAxes.x, 0);
|
||||
assert.equal(g18ArcExecution.summary.finalAxes.y, 2);
|
||||
assert.equal(g18ArcExecution.summary.finalAxes.z, 1);
|
||||
assert.equal(g18ArcMotion.type, "ARC_FEED");
|
||||
assert.equal(g18ArcMotion.axes.arc.plane, 180);
|
||||
assert.equal(g18ArcMotion.axes.arc.firstAxis, "z");
|
||||
assert.equal(g18ArcMotion.axes.arc.secondAxis, "x");
|
||||
assert.equal(g18ArcMotion.axes.arc.thirdAxis, "y");
|
||||
assert.equal(g18ArcMotion.axes.z, 1);
|
||||
assert.equal(g18ArcMotion.axes.x, 0);
|
||||
assert.equal(g18ArcMotion.axes.y, 2);
|
||||
assert.equal(g18ArcMotion.feedRate, 80);
|
||||
assert.equal(g18ArcExecution.summary.plannerRuntimeReady, true);
|
||||
|
||||
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);
|
||||
|
||||
const gmoccapyImpellerProgram = readFileSync(fileURLToPath(new URL(
|
||||
"../../../linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc",
|
||||
import.meta.url,
|
||||
)), "utf8");
|
||||
const gmoccapyImpellerExecution = runtime.runProgram(gmoccapyImpellerProgram);
|
||||
const firstG93FeedMotion = gmoccapyImpellerExecution.motion.find((event) => event.type === "STRAIGHT_FEED");
|
||||
|
||||
assert.equal(gmoccapyImpellerExecution.summary.ready, true);
|
||||
assert.equal(gmoccapyImpellerExecution.motion[0].feedMode, "inverse-time");
|
||||
assert.equal(firstG93FeedMotion.line, 9);
|
||||
assert.equal(Math.abs(firstG93FeedMotion.feedRate - 635.745) < 0.001, true);
|
||||
assert.notEqual(firstG93FeedMotion.feedRate, 318);
|
||||
|
||||
console.log("linuxcnc_interpreter_runtime_smoke=ok");
|
||||
@@ -0,0 +1,126 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
|
||||
|
||||
const DEG_TO_RAD = Math.PI / 180;
|
||||
|
||||
const cases = [
|
||||
{
|
||||
moduleId: "xyzac-trt",
|
||||
wasmFile: "linuxcnc_xyzac_trt_kinematics.wasm",
|
||||
joints: [10, 20, 30, 25, 40],
|
||||
fourthAxis: "A",
|
||||
fourthKey: "a",
|
||||
axisPose: { x: 10, y: 20, z: 30, a: 25, b: 0, c: 40 },
|
||||
kinsType: "tcp-xyzac",
|
||||
expectedToolAxis: xyzacToolAxis(25, 40),
|
||||
},
|
||||
{
|
||||
moduleId: "xyzbc-trt",
|
||||
wasmFile: "linuxcnc_xyzbc_trt_kinematics.wasm",
|
||||
joints: [10, 20, 30, 35, 40],
|
||||
fourthAxis: "B",
|
||||
fourthKey: "b",
|
||||
axisPose: { x: 10, y: 20, z: 30, a: 0, b: 35, c: 40 },
|
||||
kinsType: "tcp-xyzbc",
|
||||
expectedToolAxis: xyzbcToolAxis(35, 40),
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
const runtime = await createLinuxCncKinematicsRuntime({
|
||||
moduleId: testCase.moduleId,
|
||||
switchkinsType: 1,
|
||||
});
|
||||
const readiness = runtime.readiness();
|
||||
|
||||
assert.equal(runtime.apiName, "web-rtcp-5axis-linuxcnc-kinematics-runtime");
|
||||
assert.equal(runtime.moduleId, testCase.moduleId);
|
||||
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, testCase.wasmFile);
|
||||
assert.equal(runtime.switchkinsType, 1);
|
||||
assert.equal(readiness.loaded, true);
|
||||
assert.equal(readiness.executionContext, "direct");
|
||||
assert.equal(readiness.supportedModules.includes("xyzac-trt"), true);
|
||||
assert.equal(readiness.supportedModules.includes("xyzbc-trt"), true);
|
||||
assert.equal(runtime.switchRc, 0);
|
||||
|
||||
const linuxCncKinematicsResult = runtime.frameForJoints(testCase.joints);
|
||||
assert.equal(linuxCncKinematicsResult.moduleId, testCase.moduleId);
|
||||
assert.equal(linuxCncKinematicsResult.switchkinsType, 1);
|
||||
assert.equal(linuxCncKinematicsResult.forward.rc, 0);
|
||||
assert.equal(linuxCncKinematicsResult.inverse.rc, 0);
|
||||
assert.deepEqual(roundJoints(linuxCncKinematicsResult.inverse.joints), testCase.joints);
|
||||
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: testCase.axisPose,
|
||||
activeLine: 777,
|
||||
kinsType: testCase.kinsType,
|
||||
rtcpEnabled: true,
|
||||
profile: getFiveAxisProfile(testCase.moduleId),
|
||||
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.profileId, testCase.moduleId);
|
||||
assert.equal(frame.kinematicsModuleId, testCase.moduleId);
|
||||
assert.equal(frame.kinematicsSwitchkinsType, 1);
|
||||
assert.equal(frame.kinematicsForwardRc, 0);
|
||||
assert.equal(frame.kinematicsInverseRc, 0);
|
||||
assert.equal(frame.jointPose[3].axis, testCase.fourthAxis);
|
||||
assert.equal(frame.jointPose[3].value, testCase.joints[3]);
|
||||
assert.equal(frame.jointPose[4].axis, "C");
|
||||
assert.equal(frame.jointPose[4].value, testCase.joints[4]);
|
||||
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);
|
||||
assert.equal(frame.tcpPose.a, linuxCncKinematicsResult.forward.pose.a);
|
||||
assert.equal(frame.tcpPose.b, linuxCncKinematicsResult.forward.pose.b);
|
||||
assert.equal(frame.tcpPose.c, linuxCncKinematicsResult.forward.pose.c);
|
||||
assert.equal(frame.tcpPose[testCase.fourthKey], testCase.axisPose[testCase.fourthKey]);
|
||||
assertVectorNear(frame.toolAxisVector, testCase.expectedToolAxis);
|
||||
|
||||
console.log(`${testCase.moduleId.replace(/-/g, "_")}_kinematics_runtime=ok`);
|
||||
}
|
||||
|
||||
console.log("linuxcnc_kinematics_runtime_smoke=ok");
|
||||
console.log("xyzbc_b_axis_tcp_pose_preserved=1");
|
||||
|
||||
function roundJoints(joints) {
|
||||
return joints.map((value) => Math.round(value * 1e6) / 1e6);
|
||||
}
|
||||
|
||||
function xyzacToolAxis(aDegrees, cDegrees) {
|
||||
const tilt = aDegrees * DEG_TO_RAD;
|
||||
const c = cDegrees * DEG_TO_RAD;
|
||||
return {
|
||||
x: Math.sin(tilt) * Math.sin(c),
|
||||
y: -Math.sin(tilt) * Math.cos(c),
|
||||
z: Math.cos(tilt),
|
||||
};
|
||||
}
|
||||
|
||||
function xyzbcToolAxis(bDegrees, cDegrees) {
|
||||
const tilt = bDegrees * DEG_TO_RAD;
|
||||
const c = cDegrees * DEG_TO_RAD;
|
||||
return {
|
||||
x: Math.sin(tilt) * Math.cos(c),
|
||||
y: Math.sin(tilt) * Math.sin(c),
|
||||
z: Math.cos(tilt),
|
||||
};
|
||||
}
|
||||
|
||||
function assertVectorNear(actual, expected, tolerance = 1e-12) {
|
||||
assert.ok(Math.abs(actual.x - expected.x) < tolerance);
|
||||
assert.ok(Math.abs(actual.y - expected.y) < tolerance);
|
||||
assert.ok(Math.abs(actual.z - expected.z) < tolerance);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import {
|
||||
LINUXCNC_PARITY_ITEMS,
|
||||
RIGHT_SIDEBAR_PARITY_ENTRIES,
|
||||
SWITCHKINS_PARITY_CODES,
|
||||
} from "../../app/src/runtime/linuxcnc-parity-matrix.js";
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
function item(matrix, id) {
|
||||
const found = matrix.items.find((entry) => entry.id === id);
|
||||
assert.ok(found, `missing parity matrix item ${id}`);
|
||||
return found;
|
||||
}
|
||||
|
||||
async function waitForValidatedProgram(store) {
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
const state = store.getState();
|
||||
if (!state.interpreterExecutionPending && state.programValidation?.ready) {
|
||||
return state;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
return store.getState();
|
||||
}
|
||||
|
||||
async function loadIniConfigFromVendoredSource(profile) {
|
||||
const text = await readFile(
|
||||
new URL(`../../../wasm-port/vendor/linuxcnc/${profile.iniPath}`, import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
return parseLinuxCncIni(text, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
}
|
||||
|
||||
assert.equal(LINUXCNC_PARITY_ITEMS.length >= 11, true);
|
||||
assert.deepEqual(RIGHT_SIDEBAR_PARITY_ENTRIES, [
|
||||
"E-STOP",
|
||||
"POWER",
|
||||
"RESET",
|
||||
"AUTO",
|
||||
"MANUAL",
|
||||
"JOG",
|
||||
"MDI",
|
||||
"IDENTITY",
|
||||
"TCP",
|
||||
]);
|
||||
assert.deepEqual(SWITCHKINS_PARITY_CODES, ["M428", "M429", "M430"]);
|
||||
|
||||
const store = createSimulationStore();
|
||||
let state = store.getState();
|
||||
let matrix = state.linuxCncParityMatrix;
|
||||
|
||||
assert.equal(matrix.apiName, "web-rtcp-5axis-linuxcnc-parity-matrix");
|
||||
assert.equal(matrix.profileId, "xyzac-trt");
|
||||
assert.equal(matrix.status, "implemented");
|
||||
assert.equal(matrix.implementedCount, matrix.itemCount);
|
||||
assert.equal(matrix.rightSidebarComplete, true);
|
||||
assert.equal(matrix.switchkinsComplete, true);
|
||||
assert.deepEqual(matrix.actualRightSidebarEntries, RIGHT_SIDEBAR_PARITY_ENTRIES);
|
||||
assert.equal(matrix.requiredSwitchkinsCodes.every((code) => matrix.activeSwitchkinsCodes.includes(code)), true);
|
||||
assert.equal(matrix.linuxCncPrograms.some((path) => path.endsWith("impeller-7bl-xyzac.ngc")), true);
|
||||
assert.equal(matrix.linuxCncPrograms.some((path) => path.endsWith("boat-xyzbc.ngc")), true);
|
||||
assert.equal(matrix.linuxCncFunctions.includes("E-STOP/POWER/RESET task-state entry"), true);
|
||||
assert.equal(matrix.linuxCncFunctions.includes("M428 TCP"), true);
|
||||
assert.equal(matrix.linuxCncFunctions.includes("M429 identity"), true);
|
||||
assert.equal(matrix.linuxCncFunctions.includes("M430 userk"), true);
|
||||
|
||||
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").active, true);
|
||||
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").sourceMapped, true);
|
||||
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").linuxCncPaths.some((path) => path.endsWith("xyzac-trt.ini")), true);
|
||||
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").implemented, true);
|
||||
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").active, false);
|
||||
assert.equal(item(matrix, "gmoccapy-trt-config").linuxCncPaths.some((path) => path.includes("gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini")), true);
|
||||
assert.equal(item(matrix, "gmoccapy-trt-config").functions.includes("DISPLAY gmoccapy"), true);
|
||||
assert.equal(item(matrix, "gmoccapy-native-operator-ui").active, true);
|
||||
assert.equal(item(matrix, "right-sidebar-task-interlocks").active, true);
|
||||
assert.equal(item(matrix, "vismach-machine-preview").active, true);
|
||||
assert.equal(item(matrix, "source-derived-kinematics-switchkins").active, true);
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").active, false);
|
||||
assert.equal(item(matrix, "machine-project-directory").active, false);
|
||||
assert.equal(item(matrix, "task-hal-status-loop").implemented, true);
|
||||
assert.equal(item(matrix, "gmoccapy-postgui-tool-spindle").active, true);
|
||||
|
||||
store.dispatch({ type: "SET_PROFILE", profileId: "xyzbc-trt" });
|
||||
state = store.getState();
|
||||
matrix = state.linuxCncParityMatrix;
|
||||
assert.equal(matrix.profileId, "xyzbc-trt");
|
||||
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").active, false);
|
||||
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").active, true);
|
||||
assert.equal(item(matrix, "source-derived-kinematics-switchkins").evidence.activeSwitchkinsCodes.includes("M428"), true);
|
||||
|
||||
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
|
||||
const profile = getFiveAxisProfile("xyzac-trt");
|
||||
store.dispatch({
|
||||
type: "ATTACH_INI_CONFIG",
|
||||
profileId: profile.id,
|
||||
iniConfig: await loadIniConfigFromVendoredSource(profile),
|
||||
});
|
||||
store.dispatch({
|
||||
type: "ATTACH_INTERPRETER_RUNTIME",
|
||||
runtime: await createLinuxCncInterpreterRuntime(),
|
||||
});
|
||||
await store.stageMachineFiles({ storage: createMemorySessionStorage() });
|
||||
state = store.getState();
|
||||
matrix = state.linuxCncParityMatrix;
|
||||
assert.equal(item(matrix, "machine-project-directory").active, true);
|
||||
assert.equal(item(matrix, "machine-project-directory").evidence.iniSourceMatchesProfile, true);
|
||||
assert.equal(item(matrix, "machine-project-directory").evidence.gcodeFileCount >= 8, true);
|
||||
|
||||
store.dispatch({
|
||||
type: "LOAD_LINUXCNC_GCODE_SOURCE",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
});
|
||||
state = await waitForValidatedProgram(store);
|
||||
matrix = state.linuxCncParityMatrix;
|
||||
assert.equal(state.programValidation.ready, true);
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").active, true);
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").evidence.sourceGuard, "linuxcnc_vendored_5axis_gcode_source_file");
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").evidence.motionEventCount > 0, true);
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").evidence.plannerSampleCount > 0, true);
|
||||
assert.equal(matrix.activeCount >= 9, true);
|
||||
|
||||
console.log("linuxcnc_parity_matrix_smoke=ok");
|
||||
@@ -0,0 +1,163 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import { wrapTaskHalSdk } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = resolve(__dirname, "../../..");
|
||||
const wasmPath = resolve(rootDir, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm");
|
||||
|
||||
const sdk = await createLinuxCncTaskHalSdk({
|
||||
wasmBinary: readFileSync(wasmPath),
|
||||
print() {},
|
||||
printErr(message) {
|
||||
console.error(message);
|
||||
},
|
||||
});
|
||||
const runtime = wrapTaskHalSdk(sdk);
|
||||
const profile = getFiveAxisProfile("xyzac-trt");
|
||||
const iniText = readFileSync(
|
||||
resolve(rootDir, "wasm-port/vendor/linuxcnc", profile.iniPath),
|
||||
"utf8",
|
||||
);
|
||||
const iniConfig = parseLinuxCncIni(iniText, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
const store = createSimulationStore();
|
||||
|
||||
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
store.dispatch({
|
||||
type: "ATTACH_KINEMATICS_RUNTIME",
|
||||
runtime: await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }),
|
||||
});
|
||||
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime });
|
||||
assert.equal(store.getState().taskHalRuntimeReadiness.taskRuntimeReady, true);
|
||||
assert.equal(store.getState().taskHalRuntimeReadiness.motionRuntimeReady, true);
|
||||
assert.equal(store.getState().taskHalRuntimeReadiness.halRuntimeReady, true);
|
||||
|
||||
await store.stageMachineFiles();
|
||||
store.dispatch({
|
||||
type: "LOAD_LINUXCNC_GCODE_SOURCE",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
});
|
||||
await waitForState(store, (state) => (
|
||||
state.machineFileStaging.selectedGcodeSourceRel?.endsWith("xyzac_switchkins_test_1.ngc") &&
|
||||
state.taskHalSession?.programPath?.endsWith("xyzac_switchkins_test_1.ngc")
|
||||
));
|
||||
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
await waitForTaskHal(store);
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
await waitForTaskHal(store);
|
||||
store.dispatch({ type: "RUN" });
|
||||
await waitForTaskHal(store);
|
||||
|
||||
let state = store.getState();
|
||||
assert.equal(state.taskHalStatus.summary.taskRuntimeReady, true);
|
||||
assert.equal(state.taskHalStatus.summary.halSyncReady, true);
|
||||
assert.equal(state.taskHalStatus.ui.activeLine >= 1, true);
|
||||
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
assert.equal(state.fullExecutionBoundary.nativeTaskReady, true);
|
||||
assert.equal(state.fullExecutionBoundary.nativeHalSyncReady, true);
|
||||
|
||||
await store.initializeTaskHalSession({ openProgram: true });
|
||||
if (!store.getState().machine.powerOn) {
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
await waitForTaskHal(store);
|
||||
}
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "mdi" });
|
||||
await waitForTaskHal(store);
|
||||
store.dispatch({ type: "RUN_MDI", command: "M428" });
|
||||
await waitForTaskHal(store);
|
||||
state = store.getState();
|
||||
assert.equal(state.taskHalStatus.ui.switchkinsType, 1);
|
||||
assert.equal(state.kinsType, "tcp-xyzac");
|
||||
assert.equal(state.rtcpState, "on");
|
||||
|
||||
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||
await waitForTaskHal(store);
|
||||
store.dispatch({ type: "HOME" });
|
||||
state = store.getState();
|
||||
const homePose = { ...state.axisPose };
|
||||
assert.equal(homePose.x, 43);
|
||||
assert.equal(homePose.y, -32.15);
|
||||
assert.equal(homePose.z, -11.306);
|
||||
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 0.5 });
|
||||
await waitForTaskHal(store);
|
||||
state = store.getState();
|
||||
assert.equal(state.taskHalStatus.motionStatus.motion.teleopMode, 1);
|
||||
assert.equal(state.programRuntimeFeedback.sourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
assertNear(state.axisPose.x, homePose.x + 0.5, "task/HAL JOG X+ should keep HOME work-pose continuity");
|
||||
assertNear(state.axisPose.y, homePose.y, "task/HAL JOG X+ should not reset Y");
|
||||
assertNear(state.axisPose.z, homePose.z, "task/HAL JOG X+ should not reset Z");
|
||||
|
||||
store.dispatch({ type: "JOG", axis: "y", direction: -1, increment: 0.5 });
|
||||
await waitForTaskHal(store);
|
||||
state = store.getState();
|
||||
assertNear(state.axisPose.x, homePose.x + 0.5, "task/HAL JOG Y- should not reset X");
|
||||
assertNear(state.axisPose.y, homePose.y - 0.5, "task/HAL JOG Y- should keep HOME work-pose continuity");
|
||||
assertNear(state.axisPose.z, homePose.z, "task/HAL JOG Y- should not reset Z");
|
||||
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
await waitForTaskHal(store);
|
||||
store.dispatch({ type: "PAUSE" });
|
||||
await waitForTaskHal(store);
|
||||
assert.equal(store.getState().machine.interpState, "paused");
|
||||
|
||||
store.dispatch({ type: "RESUME" });
|
||||
await waitForTaskHal(store);
|
||||
assert.equal(store.getState().machine.interpState, "reading");
|
||||
|
||||
store.dispatch({ type: "STEP" });
|
||||
await waitForTaskHal(store);
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.interpState, "paused");
|
||||
assert.equal(state.machine.taskPaused, true);
|
||||
assert.equal(state.taskHalStatus.task.singleStepping, true);
|
||||
|
||||
store.dispatch({ type: "STOP" });
|
||||
await waitForTaskHal(store);
|
||||
state = store.getState();
|
||||
assert.equal(state.runState, "stopped");
|
||||
assert.equal(state.machine.interpState, "idle");
|
||||
assert.equal(state.taskHalStatus.motionStatus.motion.aborted, true);
|
||||
|
||||
console.log("linuxcnc_task_hal_runtime_smoke=ok");
|
||||
console.log("task_hal_machine_file_smoke=ok");
|
||||
console.log("switchkins_remap_hal_sync_smoke=ok");
|
||||
console.log("browser_task_hal_worker_smoke=ok");
|
||||
|
||||
async function waitForTaskHal(store) {
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
if (!store.getState().taskHalExecutionPending) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
if (!store.getState().taskHalExecutionPending) return store.getState();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
throw new Error("task/HAL store command did not settle");
|
||||
}
|
||||
|
||||
async function waitForState(store, predicate) {
|
||||
for (let attempt = 0; attempt < 60; attempt += 1) {
|
||||
const state = store.getState();
|
||||
if (predicate(state)) return state;
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
throw new Error("store state condition did not settle");
|
||||
}
|
||||
|
||||
function assertNear(actual, expected, message) {
|
||||
assert.equal(Math.abs(Number(actual) - Number(expected)) < 1e-9, true, `${message}: ${actual} !== ${expected}`);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
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.gcodeFileCount, 16);
|
||||
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.storageMode, "memory");
|
||||
assert.equal(staged.save.semanticBoundary, "memory_machine_file_text_staging_current_page_lifecycle_only");
|
||||
assert.equal(staged.save.summary.gcodeFileCount, 16);
|
||||
assert.equal(staged.save.gcodeFiles.length, 16);
|
||||
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.gcodeFiles.some((file) => file.sourceRel.endsWith("remap_subs/430remap.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.storageMode, "memory");
|
||||
assert.equal(state.machineFileStaging.storageCapability.reason, "explicit_storage");
|
||||
assert.equal(state.machineFileStaging.save.summary.gcodeFileCount, 16);
|
||||
assert.equal(state.machineFileStaging.save.summary.kinds.remap >= 3, true);
|
||||
assert.equal(state.machineFileStaging.gcodeSources.length >= 4, true);
|
||||
assert.equal(state.machineFileStaging.selectedGcodeSourceRel, null);
|
||||
assert.equal(state.toolDbReadiness.toolDbProcessReady, true);
|
||||
assert.equal(state.toolDbReadiness.toolDbProcessScope, "web_simulation_only");
|
||||
assert.equal(state.toolDbReadiness.hostToolDbProcessReady, false);
|
||||
assert.equal(state.toolDbReadiness.toolCount, 10);
|
||||
assert.equal(state.toolDbSimulation.toolTable.entries[1].toolNumber, 2);
|
||||
assert.equal(state.toolDbSimulation.toolTable.entries[1].offset.z, 15);
|
||||
assert.equal(state.controlledUserMReadiness.externalUserMProcessReady, true);
|
||||
assert.equal(state.controlledUserMReadiness.externalUserMProcessScope, "web_simulation_only");
|
||||
|
||||
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);
|
||||
assert.equal(sourceState.toolDbReadiness.toolDbProcessReady, true);
|
||||
assert.equal(sourceState.controlledUserMReadiness.externalUserMProcessReady, 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, true);
|
||||
assert.equal(runState.machineFileExecution.summary.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.equal(runState.machineFileExecution.plannerTiming.samples.length > 0, true);
|
||||
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, true);
|
||||
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.satisfied.includes("linuxcnc-tp-queue-runtime-timing"), true);
|
||||
|
||||
globalThis.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__ = true;
|
||||
const fallbackStore = createSimulationStore();
|
||||
const fallbackStage = await fallbackStore.stageMachineFiles();
|
||||
const fallbackState = fallbackStore.getState();
|
||||
assert.equal(fallbackStage.save.storageMode, "memory-fallback");
|
||||
assert.equal(fallbackStage.save.storageCapability.opfsUnavailable, true);
|
||||
assert.equal(fallbackStage.save.storageCapability.reason, "forced_unavailable");
|
||||
assert.equal(fallbackState.machineFileStaging.status, "staged");
|
||||
assert.equal(fallbackState.machineFileStaging.storageMode, "memory-fallback");
|
||||
assert.equal(fallbackState.toolDbReadiness.toolDbProcessReady, true);
|
||||
assert.equal(fallbackState.operatorMessage.includes("staged"), true);
|
||||
delete globalThis.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__;
|
||||
|
||||
console.log("machine_file_staging_smoke=ok");
|
||||
@@ -0,0 +1,211 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
import { createFullLinuxCncExecutionBoundary } from "../../app/src/runtime/full-execution-boundary.js";
|
||||
import { createNativeTaskHalReadinessAudit } from "../../app/src/runtime/native-task-hal-audit.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const projectDir = resolve(__dirname, "../..");
|
||||
const rootDir = resolve(projectDir, "..");
|
||||
const phase0Script = resolve(rootDir, "wasm-port/tests/native/verify_task_hal_phase0.sh");
|
||||
const manifestLog = resolve(rootDir, "wasm-port/build/task-hal/verify_task_hal_source_manifest.stdout.log");
|
||||
const probeLog = resolve(rootDir, "wasm-port/build/task-hal/probe_trt_task_hal_runtime.stdout.log");
|
||||
const manifestReport = resolve(rootDir, "wasm-port/build/task-hal/task-hal-source-manifest.tsv");
|
||||
const artifactDir = resolve(projectDir, "build/readiness");
|
||||
const artifactPath = resolve(artifactDir, "native-task-hal-readiness.json");
|
||||
|
||||
run(phase0Script);
|
||||
|
||||
const sourceManifest = parseKeyValueFile(manifestLog);
|
||||
const nativeProbe = parseKeyValueFile(probeLog);
|
||||
const fullExecutionBoundary = createFullLinuxCncExecutionBoundary(createPromotedSimulationState());
|
||||
const audit = createNativeTaskHalReadinessAudit({
|
||||
sourceManifest,
|
||||
nativeProbe,
|
||||
fullExecutionBoundary,
|
||||
taskHalRuntimeReadiness: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
},
|
||||
taskHalStatus: {
|
||||
summary: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
taskHalComparisonReady: true,
|
||||
},
|
||||
},
|
||||
generatedAt: "2026-06-22T00:00:00.000Z",
|
||||
artifactPaths: {
|
||||
readinessJson: relativeFromRoot(artifactPath),
|
||||
sourceManifestLog: relativeFromRoot(manifestLog),
|
||||
sourceManifestReport: relativeFromRoot(manifestReport),
|
||||
nativeProbeLog: relativeFromRoot(probeLog),
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(audit.status, "ok");
|
||||
assert.equal(audit.taskHalWebSimulationBoundaryConsistent, true);
|
||||
assert.equal(audit.webSimulation.promoted, true);
|
||||
assert.equal(audit.webSimulation.nativeTaskReady, true);
|
||||
assert.equal(audit.webSimulation.nativeHalSyncReady, true);
|
||||
assert.equal(audit.webSimulation.fullLinuxCncProgramExecutionReady, true);
|
||||
assert.equal(audit.nativeHostAndHardware.nativeProbe, "ok");
|
||||
assert.match(audit.nativeHostAndHardware.nativeProbeStatus, /^(passed|ready_disabled_by_default|skipped_missing_host_runtime)$/);
|
||||
assert.equal(audit.nativeHostAndHardware.nativePromotionAllowed, false);
|
||||
assert.equal(audit.nativeHostAndHardware.hardwareDrive, false);
|
||||
assert.equal(audit.nativeHostAndHardware.hostRealtimeKernel, false);
|
||||
assert.equal(audit.nativeHostAndHardware.externalUserMProcessReady, false);
|
||||
assert.equal(audit.nativeHostAndHardware.toolDbProcessReady, false);
|
||||
assert.equal(audit.nativeHostAndHardware.hostExternalUserMProcessReady, false);
|
||||
assert.equal(audit.nativeHostAndHardware.hostToolDbProcessReady, false);
|
||||
assert.equal(audit.nativeHostAndHardware.arbitraryUserMExecution, false);
|
||||
assert.equal(audit.toolUserWebSimulation.externalUserMProcessReady, true);
|
||||
assert.equal(audit.toolUserWebSimulation.externalUserMProcessScope, "web_simulation_only");
|
||||
assert.equal(audit.toolUserWebSimulation.toolDbProcessReady, true);
|
||||
assert.equal(audit.toolUserWebSimulation.toolDbProcessScope, "web_simulation_only");
|
||||
assert.equal(audit.toolUserWebSimulation.ready, true);
|
||||
assert.equal(audit.sourceManifest.ready, true);
|
||||
assert.equal(audit.sourceManifest.taskSourceCount > 0, true);
|
||||
assert.equal(audit.sourceManifest.halSourceCount > 0, true);
|
||||
assert.equal(audit.sourceManifest.motionSourceCount > 0, true);
|
||||
assert.equal(audit.gates.task_hal_web_simulation_boundary_consistent, 1);
|
||||
assert.equal(audit.gates.hardware_drive, 0);
|
||||
assert.equal(audit.gates.host_realtime_kernel, 0);
|
||||
assert.equal(audit.gates.external_user_m_process_ready, 1);
|
||||
assert.equal(audit.gates.tool_db_process_ready, 1);
|
||||
assert.equal(audit.gates.host_external_user_m_process_ready, 0);
|
||||
assert.equal(audit.gates.host_tool_db_process_ready, 0);
|
||||
assert.equal(audit.gates.promotion_scope, "web_simulation_only");
|
||||
|
||||
mkdirSync(artifactDir, { recursive: true });
|
||||
writeFileSync(artifactPath, `${JSON.stringify(audit, null, 2)}\n`);
|
||||
|
||||
const saved = JSON.parse(readFileSync(artifactPath, "utf8"));
|
||||
assert.equal(saved.apiName, "web-rtcp-5axis-native-task-hal-readiness-audit");
|
||||
assert.equal(saved.status, "ok");
|
||||
|
||||
console.log("native_task_hal_source_artifact_audit=ok");
|
||||
console.log(`native_task_hal_readiness_artifact=${relativeFromRoot(artifactPath)}`);
|
||||
console.log("task_hal_web_simulation_boundary_consistent=1");
|
||||
console.log(`native_task_hal_host_probe_status=${audit.nativeHostAndHardware.nativeProbeStatus}`);
|
||||
console.log("hardware_drive=0");
|
||||
console.log("host_realtime_kernel=0");
|
||||
console.log("external_user_m_process_ready=1");
|
||||
console.log("tool_db_process_ready=1");
|
||||
console.log("host_external_user_m_process_ready=0");
|
||||
console.log("host_tool_db_process_ready=0");
|
||||
console.log("promotion_scope=web_simulation_only");
|
||||
|
||||
function createPromotedSimulationState() {
|
||||
const programExecution = {
|
||||
sourceMode: "linuxcnc-interpreter-wasm",
|
||||
plannerTiming: {
|
||||
plannerRuntimeReady: true,
|
||||
semanticBoundary: "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
},
|
||||
summary: {
|
||||
motionEventCount: 3,
|
||||
canonicalEventCount: 8,
|
||||
plannerRuntimeReady: true,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
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",
|
||||
},
|
||||
programExecution,
|
||||
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=1",
|
||||
].join("\n"),
|
||||
},
|
||||
taskHalRuntimeReadiness: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
},
|
||||
taskHalStatus: {
|
||||
summary: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
taskHalComparisonReady: true,
|
||||
},
|
||||
ui: {
|
||||
taskCycle: 2,
|
||||
servoCycle: 20,
|
||||
motionQueueDepth: 0,
|
||||
halChangedPinCount: 4,
|
||||
},
|
||||
},
|
||||
toolDbReadiness: {
|
||||
toolDbProcessReady: true,
|
||||
toolDbProcessScope: "web_simulation_only",
|
||||
hostToolDbProcessReady: false,
|
||||
toolCount: 10,
|
||||
},
|
||||
controlledUserMReadiness: {
|
||||
externalUserMProcessReady: true,
|
||||
externalUserMProcessScope: "web_simulation_only",
|
||||
hostExternalUserMProcessReady: false,
|
||||
arbitraryUserMExecution: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function run(scriptPath) {
|
||||
const result = spawnSync("bash", [scriptPath], {
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.stdout.write(result.stdout || "");
|
||||
process.stderr.write(result.stderr || "");
|
||||
throw new Error(`${scriptPath} failed with status ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseKeyValueFile(path) {
|
||||
const fields = {};
|
||||
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const equals = line.indexOf("=");
|
||||
if (equals <= 0) continue;
|
||||
fields[line.slice(0, equals)] = line.slice(equals + 1);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function relativeFromRoot(path) {
|
||||
return path.startsWith(`${rootDir}/`) ? path.slice(rootDir.length + 1) : path;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
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";
|
||||
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);
|
||||
assert.deepEqual(fiveAxisProfiles.map(({ id }) => id), [
|
||||
"xyzac-trt",
|
||||
"xyzbc-trt",
|
||||
"gmoccapy-xyzac-trt",
|
||||
"gmoccapy-xyzab",
|
||||
]);
|
||||
|
||||
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);
|
||||
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 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 gmoccapyXyzacTrtProfile = getFiveAxisProfile("gmoccapy-xyzac-trt");
|
||||
assert.equal(gmoccapyXyzacTrtProfile.id, "gmoccapy-xyzac-trt");
|
||||
assert.equal(gmoccapyXyzacTrtProfile.display.display, "gmoccapy");
|
||||
assert.equal(gmoccapyXyzacTrtProfile.kinematics, "xyzac-trt-kins");
|
||||
assert.equal(gmoccapyXyzacTrtProfile.kinematicsModuleId, "xyzac-trt");
|
||||
assert.equal(gmoccapyXyzacTrtProfile.rtcpProof, true);
|
||||
assert.equal(gmoccapyXyzacTrtProfile.kinematicsParameters.fixedTrtDefault, true);
|
||||
const gmoccapyTrtSourceSummary = createProfileSourceReferenceSummary("gmoccapy-xyzac-trt");
|
||||
assert.equal(gmoccapyTrtSourceSummary.referenceCount >= 10, true);
|
||||
assert.ok(gmoccapyTrtSourceSummary.references.some((reference) => reference.path.endsWith("non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini")));
|
||||
assert.ok(gmoccapyTrtSourceSummary.references.some((reference) => reference.path.endsWith("examples/impeller-7bl-xyzac.ngc")));
|
||||
|
||||
const gmoccapyXyzabProfile = getFiveAxisProfile("gmoccapy-xyzab");
|
||||
assert.equal(gmoccapyXyzabProfile.id, "gmoccapy-xyzab");
|
||||
assert.deepEqual(gmoccapyXyzabProfile.coordinates, ["X", "Y", "Z", "A", "B"]);
|
||||
assert.equal(gmoccapyXyzabProfile.kinematics, "trivkins coordinates=xyzab");
|
||||
assert.equal(gmoccapyXyzabProfile.tcpCapable, false);
|
||||
assert.equal(gmoccapyXyzabProfile.rtcpProof, false);
|
||||
assert.equal(gmoccapyXyzabProfile.promotionAllowed, false);
|
||||
assert.equal(gmoccapyXyzabProfile.hal.postguiRequiresHalcompReady, true);
|
||||
assert.equal(gmoccapyXyzabProfile.hal.halcmd.toolChange.strategy, "iocontrol-loopback");
|
||||
assert.equal(gmoccapyXyzabProfile.hal.halcmd.toolChange.manualGmoccapyPinsConnected, false);
|
||||
const gmoccapySourceSummary = createProfileSourceReferenceSummary("gmoccapy-xyzab");
|
||||
assert.equal(gmoccapySourceSummary.referenceCount >= 10, true);
|
||||
assert.ok(gmoccapySourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy_XYZAB.ini")));
|
||||
assert.ok(gmoccapySourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy_postgui.hal")));
|
||||
|
||||
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");
|
||||
@@ -0,0 +1,130 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
import {
|
||||
selectMachineFileProgram,
|
||||
stageProfileMachineFiles,
|
||||
} from "../../app/src/runtime/linuxcnc-machine-file-staging.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
const TRT_DEMO_PREFIX = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/";
|
||||
const DIRECT_CANONICAL_CASES = [
|
||||
"boat-xyzac.ngc",
|
||||
"boat-xyzbc.ngc",
|
||||
"impeller-7bl-xyzac.ngc",
|
||||
"xyzac_switchkins_test_1.ngc",
|
||||
"xyzac_switchkins_test_2.ngc",
|
||||
"xyzac_switchkins_test_3.ngc",
|
||||
];
|
||||
const ENTRY_MACHINE_FILE_CASES = [
|
||||
"xyzac_switchkins.ngc",
|
||||
"xyzbc_switchkins.ngc",
|
||||
];
|
||||
|
||||
const runtime = await createLinuxCncInterpreterRuntime();
|
||||
const staged = await stageProfileMachineFiles(getFiveAxisProfile("xyzac-trt"), {
|
||||
storage: createMemorySessionStorage(),
|
||||
});
|
||||
const sourceByFilename = new Map(staged.save.gcodeSources.map((source) => [source.filename, source]));
|
||||
|
||||
for (const filename of [...DIRECT_CANONICAL_CASES, ...ENTRY_MACHINE_FILE_CASES]) {
|
||||
const source = sourceByFilename.get(filename);
|
||||
assert.ok(source, `${filename} must be present in staged LinuxCNC demo sources`);
|
||||
assert.equal(source.sourceRel, `${TRT_DEMO_PREFIX}${filename}`);
|
||||
assert.equal(source.sourceMode, "linuxcnc-vendored-5axis-gcode");
|
||||
assert.equal(source.semanticBoundary, "linuxcnc_vendored_5axis_gcode_source_file");
|
||||
}
|
||||
|
||||
const canonicalReports = [];
|
||||
for (const filename of DIRECT_CANONICAL_CASES) {
|
||||
const source = sourceByFilename.get(filename);
|
||||
const file = staged.save.files.find((candidate) => candidate.sourceRel === source.sourceRel);
|
||||
const execution = runtime.runProgram(file.text);
|
||||
|
||||
assert.equal(execution.sourceMode, "linuxcnc-interpreter-wasm", `${filename} source mode`);
|
||||
assert.equal(execution.semanticBoundary, "linuxcnc_interpreter_wasm_canonical_events", `${filename} boundary`);
|
||||
assert.equal(execution.summary.motionEventCount > 0, true, `${filename} canonical motion`);
|
||||
assert.equal(execution.summary.ready, true, `${filename} ready`);
|
||||
assert.equal(execution.summary.plannerRuntimeReady, true, `${filename} TP timing ready`);
|
||||
assert.equal(execution.plannerTiming.semanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion");
|
||||
assert.equal(execution.plannerTiming.motionCount, execution.motion.length, `${filename} TP motion count`);
|
||||
assert.equal(execution.plannerTiming.segments.length, execution.motion.length, `${filename} TP segment count`);
|
||||
assert.equal(execution.plannerTiming.samples.length > 0, true, `${filename} TP samples`);
|
||||
|
||||
const firstMotionLine = execution.motion.find((event) => Number.isFinite(Number(event.line)))?.line;
|
||||
assert.equal(Number.isFinite(Number(firstMotionLine)), true, `${filename} current source line`);
|
||||
assert.equal(execution.motion.every((event) => event.statement !== undefined), true, `${filename} source statements`);
|
||||
|
||||
if (execution.summary.switchkinsEventCount > 0) {
|
||||
assert.equal(
|
||||
execution.switchkinsEvents.some((event) => event.switchkinsType === 1),
|
||||
true,
|
||||
`${filename} switchkins TCP event`,
|
||||
);
|
||||
assert.equal(
|
||||
execution.switchkinsEvents.some((event) => event.switchkinsType === 0),
|
||||
true,
|
||||
`${filename} switchkins identity event`,
|
||||
);
|
||||
assert.equal(
|
||||
execution.summary.switchkinsRemapBoundary,
|
||||
"linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
|
||||
`${filename} switchkins boundary`,
|
||||
);
|
||||
}
|
||||
|
||||
canonicalReports.push({
|
||||
filename,
|
||||
previewPoints: execution.motion.length,
|
||||
executedPathPoints: execution.plannerTiming.samples.length,
|
||||
currentLine: firstMotionLine,
|
||||
switchkinsEvents: execution.summary.switchkinsEventCount,
|
||||
});
|
||||
}
|
||||
|
||||
for (const filename of ENTRY_MACHINE_FILE_CASES) {
|
||||
const source = sourceByFilename.get(filename);
|
||||
const selectedPlan = selectMachineFileProgram(staged.plan, staged.save, source.sourceRel);
|
||||
const execution = runtime.runMachineFileProgram({
|
||||
plan: selectedPlan,
|
||||
files: staged.save.files,
|
||||
executionMode: "fiveAxisRemap",
|
||||
});
|
||||
|
||||
assert.equal(execution.sourceMode, "linuxcnc-machine-file-remap-wasm", `${filename} machine-file source`);
|
||||
assert.equal(execution.summary.machineFileExecutionReady, true, `${filename} machine-file ready`);
|
||||
assert.equal(execution.summary.remapRuntimeReady, true, `${filename} remap ready`);
|
||||
assert.equal(execution.resultText.includes("fiveaxis_ini_open=1"), true, `${filename} INI proof`);
|
||||
assert.equal(execution.resultText.includes("fiveaxis_remaps_ready=1"), true, `${filename} remap proof`);
|
||||
assert.equal(execution.resultText.includes("fiveaxis_file_reached_exit=1"), true, `${filename} exit proof`);
|
||||
assert.equal(execution.machineFilePlan.selectedProgramFilename, filename);
|
||||
assert.equal(execution.machineFilePlan.selectedProgramSourceRel, source.sourceRel);
|
||||
}
|
||||
|
||||
const store = createSimulationStore();
|
||||
assert.equal(store.getState().programExecutionSourceMode, "fixture-line-playback");
|
||||
assert.equal(store.getState().programExecution, null);
|
||||
assert.equal(store.getState().fullExecutionBoundary.promotionAllowed, false);
|
||||
|
||||
assert.equal(canonicalReports.length >= 5, true);
|
||||
assert.equal(canonicalReports.every((report) => report.previewPoints > 0), true);
|
||||
assert.equal(canonicalReports.every((report) => report.executedPathPoints > 0), true);
|
||||
assert.equal(canonicalReports.every((report) => Number.isFinite(Number(report.currentLine))), true);
|
||||
assert.equal(canonicalReports.some((report) => report.filename === "boat-xyzac.ngc"), true);
|
||||
assert.equal(canonicalReports.some((report) => report.filename === "boat-xyzbc.ngc"), true);
|
||||
assert.equal(canonicalReports.some((report) => report.filename === "impeller-7bl-xyzac.ngc"), true);
|
||||
assert.equal(canonicalReports.some((report) => report.filename.startsWith("xyzac_switchkins")), true);
|
||||
assert.equal(sourceByFilename.has("xyzbc_switchkins.ngc"), true);
|
||||
assert.equal(canonicalReports.some((report) => report.switchkinsEvents > 0), true);
|
||||
|
||||
console.log(`linuxcnc_source_program_case_count=${canonicalReports.length + ENTRY_MACHINE_FILE_CASES.length}`);
|
||||
console.log("all_cases_program_preview_points=ok");
|
||||
console.log("all_cases_executed_path_points=ok_after_run_or_step");
|
||||
console.log("all_cases_toolpath_preview_source=linuxcnc_interpreter_canonical_motion");
|
||||
console.log("all_cases_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback");
|
||||
console.log("all_switchkins_cases_rtcp_state_changes_verified=1");
|
||||
console.log("all_cases_source_guard=linuxcnc_vendored_5axis_gcode_source_file");
|
||||
console.log("fixture_toolpath_fallback_not_promoted=1");
|
||||
console.log("real_linuxcnc_5axis_program_cases_smoke=ok");
|
||||
@@ -0,0 +1,531 @@
|
||||
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 },
|
||||
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 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 },
|
||||
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);
|
||||
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 });
|
||||
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.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);
|
||||
|
||||
kinematicsStore.dispatch({ type: "SET_RTCP", enabled: true });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.rtcpState, "off");
|
||||
assert.equal(kinematicsState.operatorMessage, "kinematics blocked: machine must be on");
|
||||
|
||||
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
|
||||
kinematicsStore.dispatch({ type: "HOME" });
|
||||
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: "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.deepEqual(
|
||||
store.getState().availableProfiles.map((profile) => profile.id),
|
||||
["xyzac-trt", "xyzbc-trt", "gmoccapy-xyzac-trt", "gmoccapy-xyzab"],
|
||||
);
|
||||
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: "TOGGLE_POWER" });
|
||||
store.dispatch({ type: "HOME" });
|
||||
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");
|
||||
assert.equal(Number.isFinite(state.tcpPose.b), true);
|
||||
assert.equal(state.rtcpFrame.tcpPose.b, state.tcpPose.b);
|
||||
assert.equal(state.rtcpFrame.tcpPose.b, state.rtcpFrame.axisPose.b);
|
||||
|
||||
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
|
||||
store.dispatch({ type: "RESET" });
|
||||
store.dispatch({ type: "RUN" });
|
||||
assert.equal(store.getState().activeLine, 501);
|
||||
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 });
|
||||
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: "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, "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 = await waitForInterpreterExecution(store);
|
||||
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);
|
||||
|
||||
const floodBeforeToggle = store.getState().coolant.flood;
|
||||
store.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
|
||||
state = store.getState();
|
||||
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, !mistBeforeToggle);
|
||||
|
||||
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: "SET_MODE", mode: "manual" });
|
||||
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.machine.powerOn, false);
|
||||
assert.equal(state.machine.taskState, "estop-reset");
|
||||
assert.equal(state.operatorMessage, "estop reset; machine off");
|
||||
|
||||
const taskHalSwitchkinsStore = createSimulationStore({
|
||||
programStartLine: 1,
|
||||
activeLine: 5,
|
||||
kinsType: "tcp-xyzac",
|
||||
rtcpState: "on",
|
||||
programExecutionSourceMode: "linuxcnc-interpreter-wasm",
|
||||
programExecution: {
|
||||
sourceMode: "linuxcnc-interpreter-wasm",
|
||||
motion: [
|
||||
{
|
||||
type: "STRAIGHT_TRAVERSE",
|
||||
line: 5,
|
||||
axes: { x: 1, y: 2, z: 3, a: 10, c: 20 },
|
||||
switchkinsType: 1,
|
||||
kinsType: "tcp",
|
||||
},
|
||||
{
|
||||
type: "STRAIGHT_FEED",
|
||||
line: 20,
|
||||
axes: { x: 2, y: 3, z: 4, a: 0, c: 0 },
|
||||
switchkinsType: 0,
|
||||
kinsType: "identity",
|
||||
},
|
||||
],
|
||||
summary: {
|
||||
motionEventCount: 2,
|
||||
switchkinsEventCount: 2,
|
||||
switchkinsCodes: ["M428", "M429"],
|
||||
},
|
||||
},
|
||||
});
|
||||
taskHalSwitchkinsStore.dispatch({
|
||||
type: "TASK_HAL_STATUS_APPLIED",
|
||||
status: taskHalStatusForSwitchkinsLine({ activeLine: 5, switchkinsType: 0 }),
|
||||
operatorMessage: "task/HAL status retained program TCP switchkins",
|
||||
});
|
||||
state = taskHalSwitchkinsStore.getState();
|
||||
assert.equal(state.activeLine, 5);
|
||||
assert.equal(state.kinsType, "tcp-xyzac");
|
||||
assert.equal(state.rtcpState, "on");
|
||||
|
||||
taskHalSwitchkinsStore.dispatch({
|
||||
type: "TASK_HAL_STATUS_APPLIED",
|
||||
status: taskHalStatusForSwitchkinsLine({ activeLine: 20, switchkinsType: 0 }),
|
||||
operatorMessage: "task/HAL status applied program identity switchkins",
|
||||
});
|
||||
state = taskHalSwitchkinsStore.getState();
|
||||
assert.equal(state.activeLine, 20);
|
||||
assert.equal(state.kinsType, "identity");
|
||||
assert.equal(state.rtcpState, "off");
|
||||
|
||||
console.log("rtcp_store_smoke=ok");
|
||||
|
||||
function taskHalStatusForSwitchkinsLine({ activeLine, switchkinsType }) {
|
||||
return {
|
||||
semanticBoundary: "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
task: {
|
||||
state: "ON",
|
||||
mode: "AUTO",
|
||||
interpState: "READING",
|
||||
execState: "WAITING_FOR_MOTION",
|
||||
},
|
||||
motionStatus: {
|
||||
motion: {
|
||||
programLine: activeLine,
|
||||
motionType: 1,
|
||||
switchkinsType,
|
||||
currentVel: 1,
|
||||
requestedVel: 1,
|
||||
inPosition: false,
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
taskState: "on",
|
||||
taskMode: "auto",
|
||||
interpState: "reading",
|
||||
activeLine,
|
||||
switchkinsType,
|
||||
axisPoseFrame: "work",
|
||||
axisPose: { x: 1, y: 2, z: 3, a: 10, b: 0, c: 20 },
|
||||
currentVelocity: 60,
|
||||
servoCycle: 1,
|
||||
taskCycle: 1,
|
||||
motionQueueDepth: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
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 { wrapTaskHalSdk } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = resolve(__dirname, "../../..");
|
||||
const wasmPath = resolve(rootDir, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm");
|
||||
|
||||
await verifyRunFeedbackLoop({
|
||||
profileId: "xyzac-trt",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
stopAction: "STOP",
|
||||
});
|
||||
|
||||
await verifyRunReadySequence({
|
||||
profileId: "xyzac-trt",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
});
|
||||
|
||||
await verifyRunFeedbackLoop({
|
||||
profileId: "xyzac-trt",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
stopAction: "ABORT",
|
||||
});
|
||||
|
||||
await verifyRunFeedbackLoop({
|
||||
profileId: "xyzbc-trt",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
|
||||
stopAction: "STOP",
|
||||
});
|
||||
|
||||
console.log("run_feedback_status_loop_smoke=ok");
|
||||
console.log("run_ready_sequence_smoke=ok");
|
||||
|
||||
async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) {
|
||||
const sdk = await createLinuxCncTaskHalSdk({
|
||||
wasmBinary: readFileSync(wasmPath),
|
||||
print() {},
|
||||
printErr(message) {
|
||||
console.error(message);
|
||||
},
|
||||
});
|
||||
const profile = getFiveAxisProfile(profileId);
|
||||
const iniText = readFileSync(resolve(rootDir, "wasm-port/vendor/linuxcnc", profile.iniPath), "utf8");
|
||||
const iniConfig = parseLinuxCncIni(iniText, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
const kinematicsModuleId = iniConfig.kinematicsModuleId;
|
||||
const store = createSimulationStore();
|
||||
|
||||
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
store.dispatch({
|
||||
type: "ATTACH_KINEMATICS_RUNTIME",
|
||||
runtime: await createLinuxCncKinematicsRuntime({ moduleId: kinematicsModuleId }),
|
||||
});
|
||||
store.dispatch({
|
||||
type: "ATTACH_INTERPRETER_RUNTIME",
|
||||
runtime: await createLinuxCncInterpreterRuntime(),
|
||||
});
|
||||
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime: wrapTaskHalSdk(sdk) });
|
||||
await store.stageMachineFiles();
|
||||
store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
|
||||
await waitForState(store, (state) => (
|
||||
state.taskHalSession?.programPath?.endsWith(sourceRel.split("/").at(-1)) &&
|
||||
state.rtcpFrame?.sourceMode === "source-derived-kinematics-wasm"
|
||||
));
|
||||
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
await waitForTaskHalCommand(store);
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
await waitForTaskHalCommand(store);
|
||||
|
||||
store.dispatch({ type: "RUN" });
|
||||
await waitForTaskHalCommand(store);
|
||||
await waitForState(store, (state) => distinctActiveLines(state.programRuntimeFeedbackHistory).length >= 3);
|
||||
|
||||
let state = store.getState();
|
||||
const history = state.programRuntimeFeedbackHistory;
|
||||
if (state.taskHalStatusLoop.profileId) {
|
||||
assert.equal(state.taskHalStatusLoop.profileId, profileId);
|
||||
}
|
||||
if (state.taskHalStatusLoop.kinematicsModuleId) {
|
||||
assert.equal(state.taskHalStatusLoop.kinematicsModuleId, kinematicsModuleId);
|
||||
}
|
||||
assert.equal(state.taskHalStatusLoop.taskPeriodNs, 10000000);
|
||||
assert.equal(state.taskHalStatusLoop.servoPeriodNs, 1000000);
|
||||
assert.equal(history.length >= 3, true);
|
||||
assert.equal(history.every((entry) => entry.sourceMode === "linuxcnc-task-motion-hal-wasm"), true);
|
||||
assert.equal(history.every((entry) => entry.semanticBoundary === "linuxcnc_task_motion_hal_wasm_simulation_runtime"), true);
|
||||
assert.equal(history.some((entry) => entry.sourceMode === "fixture-line-playback"), false);
|
||||
assert.equal(history.every((entry) => entry.activeLineSource === "motion-status"), true);
|
||||
assert.equal(history.every((entry) => entry.activeLineHalSynced === true), true);
|
||||
assert.equal(history.every((entry) => entry.line === entry.motionProgramLine), true);
|
||||
assert.equal(history.every((entry) => entry.line === entry.halProgramLine), true);
|
||||
assert.equal(history.some((entry) => entry.currentVelocityMmPerMin > 0), true);
|
||||
assert.equal(history.some((entry) => entry.currentVelocityMmPerMin !== 3600), true);
|
||||
assert.equal(isMonotonic(history.map((entry) => entry.taskCycle).reverse()), true);
|
||||
assert.equal(isMonotonic(history.map((entry) => entry.cycle).reverse()), true);
|
||||
const activeLines = distinctActiveLines(history);
|
||||
assert.equal(activeLines.length >= 3, true, `${sourceRel} activeLines=${activeLines.join(",")}`);
|
||||
assert.equal(isMonotonic(activeLines), true, `${sourceRel} activeLines=${activeLines.join(",")}`);
|
||||
assert.equal(
|
||||
sourceRel.endsWith("boat-xyzbc.ngc") ? activeLines.join(",") !== "1,10" : true,
|
||||
true,
|
||||
`${sourceRel} activeLines=${activeLines.join(",")}`,
|
||||
);
|
||||
assert.equal(
|
||||
sourceRel.endsWith("boat-xyzbc.ngc") ? activeLines.some((line) => line > activeLines.indexOf(line) + 1) : true,
|
||||
true,
|
||||
`${sourceRel} activeLines=${activeLines.join(",")}`,
|
||||
);
|
||||
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
assert.equal(state.taskHalStatus.summary.taskRuntimeReady, true);
|
||||
assert.equal(state.taskHalStatus.summary.halSyncReady, true);
|
||||
|
||||
const cycleAfterRun = state.taskHalStatus.ui.taskCycle;
|
||||
store.dispatch({ type: "STEP" });
|
||||
await waitForTaskHalCommand(store);
|
||||
state = store.getState();
|
||||
assert.equal(state.programRuntimeFeedback.sourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
|
||||
store.dispatch({ type: stopAction });
|
||||
await waitForTaskHalCommand(store);
|
||||
state = store.getState();
|
||||
assert.equal(state.taskHalStatusLoop.active, false);
|
||||
assert.equal(["stopped", "complete", "idle"].includes(state.runState), true);
|
||||
assert.equal(
|
||||
stopAction !== "ABORT" || ["aborted", "stopped", "complete", "idle"].includes(state.taskHalStatusLoop.stopReason),
|
||||
true,
|
||||
);
|
||||
assert.equal(state.taskHalStatus.ui.taskCycle >= cycleAfterRun, true);
|
||||
}
|
||||
|
||||
async function verifyRunReadySequence({ profileId, sourceRel }) {
|
||||
const sdk = await createLinuxCncTaskHalSdk({
|
||||
wasmBinary: readFileSync(wasmPath),
|
||||
print() {},
|
||||
printErr(message) {
|
||||
console.error(message);
|
||||
},
|
||||
});
|
||||
const profile = getFiveAxisProfile(profileId);
|
||||
const iniText = readFileSync(resolve(rootDir, "wasm-port/vendor/linuxcnc", profile.iniPath), "utf8");
|
||||
const iniConfig = parseLinuxCncIni(iniText, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
const store = createSimulationStore();
|
||||
|
||||
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
store.dispatch({
|
||||
type: "ATTACH_KINEMATICS_RUNTIME",
|
||||
runtime: await createLinuxCncKinematicsRuntime({ moduleId: iniConfig.kinematicsModuleId }),
|
||||
});
|
||||
store.dispatch({
|
||||
type: "ATTACH_INTERPRETER_RUNTIME",
|
||||
runtime: await createLinuxCncInterpreterRuntime(),
|
||||
});
|
||||
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime: wrapTaskHalSdk(sdk) });
|
||||
await store.stageMachineFiles();
|
||||
store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
|
||||
await waitForState(store, (state) => (
|
||||
state.taskHalSession?.programPath?.endsWith(sourceRel.split("/").at(-1)) &&
|
||||
state.rtcpFrame?.sourceMode === "source-derived-kinematics-wasm"
|
||||
));
|
||||
|
||||
store.dispatch({ type: "RUN_READY" });
|
||||
await waitForTaskHalCommand(store);
|
||||
await waitForState(store, (state) => state.machine.taskState === "on" && state.machine.mode === "auto");
|
||||
|
||||
let state = store.getState();
|
||||
assert.equal(state.machine.powerOn, true);
|
||||
assert.equal(state.machine.taskState, "on");
|
||||
assert.equal(state.machine.mode, "auto");
|
||||
assert.equal(state.machine.allHomed, true);
|
||||
assert.equal(state.rtcpState, "on");
|
||||
assert.equal(String(state.kinsType).startsWith("tcp-"), true);
|
||||
|
||||
store.dispatch({ type: "RUN" });
|
||||
await waitForTaskHalCommand(store);
|
||||
await waitForState(store, (nextState) => distinctActiveLines(nextState.programRuntimeFeedbackHistory).length >= 2);
|
||||
|
||||
state = store.getState();
|
||||
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
assert.equal(state.programRuntimeFeedback.sourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
assert.notEqual(state.operatorMessage, "run blocked: home machine first");
|
||||
assert.equal(state.machine.allHomed, true);
|
||||
|
||||
const activeLineBeforeStop = state.activeLine;
|
||||
store.dispatch({ type: "STOP" });
|
||||
await waitForTaskHalCommand(store);
|
||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||
state = store.getState();
|
||||
assert.equal(state.taskHalStatusLoop.active, false);
|
||||
assert.equal(state.runState, "stopped");
|
||||
assert.equal(state.machine.interpState, "idle");
|
||||
assert.equal(state.feed.currentVelocity, 0);
|
||||
assert.equal(state.activeLine, activeLineBeforeStop);
|
||||
}
|
||||
|
||||
async function waitForTaskHalCommand(store) {
|
||||
for (let attempt = 0; attempt < 80; attempt += 1) {
|
||||
if (!store.getState().taskHalExecutionPending) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
if (!store.getState().taskHalExecutionPending) return store.getState();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
throw new Error("task/HAL command did not settle");
|
||||
}
|
||||
|
||||
async function waitForState(store, predicate) {
|
||||
for (let attempt = 0; attempt < 160; attempt += 1) {
|
||||
const state = store.getState();
|
||||
if (predicate(state)) return state;
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
throw new Error("store state condition did not settle");
|
||||
}
|
||||
|
||||
function isMonotonic(values) {
|
||||
for (let index = 1; index < values.length; index += 1) {
|
||||
if (Number(values[index]) < Number(values[index - 1])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function distinctActiveLines(history = []) {
|
||||
return [...new Set(history.map((entry) => Number(entry.line)).reverse())]
|
||||
.filter((line) => Number.isFinite(line));
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import {
|
||||
applyIniConfigToProfile,
|
||||
parseLinuxCncIni,
|
||||
} from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import { stageProfileMachineFiles } from "../../app/src/runtime/linuxcnc-machine-file-staging.js";
|
||||
import { buildTaskHalSessionFromMachineFiles } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
|
||||
import {
|
||||
createSimulationStore,
|
||||
validateRunPreconditions,
|
||||
} from "../../app/src/state/store.js";
|
||||
import { gateLinuxCncTaskAction } from "../../app/src/state/linuxcnc-task-policy.js";
|
||||
|
||||
const cases = [
|
||||
{
|
||||
profileId: "xyzac-trt",
|
||||
machineName: "sim-xyzac-trt-kins (switchkins)",
|
||||
coordinates: "XYZAC",
|
||||
kinematicsName: "xyzac-trt-kins",
|
||||
kinematicsModuleId: "xyzac-trt",
|
||||
wasmFile: "linuxcnc_xyzac_trt_kinematics.wasm",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
},
|
||||
{
|
||||
profileId: "xyzbc-trt",
|
||||
machineName: "sim-xyzbc-trt-kins (switchkins)",
|
||||
coordinates: "XYZBC",
|
||||
kinematicsName: "xyzbc-trt-kins",
|
||||
kinematicsModuleId: "xyzbc-trt",
|
||||
wasmFile: "linuxcnc_xyzbc_trt_kinematics.wasm",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
|
||||
},
|
||||
];
|
||||
|
||||
for (const item of cases) {
|
||||
const baseProfile = getFiveAxisProfile(item.profileId);
|
||||
const iniText = await readLinuxCncFile(baseProfile.iniPath);
|
||||
const iniConfig = parseLinuxCncIni(iniText, {
|
||||
path: baseProfile.iniPath,
|
||||
profileId: item.profileId,
|
||||
});
|
||||
|
||||
assert.equal(iniConfig.profileId, item.profileId);
|
||||
assert.equal(iniConfig.machineName, item.machineName);
|
||||
assert.equal(iniConfig.traj.coordinates, item.coordinates);
|
||||
assert.equal(iniConfig.kinematics.name, item.kinematicsName);
|
||||
assert.equal(iniConfig.kinematicsModuleId, item.kinematicsModuleId);
|
||||
assert.equal(iniConfig.validation.ready, true);
|
||||
|
||||
const profile = applyIniConfigToProfile(baseProfile, iniConfig);
|
||||
assert.equal(profile.traj.coordinates, item.coordinates);
|
||||
assert.equal(profile.kinematicsModuleId, item.kinematicsModuleId);
|
||||
assert.equal(profile.axisLimits[item.coordinates[0]].max > 0, true);
|
||||
assert.equal(profile.jointConfig.length, item.coordinates.length);
|
||||
|
||||
const kinematicsRuntime = await createLinuxCncKinematicsRuntime({ moduleId: item.kinematicsModuleId });
|
||||
assert.equal(kinematicsRuntime.readiness().moduleId, item.kinematicsModuleId);
|
||||
assert.equal(kinematicsRuntime.wasmFile, item.wasmFile);
|
||||
assert.equal(kinematicsRuntime.frameForJoints([10, 20, 30, 5, 15]).moduleId, item.kinematicsModuleId);
|
||||
|
||||
const staged = await stageProfileMachineFiles(profile);
|
||||
const selectedPlan = staged.plan.selectedProgramSourceRel === item.sourceRel
|
||||
? staged.plan
|
||||
: {
|
||||
...staged.plan,
|
||||
wasmProgramPath: staged.save.files.find((file) => file.sourceRel === item.sourceRel)?.wasmPath,
|
||||
selectedProgramSourceRel: item.sourceRel,
|
||||
};
|
||||
const session = buildTaskHalSessionFromMachineFiles({
|
||||
profile,
|
||||
plan: selectedPlan,
|
||||
save: staged.save,
|
||||
selectedProgramRel: item.sourceRel,
|
||||
});
|
||||
assert.equal(session.profileId, item.profileId);
|
||||
assert.equal(session.iniPath.endsWith(`${item.profileId}.ini`), true);
|
||||
assert.equal(session.programSourceRel, item.sourceRel);
|
||||
assert.equal(session.programPath.endsWith(item.sourceRel.split("/").at(-1)), true);
|
||||
}
|
||||
|
||||
const initialStore = createSimulationStore();
|
||||
let check = validateRunPreconditions(initialStore.getState(), {
|
||||
requireTaskHalRuntime: false,
|
||||
requireTaskHalSession: false,
|
||||
});
|
||||
assert.equal(check.ok, false);
|
||||
assert.equal(check.operatorMessage, "run blocked: LinuxCNC INI not loaded");
|
||||
|
||||
const profile = getFiveAxisProfile("xyzac-trt");
|
||||
const iniText = await readLinuxCncFile(profile.iniPath);
|
||||
const iniConfig = parseLinuxCncIni(iniText, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
const store = createSimulationStore();
|
||||
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
store.dispatch({
|
||||
type: "ATTACH_KINEMATICS_RUNTIME",
|
||||
runtime: await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }),
|
||||
});
|
||||
await store.stageMachineFiles();
|
||||
store.dispatch({
|
||||
type: "LOAD_LINUXCNC_GCODE_SOURCE",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
});
|
||||
|
||||
check = validateRunPreconditions(store.getState(), {
|
||||
requireTaskHalRuntime: false,
|
||||
requireTaskHalSession: false,
|
||||
});
|
||||
assert.equal(check.ok, true);
|
||||
assert.equal(check.profileId, "xyzac-trt");
|
||||
assert.equal(check.coordinates, "XYZAC");
|
||||
assert.equal(check.kinematicsModuleId, "xyzac-trt");
|
||||
assert.equal(check.selectedGcodeSourceRel.endsWith("xyzac_switchkins_test_1.ngc"), true);
|
||||
|
||||
check = validateRunPreconditions(store.getState(), {
|
||||
requireTaskHalRuntime: true,
|
||||
requireTaskHalSession: false,
|
||||
});
|
||||
assert.equal(check.ok, false);
|
||||
assert.equal(check.operatorMessage, "run blocked: task/HAL runtime not ready");
|
||||
|
||||
let gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: machine must be on");
|
||||
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
|
||||
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "auto" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "mode blocked: home machine before AUTO");
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
assert.equal(store.getState().machine.mode, "manual");
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
|
||||
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
|
||||
assert.equal(gate.allowed, false);
|
||||
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
|
||||
|
||||
const unopenedStore = createSimulationStore();
|
||||
unopenedStore.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
unopenedStore.dispatch({
|
||||
type: "ATTACH_KINEMATICS_RUNTIME",
|
||||
runtime: await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }),
|
||||
});
|
||||
await unopenedStore.stageMachineFiles();
|
||||
check = validateRunPreconditions(unopenedStore.getState(), {
|
||||
requireTaskHalRuntime: false,
|
||||
requireTaskHalSession: false,
|
||||
});
|
||||
assert.equal(check.ok, false);
|
||||
assert.equal(check.operatorMessage, "run blocked: no machine-file G-code opened for task/HAL session");
|
||||
|
||||
console.log("run_preconditions_ini_profile_smoke=ok");
|
||||
console.log("run_preconditions_kinematics_smoke=ok");
|
||||
console.log("run_preconditions_machine_file_smoke=ok");
|
||||
|
||||
async function readLinuxCncFile(sourceRel) {
|
||||
return readFile(new URL(`../../../wasm-port/vendor/linuxcnc/${sourceRel}`, import.meta.url), "utf8");
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
import {
|
||||
applyToolCommandSequence,
|
||||
createToolDbReadiness,
|
||||
createToolDbSimulation,
|
||||
editToolEntry,
|
||||
parseLinuxCncToolTable,
|
||||
queryToolEntry,
|
||||
saveToolDbSimulation,
|
||||
} from "../../app/src/runtime/tool-db-simulation.js";
|
||||
import {
|
||||
createControlledUserMReadiness,
|
||||
createControlledUserMSimulation,
|
||||
runControlledUserM,
|
||||
runControlledUserMProgramScan,
|
||||
} from "../../app/src/runtime/controlled-user-m-simulation.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
|
||||
const profile = getFiveAxisProfile("xyzac-trt");
|
||||
const toolTableText = await readFile(
|
||||
new URL("../../../linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const parsed = parseLinuxCncToolTable(toolTableText, {
|
||||
sourceRel: profile.toolTablePath,
|
||||
});
|
||||
assert.equal(parsed.apiName, "web-rtcp-5axis-tool-table");
|
||||
assert.equal(parsed.toolCount, 10);
|
||||
assert.equal(parsed.entries[0].toolNumber, 1);
|
||||
assert.equal(parsed.entries[0].pocket, 1);
|
||||
assert.equal(parsed.entries[1].offset.z, 15);
|
||||
assert.equal(parsed.entries[1].diameter, 8);
|
||||
assert.equal(parsed.semanticBoundary, "linuxcnc_tool_table_text_parsed_for_web_simulation");
|
||||
|
||||
let toolDb = createToolDbSimulation({
|
||||
toolTable: parsed,
|
||||
profile,
|
||||
storageMode: "memory",
|
||||
});
|
||||
assert.equal(toolDb.processReady, true);
|
||||
assert.equal(toolDb.processScope, "web_simulation_only");
|
||||
assert.equal(toolDb.hostProcessReady, false);
|
||||
assert.equal(createToolDbReadiness(toolDb).toolDbProcessReady, true);
|
||||
assert.equal(createToolDbReadiness(toolDb).hostProcessExecution, false);
|
||||
|
||||
toolDb = applyToolCommandSequence(toolDb, [
|
||||
{ code: "T", toolNumber: 2 },
|
||||
{ code: "M6" },
|
||||
{ code: "G43", h: 2 },
|
||||
]);
|
||||
assert.equal(toolDb.toolInSpindle, 2);
|
||||
assert.equal(toolDb.toolFromPocket, 2);
|
||||
assert.equal(toolDb.activeToolOffset.toolNumber, 2);
|
||||
assert.equal(toolDb.activeToolOffset.offset.z, 15);
|
||||
assert.equal(toolDb.iocontrol.toolNumber, 2);
|
||||
assert.equal(toolDb.events.some((event) => event.code === "M6"), true);
|
||||
|
||||
toolDb = applyToolCommandSequence(toolDb, [{ code: "M61", q: 4 }]);
|
||||
assert.equal(toolDb.toolInSpindle, 4);
|
||||
assert.equal(toolDb.iocontrol.toolNumber, 4);
|
||||
|
||||
toolDb = editToolEntry(toolDb, {
|
||||
toolNumber: 4,
|
||||
pocket: 44,
|
||||
diameter: 12.5,
|
||||
offset: { z: 42.25 },
|
||||
comment: "edited by web simulation",
|
||||
});
|
||||
const editedTool = queryToolEntry(toolDb, { toolNumber: 4 });
|
||||
assert.equal(editedTool.pocket, 44);
|
||||
assert.equal(editedTool.diameter, 12.5);
|
||||
assert.equal(editedTool.offset.z, 42.25);
|
||||
|
||||
const storage = createMemorySessionStorage();
|
||||
const save = await saveToolDbSimulation(toolDb, {
|
||||
storage,
|
||||
storageMode: "memory",
|
||||
});
|
||||
assert.equal(save.semanticBoundary, "tool_table_saved_in_web_storage_boundary");
|
||||
assert.equal(save.text.includes("T4 P44 D+12.500000 Z+42.250000 ;edited by web simulation"), true);
|
||||
assert.equal(storage.files.get(save.path), save.text);
|
||||
|
||||
let userM = createControlledUserMSimulation();
|
||||
assert.equal(createControlledUserMReadiness(userM).externalUserMProcessReady, true);
|
||||
assert.equal(createControlledUserMReadiness(userM).hostProcessExecution, false);
|
||||
|
||||
let result = runControlledUserM(userM, "M428", { profile });
|
||||
userM = result.simulation;
|
||||
assert.equal(result.event.allowed, true);
|
||||
assert.equal(result.statePatch.kinsType, "tcp-xyzac");
|
||||
assert.equal(result.statePatch.rtcpState, "on");
|
||||
assert.equal(result.halPatch["motion.switchkins-type"], 1);
|
||||
|
||||
result = runControlledUserM(userM, "M429", { profile });
|
||||
userM = result.simulation;
|
||||
assert.equal(result.event.allowed, true);
|
||||
assert.equal(result.statePatch.kinsType, "identity");
|
||||
assert.equal(result.statePatch.rtcpState, "off");
|
||||
assert.equal(result.halPatch["motion.switchkins-type"], 0);
|
||||
|
||||
result = runControlledUserM(userM, "M430", { profile });
|
||||
userM = result.simulation;
|
||||
assert.equal(result.event.allowed, true);
|
||||
assert.equal(result.statePatch.kinsType, "userk");
|
||||
assert.equal(result.halPatch["motion.switchkins-type"], 2);
|
||||
|
||||
result = runControlledUserM(userM, "M128", { profile });
|
||||
userM = result.simulation;
|
||||
assert.equal(result.event.allowed, true);
|
||||
assert.equal(result.event.hostProcessExecution, false);
|
||||
|
||||
result = runControlledUserM(userM, "M199", { profile });
|
||||
userM = result.simulation;
|
||||
assert.equal(result.event.allowed, false);
|
||||
assert.equal(result.event.semanticBoundary, "arbitrary_external_user_m_blocked");
|
||||
assert.equal(result.event.hostProcessExecution, false);
|
||||
assert.equal(userM.blockedEvents.length, 1);
|
||||
|
||||
const scan = runControlledUserMProgramScan(
|
||||
userM,
|
||||
[
|
||||
"M428",
|
||||
"G0 X1",
|
||||
"M129",
|
||||
"M199",
|
||||
"M429",
|
||||
].join("\n"),
|
||||
{ profile, sourceRel: "operator-test.ngc" },
|
||||
);
|
||||
assert.equal(scan.events.length, 4);
|
||||
assert.equal(scan.events.filter((event) => event.allowed).length, 3);
|
||||
assert.equal(scan.events.filter((event) => !event.allowed).length, 1);
|
||||
|
||||
console.log("tool_db_user_m_simulation_smoke=ok");
|
||||
console.log("tool_db_process_ready_web_simulation_only=1");
|
||||
console.log("external_user_m_process_ready_web_simulation_only=1");
|
||||
console.log("arbitrary_user_m_blocked=1");
|
||||
console.log("host_process_execution=0");
|
||||
@@ -0,0 +1,150 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
import {
|
||||
applyToolCommandSequence,
|
||||
createMemoryToolDbStorage,
|
||||
createToolDbReadiness,
|
||||
createToolDbSimulation,
|
||||
editToolEntry,
|
||||
extractToolCommandSequenceFromProgram,
|
||||
listToolEntries,
|
||||
parseLinuxCncToolTable,
|
||||
queryToolEntry,
|
||||
saveToolDbSimulation,
|
||||
} from "../../app/src/runtime/tool-db-simulation.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
const profile = getFiveAxisProfile("xyzac-trt");
|
||||
const toolTableText = await readFile(
|
||||
new URL("../../../linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const parsed = parseLinuxCncToolTable(toolTableText, {
|
||||
sourceRel: profile.toolTablePath,
|
||||
});
|
||||
assert.equal(parsed.randomToolChanger, false);
|
||||
assert.equal(parsed.toolCount, 10);
|
||||
assert.equal(parsed.pockets[0].toolNumber, 0);
|
||||
assert.equal(parsed.entries[0].idx, 1);
|
||||
assert.equal(parsed.entries[0].toolNumber, 1);
|
||||
assert.equal(parsed.entries[0].pocket, 1);
|
||||
assert.equal(parsed.entries[1].idx, 2);
|
||||
assert.equal(parsed.entries[1].toolNumber, 2);
|
||||
assert.equal(parsed.entries[1].pocket, 2);
|
||||
assert.equal(parsed.entries[1].offset.z, 15);
|
||||
assert.equal(parsed.entries[1].diameter, 8);
|
||||
|
||||
let toolDb = createToolDbSimulation({
|
||||
toolTable: parsed,
|
||||
profile,
|
||||
});
|
||||
assert.equal(createToolDbReadiness(toolDb).toolDbProcessReady, true);
|
||||
assert.equal(createToolDbReadiness(toolDb).dbProgramReady, false);
|
||||
assert.equal(toolDb.hostProcessExecution, false);
|
||||
assert.equal(toolDb.toolTable.pockets[0].toolNumber, 0);
|
||||
|
||||
assert.equal(listToolEntries(toolDb).length, 10);
|
||||
assert.equal(queryToolEntry(toolDb, { idx: 2 }).toolNumber, 2);
|
||||
assert.equal(queryToolEntry(toolDb, { toolNumber: 2 }).idx, 2);
|
||||
assert.equal(queryToolEntry(toolDb, { pocket: 2 }).diameter, 8);
|
||||
|
||||
toolDb = applyToolCommandSequence(toolDb, [
|
||||
{ code: "T", toolNumber: 2 },
|
||||
{ code: "M6" },
|
||||
{ code: "G43", h: 2 },
|
||||
]);
|
||||
assert.equal(toolDb.toolInSpindle, 2);
|
||||
assert.equal(toolDb.toolFromPocket, 2);
|
||||
assert.equal(toolDb.currentPocket, 2);
|
||||
assert.equal(toolDb.toolTable.pockets[0].toolNumber, 2);
|
||||
assert.equal(toolDb.toolTable.pockets[0].offset.z, 15);
|
||||
assert.equal(queryToolEntry(toolDb, { pocket: 2 }).idx, 2);
|
||||
assert.equal(toolDb.emcioStatus.tool.pocketPrepped, -1);
|
||||
assert.equal(toolDb.iocontrol.toolNumber, 2);
|
||||
assert.equal(toolDb.iocontrol.toolFromPocket, 2);
|
||||
assert.equal(toolDb.activeToolOffset.toolNumber, 2);
|
||||
assert.equal(toolDb.activeToolOffset.offset.z, 15);
|
||||
assert.equal(toolDb.interpreterParameters[5400], 2);
|
||||
assert.equal(toolDb.interpreterParameters[5403], 15);
|
||||
assert.equal(toolDb.interpreterParameters[5410], 8);
|
||||
assert.equal(toolDb.events.some((event) => event.code === "READ_TOOL_INPUTS_PREPARED"), true);
|
||||
assert.equal(toolDb.events.some((event) => event.code === "READ_TOOL_INPUTS_CHANGED"), true);
|
||||
|
||||
const compactToolCommands = extractToolCommandSequenceFromProgram("T2M6G43H2");
|
||||
assert.deepEqual(compactToolCommands.map((command) => command.code), ["T", "M6", "G43"]);
|
||||
assert.equal(compactToolCommands[2].toolNumber, 2);
|
||||
|
||||
toolDb = editToolEntry(toolDb, {
|
||||
toolNumber: 4,
|
||||
pocket: 44,
|
||||
diameter: 12.5,
|
||||
length: 42.25,
|
||||
comment: "edited by web simulation",
|
||||
});
|
||||
const edited = queryToolEntry(toolDb, { toolNumber: 4 });
|
||||
assert.equal(edited.idx, 4);
|
||||
assert.equal(edited.pocket, 44);
|
||||
assert.equal(edited.offset.z, 42.25);
|
||||
assert.equal(edited.diameter, 12.5);
|
||||
|
||||
toolDb = editToolEntry(toolDb, {
|
||||
idx: 6,
|
||||
toolNumber: 66,
|
||||
pocket: 66,
|
||||
diameter: 6.6,
|
||||
toolLength: 16.6,
|
||||
});
|
||||
const renumbered = queryToolEntry(toolDb, { toolNumber: 66 });
|
||||
assert.equal(renumbered.idx, 6);
|
||||
assert.equal(renumbered.pocket, 66);
|
||||
assert.equal(renumbered.offset.z, 16.6);
|
||||
assert.equal(renumbered.diameter, 6.6);
|
||||
|
||||
const storage = createMemoryToolDbStorage();
|
||||
const save = await saveToolDbSimulation(toolDb, {
|
||||
storage,
|
||||
storageMode: "memory",
|
||||
});
|
||||
assert.equal(save.storageMode, "memory");
|
||||
assert.equal(storage.files.get(save.path), save.text);
|
||||
assert.equal(save.text.includes("T4 P44 D+12.500000 Z+42.250000 ;edited by web simulation"), true);
|
||||
assert.equal(save.text.includes("T66 P66 D+6.600000 Z+16.600000"), true);
|
||||
|
||||
const fallbackSave = await saveToolDbSimulation(toolDb);
|
||||
assert.equal(fallbackSave.storageMode, "memory-fallback");
|
||||
assert.equal(fallbackSave.storageCapability.opfsUnavailable, true);
|
||||
|
||||
const store = createSimulationStore();
|
||||
const machineStorage = createMemorySessionStorage();
|
||||
await store.stageMachineFiles({ storage: machineStorage });
|
||||
assert.equal(store.getState().toolDbReadiness.toolDbProcessReady, true);
|
||||
assert.equal(store.queryToolDb({ toolNumber: 2 }).offset.z, 15);
|
||||
assert.equal(store.queryToolDb({ all: true }).length, 10);
|
||||
|
||||
store.editToolDb({
|
||||
toolNumber: 3,
|
||||
pocket: 33,
|
||||
diameter: 9.75,
|
||||
toolLength: 21.5,
|
||||
});
|
||||
assert.equal(store.queryToolDb({ toolNumber: 3 }).pocket, 33);
|
||||
assert.equal(store.queryToolDb({ toolNumber: 3 }).offset.z, 21.5);
|
||||
|
||||
const storeSaveStorage = createMemoryToolDbStorage();
|
||||
const storeSave = await store.saveToolDb({
|
||||
storage: storeSaveStorage,
|
||||
storageMode: "memory",
|
||||
});
|
||||
assert.equal(storeSave.text.includes("T3 P33 D+9.750000 Z+21.500000"), true);
|
||||
assert.equal(storeSaveStorage.files.get(storeSave.path), storeSave.text);
|
||||
|
||||
console.log("tool_db_web_simulation_smoke=ok");
|
||||
console.log("staged_tool_tbl_loaded=1");
|
||||
console.log("tool_db_query_edit_save=1");
|
||||
console.log("tool_db_t_m6_g43_state=1");
|
||||
console.log("tool_db_memory_fallback_save=1");
|
||||
console.log("host_tool_db_process=0");
|
||||
@@ -0,0 +1,96 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-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";
|
||||
|
||||
const profile = getFiveAxisProfile();
|
||||
|
||||
assert.equal(profile.id, "xyzbc-trt");
|
||||
assert.equal(profile.machineName, "sim-xyzbc-trt-kins (switchkins)");
|
||||
assert.equal(profile.traj.coordinates, "XYZBC");
|
||||
assert.deepEqual(profile.coordinates, ["X", "Y", "Z", "B", "C"]);
|
||||
assert.equal(profile.kinematics, "xyzbc-trt-kins");
|
||||
assert.equal(profile.kinematicsModuleId, "xyzbc-trt");
|
||||
assert.equal(profile.machineFileStaging.defaultProgramFilename, "xyzbc_switchkins.ngc");
|
||||
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.ok(profile.remaps.some((remap) => remap.code === "M428" && remap.switchkinsType === 1));
|
||||
assert.ok(profile.remaps.some((remap) => remap.code === "M429" && remap.switchkinsType === 0));
|
||||
assert.ok(profile.remaps.some((remap) => remap.code === "M430" && remap.switchkinsType === 2));
|
||||
|
||||
const iniText = await readFile(
|
||||
new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const ini = parseLinuxCncIni(iniText, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
|
||||
assert.equal(ini.validation.ready, true);
|
||||
assert.equal(ini.machineName, "sim-xyzbc-trt-kins (switchkins)");
|
||||
assert.equal(ini.kinematics.name, "xyzbc-trt-kins");
|
||||
assert.equal(ini.kinematicsModuleId, "xyzbc-trt");
|
||||
assert.equal(ini.traj.coordinates, "XYZBC");
|
||||
assert.equal(ini.rs274ngc.halPinVars, true);
|
||||
assert.equal(ini.rs274ngc.parameterFile, "xyzbc.var");
|
||||
assert.equal(ini.emcio.toolTable, "xyzbc-trt.tbl");
|
||||
assert.equal(ini.jointConfig[3].axis, "B");
|
||||
assert.equal(ini.jointConfig[4].axis, "C");
|
||||
assert.deepEqual(ini.halui.mdiCommands, ["M429", "M428", "M430"]);
|
||||
|
||||
const storage = createMemorySessionStorage();
|
||||
const staged = await stageProfileMachineFiles(profile, { storage });
|
||||
|
||||
assert.equal(staged.plan.profileId, "xyzbc-trt");
|
||||
assert.equal(staged.plan.wasmDir, "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt");
|
||||
assert.equal(staged.save.status, "saved");
|
||||
assert.equal(staged.save.storageMode, "memory");
|
||||
assert.ok(staged.save.opfsRoot.endsWith("/xyzbc-trt"));
|
||||
assert.ok(staged.save.files.every((file) => file.opfsPath.startsWith("web-rtcp-5axis-xyzbc-trt-sim-plan/machines/xyzbc-trt/")));
|
||||
assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.ini") && file.kind === "ini"));
|
||||
assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.xml") && file.kind === "pyvcp"));
|
||||
assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.tbl") && file.kind === "toolTable"));
|
||||
assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc") && file.kind === "remap"));
|
||||
assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/429remap.ngc") && file.kind === "remap"));
|
||||
assert.ok(staged.save.files.some((file) => file.sourceRel.endsWith("remap_subs/430remap.ngc") && file.kind === "remap"));
|
||||
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);
|
||||
|
||||
const selectedPlan = selectMachineFileProgram(
|
||||
staged.plan,
|
||||
staged.save,
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
|
||||
);
|
||||
assert.equal(selectedPlan.selectedProgramFilename, "xyzbc_switchkins.ngc");
|
||||
assert.ok(selectedPlan.wasmProgramPath.endsWith("/demos/xyzbc_switchkins.ngc"));
|
||||
|
||||
const store = createSimulationStore();
|
||||
const state = store.getState();
|
||||
assert.equal(state.machineProfile, "xyzbc-trt");
|
||||
assert.equal(state.profile.id, "xyzbc-trt");
|
||||
assert.equal(state.sessionName, "xyzbc-trt-web-session");
|
||||
assert.equal(state.kinematicsRuntimeReadiness, null);
|
||||
|
||||
const storeStage = await store.stageMachineFiles({ storage: createMemorySessionStorage() });
|
||||
const stagedState = store.getState();
|
||||
assert.equal(storeStage.save.profileId, "xyzbc-trt");
|
||||
assert.equal(stagedState.machineFileStaging.profileId, "xyzbc-trt");
|
||||
assert.ok(stagedState.machineProject.projectRoot.startsWith("web-rtcp-5axis-xyzbc-trt-sim-plan/machines/xyzbc-trt"));
|
||||
assert.ok(stagedState.machineFileStaging.gcodeSources.some((source) => source.filename === "xyzbc_switchkins.ngc"));
|
||||
|
||||
const stagingPlan = await createMachineFileStagingPlan({ profile });
|
||||
assert.equal(stagingPlan.summary.remapFileCount >= 3, true);
|
||||
assert.equal(stagingPlan.files.some((file) => file.sourceRel.endsWith("xyzbc.var")), true);
|
||||
|
||||
console.log("xyzbc_trt_web_app_smoke=ok");
|
||||
Reference in New Issue
Block a user