提交当前项目改动

This commit is contained in:
mes123456
2026-07-02 08:01:34 -04:00
parent 83075c4b96
commit c4a721f6cf
274 changed files with 157294 additions and 1394 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
CHROMIUM="${CHROMIUM:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}"
if [[ -z "$CHROMIUM" ]]; then
echo "missing Chromium-compatible browser; set CHROMIUM=/path/to/browser" >&2
exit 1
fi
npm --prefix "$ROOT_DIR/web-rtcp-5axis-sim-plan/app" run build >/dev/null
TMP_DIR="$(mktemp -d)"
PORT_FILE="$TMP_DIR/port"
SERVER_LOG="$TMP_DIR/server.log"
CHROME_PROFILE="$TMP_DIR/chrome-profile"
mkdir -p "$CHROME_PROFILE"
cleanup() {
if [[ -n "${SERVER_PID:-}" ]]; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
python3 - <<'PY' "$ROOT_DIR" "$PORT_FILE" >"$SERVER_LOG" 2>&1 &
import functools
import http.server
import pathlib
import socketserver
import sys
root = pathlib.Path(sys.argv[1])
port_file = pathlib.Path(sys.argv[2])
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root))
with socketserver.TCPServer(("127.0.0.1", 0), handler) as httpd:
port_file.write_text(str(httpd.server_address[1]), encoding="ascii")
httpd.serve_forever()
PY
SERVER_PID=$!
for _ in $(seq 1 100); do
[[ -s "$PORT_FILE" ]] && break
sleep 0.05
done
if [[ ! -s "$PORT_FILE" ]]; then
echo "gmoccapy dist browser smoke HTTP server did not start" >&2
cat "$SERVER_LOG" >&2 || true
exit 1
fi
PORT="$(cat "$PORT_FILE")"
APP_PATH="../../app/dist/index.html"
URL="http://127.0.0.1:$PORT/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html?app=$APP_PATH"
OUT="$TMP_DIR/chromium-gmoccapy-dist.out"
"$CHROMIUM" \
--headless=new \
--disable-gpu \
--no-sandbox \
--user-data-dir="$CHROME_PROFILE" \
--virtual-time-budget=10000 \
--dump-dom \
"$URL" >"$OUT" 2>&1
if ! grep -Fq "gmoccapy_shell_smoke=ok" "$OUT"; then
echo "gmoccapy dist browser smoke failed" >&2
sed -n '1,260p' "$OUT" >&2
exit 1
fi
echo "gmoccapy_dist_smoke=ok"

View File

@@ -0,0 +1,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"

View File

@@ -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");

View File

@@ -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");

View File

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

View File

@@ -0,0 +1,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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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}`);
}

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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}`);
}

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -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");

View File

@@ -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,
},
};
}

View File

@@ -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));
}

View File

@@ -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");
}

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");