feat: sync latest run execution updates
This commit is contained in:
171
web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
Normal file
171
web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
Normal file
@@ -0,0 +1,171 @@
|
||||
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 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");
|
||||
|
||||
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 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));
|
||||
}
|
||||
Reference in New Issue
Block a user