Validate AXIS task state flow

This commit is contained in:
wangdequan
2026-07-09 18:12:01 -04:00
parent f99ba2bbe8
commit f4b9911d45
57 changed files with 195248 additions and 647 deletions

View File

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

View File

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