feat: sync latest run execution updates
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { buildProgramExecutionTiming } from "../../app/src/runtime/execution-timing.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import {
|
||||
buildTaskHalProgramMotionPlan,
|
||||
wrapTaskHalSdk,
|
||||
} from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = resolve(__dirname, "../../..");
|
||||
const sourceDir = resolve(rootDir, "web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source");
|
||||
const iniPath = resolve(sourceDir, "xyzac-trt.ini");
|
||||
const gcodePath = resolve(sourceDir, "impeller-7bl-xyzac.ngc");
|
||||
const wasmPath = resolve(rootDir, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm");
|
||||
|
||||
const iniText = readFileSync(iniPath, "utf8");
|
||||
const gcodeText = readFileSync(gcodePath, "utf8");
|
||||
const profile = {
|
||||
...getFiveAxisProfile("xyzac-trt"),
|
||||
linuxCncIniConfig: parseLinuxCncIni(iniText, {
|
||||
path: "working_run/test_linuxcnc_source/xyzac-trt.ini",
|
||||
profileId: "xyzac-trt",
|
||||
}),
|
||||
};
|
||||
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
|
||||
const execution = interpreterRuntime.runProgram(gcodeText);
|
||||
const timing = buildProgramExecutionTiming({
|
||||
motion: execution.motion,
|
||||
profile,
|
||||
feedOverride: 100,
|
||||
rapidOverride: 100,
|
||||
defaultFeedRate: 100,
|
||||
});
|
||||
const feedSegments = timing.segments.filter((segment) => segment.motionClass === "feed");
|
||||
const f159 = feedSegments.find((segment) => segment.feedRate === 159);
|
||||
const f636 = feedSegments.find((segment) => segment.feedRate === 636);
|
||||
|
||||
assert.equal(execution.summary.ready, true);
|
||||
assert.equal(execution.motion.length > 1000, true);
|
||||
assert.equal(execution.motion.some((event) => event.feedMode === "inverse-time"), true);
|
||||
assert.equal(feedSegments.length > 1000, true);
|
||||
assert.equal(f159?.feedMode, "inverse-time");
|
||||
assert.equal(f636?.feedMode, "inverse-time");
|
||||
assertNear(f159.durationSeconds, 60 / 159, "F159 inverse-time duration");
|
||||
assertNear(f636.durationSeconds, 60 / 636, "F636 inverse-time duration");
|
||||
assert.equal(f636.durationSeconds < f159.durationSeconds, true);
|
||||
assert.equal(f159.velocityMmPerMin > 0, true);
|
||||
assert.equal(f636.velocityMmPerMin > 0, true);
|
||||
|
||||
const taskHal = wrapTaskHalSdk(await createLinuxCncTaskHalSdk({
|
||||
wasmBinary: readFileSync(wasmPath),
|
||||
print() {},
|
||||
printErr() {},
|
||||
}));
|
||||
const programPath = "/work/sim/xyzac-trt/impeller-7bl-xyzac.ngc";
|
||||
taskHal.initSession({
|
||||
profileId: "xyzac-trt",
|
||||
iniPath: "/work/sim/xyzac-trt/xyzac-trt.ini",
|
||||
iniText,
|
||||
programPath,
|
||||
});
|
||||
taskHal.stageFiles([{ path: programPath, text: gcodeText }]);
|
||||
taskHal.openProgram(programPath);
|
||||
taskHal.loadProgramMotionPlan(buildTaskHalProgramMotionPlan({
|
||||
programPath,
|
||||
motion: execution.motion,
|
||||
timing,
|
||||
linearUnits: timing.linearUnits,
|
||||
}));
|
||||
taskHal.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" });
|
||||
taskHal.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" });
|
||||
taskHal.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 });
|
||||
|
||||
const samples = [];
|
||||
for (let index = 0; index < 200; index += 1) {
|
||||
taskHal.runCycles({ taskPeriodNs: 100000000, servoPeriodNs: 1000000, taskCycles: 1 });
|
||||
const status = taskHal.readStatus();
|
||||
samples.push({
|
||||
index,
|
||||
activeLine: status.ui.activeLine,
|
||||
axisPose: status.ui.axisPose,
|
||||
currentVelocity: status.ui.currentVelocity,
|
||||
taskCycle: status.ui.taskCycle,
|
||||
});
|
||||
}
|
||||
|
||||
const movedSamples = samples.filter((sample) => sample.currentVelocity > 0);
|
||||
const distinctVelocities = [...new Set(movedSamples.map((sample) => Math.round(sample.currentVelocity * 1000) / 1000))];
|
||||
|
||||
assert.equal(movedSamples.length > 20, true);
|
||||
assert.equal(distinctVelocities.length > 3, true);
|
||||
assert.equal(distinctVelocities.includes(3600), false);
|
||||
assert.equal(samples.at(0).activeLine >= 7, true);
|
||||
assert.equal(samples.at(-1).activeLine > samples.at(0).activeLine, true);
|
||||
assert.equal(samples.some((sample) => sample.activeLine === f159.line), true);
|
||||
assert.equal(samples.some((sample) => sample.activeLine === f636.line), true);
|
||||
assert.equal(samples.some((sample) => Math.abs(sample.currentVelocity - f159.velocityMmPerMin) < 0.001), true);
|
||||
assert.equal(samples.some((sample) => Math.abs(sample.currentVelocity - f636.velocityMmPerMin) < 0.001), true);
|
||||
|
||||
console.log(`impeller_motion_count=${execution.motion.length}`);
|
||||
console.log(`impeller_feed_segments=${feedSegments.length}`);
|
||||
console.log(`impeller_f159_duration_seconds=${f159.durationSeconds}`);
|
||||
console.log(`impeller_f636_duration_seconds=${f636.durationSeconds}`);
|
||||
console.log(`impeller_task_hal_distinct_velocities=${distinctVelocities.slice(0, 10).join(",")}`);
|
||||
console.log("impeller_feed_task_hal_run=ok");
|
||||
|
||||
function assertNear(actual, expected, label, tolerance = 1e-9) {
|
||||
assert.equal(Math.abs(Number(actual) - Number(expected)) <= tolerance, true, `${label}: ${actual} != ${expected}`);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
linearUnitsToMetersFactor,
|
||||
linearUnitsToMillimetersFactor,
|
||||
linearValueToMeters,
|
||||
linearValueToMillimeters,
|
||||
normalizeLinearUnits,
|
||||
resolveStateLinearUnits,
|
||||
} from "../../app/src/runtime/linear-units.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { buildProgramExecutionTiming } from "../../app/src/runtime/execution-timing.js";
|
||||
import { axesToSceneMeters } from "../../app/src/visualization/five-axis-scene.js";
|
||||
|
||||
function assertNear(actual, expected, label) {
|
||||
assert.equal(Math.abs(actual - expected) < 1e-9, true, `${label}: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
|
||||
assert.equal(normalizeLinearUnits("MM"), "mm");
|
||||
assert.equal(normalizeLinearUnits("inch"), "inch");
|
||||
assert.equal(normalizeLinearUnits("meters"), "m");
|
||||
assertNear(linearUnitsToMetersFactor("mm"), 0.001, "mm to m factor");
|
||||
assertNear(linearUnitsToMetersFactor("inch"), 0.0254, "inch to m factor");
|
||||
assertNear(linearUnitsToMetersFactor("m"), 1, "m to m factor");
|
||||
assertNear(linearUnitsToMillimetersFactor("inch"), 25.4, "inch to mm factor");
|
||||
assertNear(linearValueToMeters(25.4, "mm"), 0.0254, "25.4 mm to meters");
|
||||
assertNear(linearValueToMeters(1, "inch"), 0.0254, "1 inch to meters");
|
||||
assertNear(linearValueToMillimeters(0.5, "m"), 500, "0.5 m to millimeters");
|
||||
|
||||
const mmState = { profile: { traj: { linearUnits: "mm" } } };
|
||||
const inchState = { profile: { traj: { linearUnits: "inch" } } };
|
||||
const meterState = { profile: { traj: { linearUnits: "m" } } };
|
||||
const iniState = {
|
||||
linuxCncIniConfig: { traj: { linearUnits: "inch" } },
|
||||
profile: { traj: { linearUnits: "mm" } },
|
||||
};
|
||||
|
||||
assert.equal(resolveStateLinearUnits(iniState), "inch");
|
||||
assert.deepEqual(axesToSceneMeters({ x: 25.4, y: -10, z: 2000 }, mmState), {
|
||||
x: 0.0254,
|
||||
y: -0.01,
|
||||
z: 2,
|
||||
});
|
||||
assert.deepEqual(axesToSceneMeters({ x: 1, y: -0.5, z: 2 }, inchState), {
|
||||
x: 0.0254,
|
||||
y: -0.0127,
|
||||
z: 0.0508,
|
||||
});
|
||||
assert.deepEqual(axesToSceneMeters({ x: 0.1, y: -0.2, z: 0.3 }, meterState), {
|
||||
x: 0.1,
|
||||
y: -0.2,
|
||||
z: 0.3,
|
||||
});
|
||||
|
||||
const inchTiming = buildProgramExecutionTiming({
|
||||
profile: {
|
||||
traj: {
|
||||
linearUnits: "inch",
|
||||
maxLinearVelocity: 2,
|
||||
defaultLinearVelocity: 1,
|
||||
},
|
||||
},
|
||||
defaultFeedRate: 1,
|
||||
motion: [
|
||||
{ type: "STRAIGHT_FEED", line: 1, axes: { x: 0, y: 0, z: 0 } },
|
||||
{ type: "STRAIGHT_FEED", line: 2, axes: { x: 1, y: 0, z: 0 } },
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(inchTiming.linearUnits, "inch");
|
||||
assertNear(inchTiming.segments[1].linearDistanceMm, 25.4, "inch segment distance in mm");
|
||||
assertNear(inchTiming.segments[1].velocityMmPerMin, 25.4, "inch feed velocity in mm/min");
|
||||
|
||||
const mixedUnitsTiming = buildProgramExecutionTiming({
|
||||
profile: {
|
||||
traj: {
|
||||
linearUnits: "mm",
|
||||
maxLinearVelocity: 100,
|
||||
defaultLinearVelocity: 10,
|
||||
},
|
||||
},
|
||||
motion: [
|
||||
{ type: "STRAIGHT_FEED", line: 1, axes: { x: 0, y: 0, z: 0 }, linearUnits: "inch", feedRate: 10 },
|
||||
{ type: "STRAIGHT_FEED", line: 2, axes: { x: 1, y: 0, z: 0 }, linearUnits: "inch", feedRate: 10 },
|
||||
{ type: "STRAIGHT_FEED", line: 3, axes: { x: 25.4, y: 0, z: 0 }, linearUnits: "mm", feedRate: 100 },
|
||||
],
|
||||
});
|
||||
assertNear(mixedUnitsTiming.segments[1].linearDistanceMm, 25.4, "mixed units inch segment distance");
|
||||
assertNear(mixedUnitsTiming.segments[2].linearDistanceMm, 0, "mixed units unchanged position distance");
|
||||
|
||||
const inverseTimeTiming = buildProgramExecutionTiming({
|
||||
profile: {
|
||||
traj: {
|
||||
linearUnits: "mm",
|
||||
maxLinearVelocity: 100,
|
||||
defaultLinearVelocity: 10,
|
||||
},
|
||||
},
|
||||
motion: [
|
||||
{ type: "STRAIGHT_FEED", line: 1, axes: { x: 0, y: 0, z: 0 }, linearUnits: "mm", feedMode: "inverse-time", feedRate: 120 },
|
||||
{ type: "STRAIGHT_FEED", line: 2, axes: { x: 10, y: 0, z: 0 }, linearUnits: "mm", feedMode: "inverse-time", feedRate: 120 },
|
||||
],
|
||||
});
|
||||
assertNear(inverseTimeTiming.segments[1].durationSeconds, 0.5, "G93 inverse-time F120 duration");
|
||||
assertNear(inverseTimeTiming.segments[1].velocityMmPerMin, 1200, "G93 inverse-time velocity");
|
||||
|
||||
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
|
||||
const inchExecution = interpreterRuntime.runProgram("G20 G90\nG1 X1 F10\nM2");
|
||||
const metricExecution = interpreterRuntime.runProgram("G21 G90\nG1 X25.4 F100\nM2");
|
||||
const inverseExecution = interpreterRuntime.runProgram("G21 G90 G93\nG1 X1 F120\nM2");
|
||||
|
||||
assert.equal(inchExecution.motion[0].linearUnits, "inch");
|
||||
assert.equal(metricExecution.motion[0].linearUnits, "mm");
|
||||
assert.equal(inverseExecution.motion[0].feedMode, "inverse-time");
|
||||
assertNear(
|
||||
axesToSceneMeters(inchExecution.motion[0].axes, mmState, inchExecution.motion[0].linearUnits).x,
|
||||
0.0254,
|
||||
"G20 interpreter motion uses inch scene conversion",
|
||||
);
|
||||
assertNear(
|
||||
axesToSceneMeters(metricExecution.motion[0].axes, mmState, metricExecution.motion[0].linearUnits).x,
|
||||
0.0254,
|
||||
"G21 interpreter motion uses millimeter scene conversion",
|
||||
);
|
||||
|
||||
console.log("linear_unit_conversion_smoke=ok");
|
||||
@@ -28,6 +28,16 @@ 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);
|
||||
@@ -50,8 +60,25 @@ 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");
|
||||
|
||||
@@ -71,8 +71,10 @@ assert.equal(state.fullExecutionBoundary.nativeTaskReady, true);
|
||||
assert.equal(state.fullExecutionBoundary.nativeHalSyncReady, true);
|
||||
|
||||
await store.initializeTaskHalSession({ openProgram: true });
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
await waitForTaskHal(store);
|
||||
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);
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
createSimulationStore,
|
||||
validateRunPreconditions,
|
||||
} from "../../app/src/state/store.js";
|
||||
import { gateLinuxCncTaskAction } from "../../app/src/state/linuxcnc-task-policy.js";
|
||||
|
||||
const cases = [
|
||||
{
|
||||
@@ -124,6 +125,35 @@ check = validateRunPreconditions(store.getState(), {
|
||||
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: home machine 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");
|
||||
|
||||
Reference in New Issue
Block a user