Validate AXIS task state flow
This commit is contained in:
@@ -833,8 +833,8 @@ async function pathFromTaskHalExecution({ profile, staged, selectedPlan, executi
|
||||
taskCycles: 1,
|
||||
});
|
||||
const status = taskHal.readStatus();
|
||||
const motion = status.motionStatus?.motion || {};
|
||||
const task = status.task || {};
|
||||
const motion = status.emcStatus?.motion || {};
|
||||
const task = status.emcStatus?.task || {};
|
||||
const activeLine = status.ui?.activeLine ?? motion.programLine ?? 0;
|
||||
const sample = normalizePathSample({
|
||||
sampleIndex: samples.length,
|
||||
@@ -1519,7 +1519,10 @@ function machineStateForMotion({
|
||||
}
|
||||
|
||||
function xyzbcAxesFromTaskHalStatus(status = {}) {
|
||||
const axis = status.motionStatus?.axis || {};
|
||||
const axis = status.ui?.axisPose ||
|
||||
status.emcStatus?.motion?.axisByName ||
|
||||
status.emcStatus?.motion?.traj?.actualPosition ||
|
||||
{};
|
||||
const pins = status.halSnapshot?.pins || {};
|
||||
return {
|
||||
x: firstFiniteNumber(
|
||||
@@ -1563,7 +1566,7 @@ function xyzbcAxesFromTaskHalStatus(status = {}) {
|
||||
function spindleFromTaskHalStatus(status = {}) {
|
||||
const pins = status.halSnapshot?.pins || {};
|
||||
return firstFiniteNumber(
|
||||
status.motionStatus?.motion?.spindleSpeed,
|
||||
status.emcStatus?.motion?.spindleSpeed,
|
||||
pins["spindle.0.speed-out"]?.value,
|
||||
pins["motion.spindle-speed-out"]?.value,
|
||||
0,
|
||||
|
||||
@@ -11,7 +11,8 @@ const linuxCncSourceRoot = resolve(repoRoot, "linuxcnc");
|
||||
const appUrlPath = "/web-rtcp-5axis-xyzbc-trt-sim-plan/app/index.html";
|
||||
const sourceRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc";
|
||||
const samplePeriodMs = 50;
|
||||
const runStableBeforeFirstPauseMs = Math.max(Number(process.env.RUN_STABLE_BEFORE_FIRST_PAUSE_MS || 0), 0);
|
||||
const runSegmentMs = 5000;
|
||||
const pauseSegmentMs = 5000;
|
||||
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
||||
const outputDir = resolve(projectRoot, "working/screenshots", `estop-power-home-run-pause-50ms-${timestamp}`);
|
||||
const chromiumExecutable = process.env.CHROMIUM || findSystemChromium();
|
||||
@@ -68,7 +69,7 @@ try {
|
||||
await clickAndWait(page, '[data-action="estop"]', "解除 ESTOP", (state) => (
|
||||
state.machine?.estopActive === false &&
|
||||
state.machine?.taskState === "estop-reset" &&
|
||||
state.runState === "idle"
|
||||
state.machine?.powerOn === false
|
||||
));
|
||||
await clickAndWait(page, '[data-action="power"]', "上电", (state) => (
|
||||
state.machine?.powerOn === true &&
|
||||
@@ -78,70 +79,72 @@ try {
|
||||
state.machine?.allHomed === true &&
|
||||
state.machine?.mode === "manual"
|
||||
));
|
||||
await clickAndWait(page, '[data-action="run"]', "Run", (state) => (
|
||||
await clickAndWait(page, '[data-action="toggle-auto-manual"]', "自动模式", (state) => (
|
||||
state.machine?.mode === "auto" &&
|
||||
state.machine?.interpState === "idle"
|
||||
));
|
||||
await domClickAndWait(page, '[data-tool-id="btn_run"]', "Run", (state) => (
|
||||
state.machine?.mode === "auto" &&
|
||||
(state.runState === "running" || state.runState === "complete") &&
|
||||
state.machine?.interpState === "reading" &&
|
||||
state.programRuntimeFeedback
|
||||
), 60000);
|
||||
await holdState(page, "Run 后执行 5 秒", runSegmentMs, (state) => (
|
||||
state.runState === "running" &&
|
||||
state.machine?.interpState === "reading" &&
|
||||
state.machine?.taskPaused === false
|
||||
), { requirePositionChange: true });
|
||||
|
||||
if (runStableBeforeFirstPauseMs > 0) {
|
||||
await holdState(page, `Run 后稳定运行 ${runStableBeforeFirstPauseMs}ms`, runStableBeforeFirstPauseMs, (state) => (
|
||||
state.runState === "running" &&
|
||||
state.machine?.interpState === "reading" &&
|
||||
state.machine?.taskPaused === false
|
||||
), { requirePositionChange: true });
|
||||
}
|
||||
|
||||
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次暂停", (state) => (
|
||||
await domClickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次暂停", (state) => (
|
||||
state.runState === "paused" &&
|
||||
state.machine?.interpState === "paused" &&
|
||||
state.machine?.taskPaused === true
|
||||
));
|
||||
await holdState(page, "第一次暂停保持 5 秒", 5000, (state) => (
|
||||
await holdState(page, "第一次暂停保持 5 秒", pauseSegmentMs, (state) => (
|
||||
state.runState === "paused" &&
|
||||
state.machine?.interpState === "paused" &&
|
||||
state.machine?.taskPaused === true
|
||||
), { freezePosition: true });
|
||||
|
||||
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次继续执行", (state) => (
|
||||
await domClickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次继续执行", (state) => (
|
||||
state.runState === "running" &&
|
||||
state.machine?.interpState === "reading" &&
|
||||
state.machine?.taskPaused === false
|
||||
));
|
||||
await holdState(page, "第一次继续执行 10 秒", 10000, (state) => (
|
||||
await holdState(page, "第一次继续执行 5 秒", runSegmentMs, (state) => (
|
||||
state.runState === "running" &&
|
||||
state.machine?.interpState === "reading" &&
|
||||
state.machine?.taskPaused === false
|
||||
));
|
||||
), { requirePositionChange: true });
|
||||
|
||||
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次暂停", (state) => (
|
||||
await domClickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次暂停", (state) => (
|
||||
state.runState === "paused" &&
|
||||
state.machine?.interpState === "paused" &&
|
||||
state.machine?.taskPaused === true
|
||||
));
|
||||
await holdState(page, "第二次暂停保持 5 秒", 5000, (state) => (
|
||||
await holdState(page, "第二次暂停保持 5 秒", pauseSegmentMs, (state) => (
|
||||
state.runState === "paused" &&
|
||||
state.machine?.interpState === "paused" &&
|
||||
state.machine?.taskPaused === true
|
||||
), { freezePosition: true });
|
||||
|
||||
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次继续执行", (state) => (
|
||||
await domClickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次继续执行", (state) => (
|
||||
state.runState === "running" &&
|
||||
state.machine?.interpState === "reading" &&
|
||||
state.machine?.taskPaused === false
|
||||
));
|
||||
await holdState(page, "第二次继续执行 10 秒", 10000, (state) => (
|
||||
await holdState(page, "第二次继续执行 5 秒", runSegmentMs, (state) => (
|
||||
state.runState === "running" &&
|
||||
state.machine?.interpState === "reading" &&
|
||||
state.machine?.taskPaused === false
|
||||
));
|
||||
), { requirePositionChange: true });
|
||||
|
||||
clearInterval(capture.timer);
|
||||
await capture.inFlight;
|
||||
capture = null;
|
||||
|
||||
const finalState = await readStateSummary(page);
|
||||
const gcodeDataChecks = assertGcodeExecutionDataReasonable(frames);
|
||||
const manifest = {
|
||||
apiName: "xyzbc-trt-estop-power-home-run-pause-50ms-verification",
|
||||
status: "passed",
|
||||
@@ -160,18 +163,21 @@ try {
|
||||
outputDir,
|
||||
captureMethod: "Playwright clicked AXIS DOM controls for ESTOP reset, power, Home All, Run, pause/resume, while a 50ms interval full-page screenshot loop recorded the visible web simulator.",
|
||||
samplePeriodMs,
|
||||
runStableBeforeFirstPauseMs,
|
||||
runSegmentMs,
|
||||
pauseSegmentMs,
|
||||
expectedSequence: [
|
||||
"解除 ESTOP",
|
||||
"上电",
|
||||
"Home All",
|
||||
"自动模式",
|
||||
"Run",
|
||||
...(runStableBeforeFirstPauseMs > 0 ? [`Run 后稳定运行 ${runStableBeforeFirstPauseMs}ms`] : []),
|
||||
"Run 后执行 5 秒",
|
||||
"第一次暂停并保持 5 秒",
|
||||
"第一次继续执行 10 秒",
|
||||
"第一次继续执行 5 秒",
|
||||
"第二次暂停并保持 5 秒",
|
||||
"第二次继续执行 10 秒",
|
||||
"第二次继续执行 5 秒",
|
||||
],
|
||||
gcodeDataChecks,
|
||||
events,
|
||||
assertions,
|
||||
capturedFrameCount: frames.length,
|
||||
@@ -226,12 +232,13 @@ function startFrameCapture(page) {
|
||||
const startedAt = Date.now();
|
||||
let index = 0;
|
||||
let inFlight = Promise.resolve();
|
||||
let capturing = false;
|
||||
const timer = setInterval(() => {
|
||||
if (capturing) return;
|
||||
capturing = true;
|
||||
const frameIndex = index;
|
||||
index += 1;
|
||||
inFlight = inFlight
|
||||
.catch(() => {})
|
||||
.then(async () => {
|
||||
inFlight = (async () => {
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const summary = await readStateSummary(page);
|
||||
const frameName = `frame-${String(frameIndex).padStart(5, "0")}-t${String(elapsedMs).padStart(6, "0")}ms-${slug(summary.runState)}.png`;
|
||||
@@ -253,6 +260,12 @@ function startFrameCapture(page) {
|
||||
statement: summary.programUiExecution?.statement || "",
|
||||
currentVelocity: summary.currentVelocity,
|
||||
});
|
||||
})()
|
||||
.catch((error) => {
|
||||
captureError = error;
|
||||
})
|
||||
.finally(() => {
|
||||
capturing = false;
|
||||
});
|
||||
}, samplePeriodMs);
|
||||
return { timer, get inFlight() { return inFlight; } };
|
||||
@@ -281,6 +294,34 @@ async function clickAndWait(page, selector, label, predicate, timeoutMs = 45000)
|
||||
return after;
|
||||
}
|
||||
|
||||
async function domClickAndWait(page, selector, label, predicate, timeoutMs = 45000) {
|
||||
const before = await readStateSummary(page);
|
||||
const startedAt = Date.now();
|
||||
await page.evaluate((targetSelector) => {
|
||||
const target = document.querySelector(targetSelector);
|
||||
if (!target) throw new Error(`missing click target ${targetSelector}`);
|
||||
target.click();
|
||||
}, selector);
|
||||
const after = await waitForState(page, predicate, label, timeoutMs);
|
||||
const event = {
|
||||
label,
|
||||
selector,
|
||||
clickMethod: "dom-element-click",
|
||||
at: new Date().toISOString(),
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
before,
|
||||
after: summarizeForEvent(after),
|
||||
};
|
||||
events.push(event);
|
||||
assertions.push({
|
||||
label,
|
||||
passed: true,
|
||||
checkedAt: new Date().toISOString(),
|
||||
state: summarizeForEvent(after),
|
||||
});
|
||||
return after;
|
||||
}
|
||||
|
||||
async function holdState(page, label, durationMs, predicate, options = {}) {
|
||||
const startedAt = Date.now();
|
||||
let last = null;
|
||||
@@ -468,6 +509,79 @@ function hasPositionChanged(before, after) {
|
||||
));
|
||||
}
|
||||
|
||||
function assertGcodeExecutionDataReasonable(capturedFrames) {
|
||||
const runningFrames = capturedFrames.filter((frame) => frame.runState === "running");
|
||||
const pausedFrames = capturedFrames.filter((frame) => frame.runState === "paused");
|
||||
if (runningFrames.length < 5) {
|
||||
throw new Error(`G-code execution check failed: expected running frames, got ${runningFrames.length}`);
|
||||
}
|
||||
if (pausedFrames.length < 5) {
|
||||
throw new Error(`G-code execution check failed: expected paused frames, got ${pausedFrames.length}`);
|
||||
}
|
||||
|
||||
const framesWithSource = runningFrames.filter((frame) => (
|
||||
frame.sourceFile || frame.statement || Number(frame.line || 0) > 0
|
||||
));
|
||||
if (framesWithSource.length === 0) {
|
||||
throw new Error("G-code execution check failed: no running frame exposed source line data");
|
||||
}
|
||||
|
||||
const sampleIndexes = runningFrames
|
||||
.map((frame) => Number(frame.sampleIndex))
|
||||
.filter(Number.isFinite);
|
||||
const activeLines = runningFrames
|
||||
.map((frame) => Number(frame.activeLine))
|
||||
.filter(Number.isFinite);
|
||||
const velocities = capturedFrames
|
||||
.map((frame) => Number(frame.currentVelocity))
|
||||
.filter(Number.isFinite);
|
||||
const runningVelocities = runningFrames
|
||||
.map((frame) => Number(frame.currentVelocity))
|
||||
.filter(Number.isFinite);
|
||||
|
||||
if (sampleIndexes.length === 0 || Math.max(...sampleIndexes) <= Math.min(...sampleIndexes)) {
|
||||
throw new Error("G-code execution check failed: sample index did not advance during running segments");
|
||||
}
|
||||
if (activeLines.length === 0 || activeLines.some((line) => line < 1)) {
|
||||
throw new Error("G-code execution check failed: active G-code line values are invalid");
|
||||
}
|
||||
if (velocities.some((velocity) => velocity < -1e-6)) {
|
||||
throw new Error("G-code execution check failed: negative velocity observed");
|
||||
}
|
||||
if (!runningVelocities.some((velocity) => velocity > 0)) {
|
||||
throw new Error("G-code execution check failed: no positive velocity observed while running");
|
||||
}
|
||||
|
||||
return {
|
||||
apiName: "xyzbc-trt-gcode-execution-data-checks",
|
||||
runningFrameCount: runningFrames.length,
|
||||
pausedFrameCount: pausedFrames.length,
|
||||
framesWithSourceCount: framesWithSource.length,
|
||||
firstRunningFrame: summarizeFrameForDataCheck(runningFrames[0]),
|
||||
lastRunningFrame: summarizeFrameForDataCheck(runningFrames[runningFrames.length - 1]),
|
||||
minSampleIndex: Math.min(...sampleIndexes),
|
||||
maxSampleIndex: Math.max(...sampleIndexes),
|
||||
minActiveLine: Math.min(...activeLines),
|
||||
maxActiveLine: Math.max(...activeLines),
|
||||
maxRunningVelocity: Math.max(...runningVelocities),
|
||||
sourceRel,
|
||||
passed: true,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeFrameForDataCheck(frame = {}) {
|
||||
return {
|
||||
elapsedMs: frame.elapsedMs,
|
||||
runState: frame.runState,
|
||||
activeLine: frame.activeLine,
|
||||
sampleIndex: frame.sampleIndex,
|
||||
line: frame.line,
|
||||
statement: frame.statement,
|
||||
currentVelocity: frame.currentVelocity,
|
||||
sourceFile: frame.sourceFile,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeForEvent(state) {
|
||||
return {
|
||||
runState: state?.runState,
|
||||
|
||||
Reference in New Issue
Block a user