fix: make Pause button pause running program

This commit is contained in:
wangdequan
2026-07-05 21:55:17 -04:00
parent 220c15295c
commit 58854f9ecf
12 changed files with 1774 additions and 126 deletions

View File

@@ -1007,6 +1007,19 @@
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
throw new Error("RESUME action did not restore running state");
}
doc.querySelector('[data-action="PAUSE"]').click();
await wait(50);
if (
win.webRtcp5AxisSimulation.getState().runState !== "paused" ||
win.webRtcp5AxisSimulation.getState().machine.interpState !== "paused"
) {
throw new Error("PAUSE action did not pause running task on direct second use");
}
doc.querySelector('[data-action="RESUME"]').click();
await wait(50);
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
throw new Error("RESUME action did not restore running state after direct second pause");
}
const tcpButton = doc.querySelector('[data-action="kins-tcp"]');
tcpButton.click();
await wait(50);

View File

@@ -143,11 +143,24 @@
if (!Array.isArray(buttonParity) || buttonParity.length < 50) {
throw new Error(`AXIS button parity list too small: ${buttonParity?.length}`);
}
for (const action of ["estop", "power", "run-ready", "run", "pause", "resume", "step", "stop", "home-all", "jog-plus", "jog-minus", "kins-identity", "kins-tcp", "kins-userk", "clear-preview"]) {
for (const action of ["estop", "power", "run-ready", "run", "pause", "resume", "pause-resume", "step", "stop", "home-all", "jog-plus", "jog-minus", "spindle-reverse", "spindle-stop", "spindle-forward", "kins-identity", "kins-tcp", "kins-userk", "clear-preview"]) {
if (!buttonParity.some((item) => item.action === action && item.sourceSymbol && item.sourceLines && item.expected)) {
throw new Error(`missing source-referenced button parity for ${action}`);
}
}
const domControls = [...doc.querySelectorAll("[data-action], [data-menu-command]")];
for (const action of [...new Set(buttonParity.map((item) => item.action))]) {
const controls = domControls.filter((control) => (
control.dataset.action === action ||
control.dataset.menuCommand === action
));
if (controls.length === 0) {
throw new Error(`button parity action has no DOM control: ${action}`);
}
if (!controls.some((control) => control.dataset.axisSourceRef && control.dataset.axisExpectedEffect)) {
throw new Error(`button parity action has no source-tagged DOM control: ${action}`);
}
}
for (const selector of [
'[data-action="estop"]',
'[data-action="power"]',
@@ -156,6 +169,9 @@
'[data-action="step"]',
'[data-action="stop"]',
'[data-action="jog-plus"]',
'[data-action="spindle-reverse"]',
'[data-action="spindle-stop"]',
'[data-action="spindle-forward"]',
'[data-action="toggle-flood"]',
'[data-action="kins-tcp"]',
'[data-action="clear-preview"]',
@@ -273,17 +289,90 @@
await click('[data-action="power"]', "AXIS power");
await waitState((state) => state.machine.powerOn === true && state.machine.taskState === "on", "machine power on");
await click('[data-action="home-all"]', "AXIS home all");
await waitState((state) => state.machine.allHomed === true && state.machine.mode === "manual", "home all");
await waitState((state) => (
state.machine.allHomed === true
&& state.machine.mode === "manual"
&& Math.abs(Number(state.axisPose.x) - 0) < 1e-6
&& Math.abs(Number(state.axisPose.y) - 0) < 1e-6
&& Math.abs(Number(state.axisPose.z) - 10) < 1e-6
&& Math.abs(Number(state.dro.x) - 0) < 1e-6
&& Math.abs(Number(state.dro.y) - 0) < 1e-6
&& Math.abs(Number(state.dro.z) - 10) < 1e-6
), "home all");
await click('[data-action="toggle-auto-manual"]', "AXIS AUTO mode toolbar toggle");
await waitState((state) => state.machine.mode === "auto", "toolbar toggle to AUTO");
await click('[data-action="toggle-auto-manual"]', "AXIS MANUAL mode toolbar toggle");
await waitState((state) => state.machine.mode === "manual", "toolbar toggle to MANUAL");
await click('[data-action="run"]', "AXIS immediate-pause run");
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.action === "pause-resume", "toolbar pause button is LinuxCNC task_pauseresume");
await click('[data-tool-id="tbtn_pause"]', "AXIS immediate pause/resume button after run click");
await waitState((state) => (
state.runState === "paused"
&& state.machine.interpState === "paused"
&& state.machine.taskPaused === true
), "immediate pause after run click");
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.paused === "true", "toolbar pause button visibly enters paused state");
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.title === "Resume program", "toolbar pause button title switches to resume");
await new Promise((resolve) => setTimeout(resolve, 500));
assert(api.getState().runState === "paused", "immediate pause remains paused after async task/HAL settle");
await click('[data-action="stop"]', "AXIS stop after immediate pause");
await waitState((state) => state.runState === "stopped" && state.machine.mode === "auto", "stop after immediate pause");
await click('[data-action="tab-manual"]', "AXIS manual tab after immediate pause");
await waitState((state) => state.machine.mode === "manual", "manual mode after immediate pause");
await click('[data-action="run"]', "AXIS direct run after home");
await waitState((state) => (
(state.runState === "running" || state.runState === "complete")
&& state.machine.mode === "auto"
&& state.programRuntimeFeedback
), "direct run after power and home");
await click('[data-menu-command="pause"]', "AXIS menu Pause after direct run");
await waitState((state) => (
state.runState === "paused"
&& state.machine.interpState === "paused"
&& state.machine.taskPaused === true
), "menu Pause after direct run");
await click('[data-menu-command="resume"]', "AXIS menu Resume after direct Pause");
await waitState((state) => state.runState === "running" && state.machine.interpState === "reading", "menu Resume after direct Pause");
await click('[data-tool-id="tbtn_pause"]', "AXIS toolbar pause after direct run");
await waitState((state) => (
state.runState === "paused"
&& state.machine.interpState === "paused"
&& state.machine.taskPaused === true
), "pause after direct run");
const pauseToolbarAfterPause = doc.querySelector('[data-tool-id="tbtn_pause"]');
assert(pauseToolbarAfterPause?.dataset.action === "pause-resume", "toolbar pause button stays bound to LinuxCNC task_pauseresume while paused");
assert(pauseToolbarAfterPause?.dataset.paused === "true", "toolbar pause button has paused dataset while paused");
assert(pauseToolbarAfterPause?.getAttribute("aria-pressed") === "true", "toolbar pause button aria pressed while paused");
await click('[data-tool-id="tbtn_pause"]', "AXIS toolbar pause/resume resumes while already paused");
await waitState((state) => state.runState === "running" && state.machine.interpState === "reading", "toolbar pause/resume resumes while paused");
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.paused === "false", "toolbar pause button leaves paused state after resume");
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.title === "Pause program", "toolbar pause button title switches to pause after resume");
await click('[data-tool-id="tbtn_pause"]', "AXIS toolbar pause/resume pauses again");
await waitState((state) => (
state.runState === "paused"
&& state.machine.interpState === "paused"
&& state.machine.taskPaused === true
), "toolbar pause/resume pauses again");
const beforeStepSample = api.getState().programExecutionSampleIndex;
await click('[data-action="step"]', "AXIS step after pause");
await waitState((state) => (
(state.runState === "stepping" || state.runState === "paused")
&& state.machine.interpState === "paused"
&& state.machine.taskPaused === true
&& Number(state.programExecutionSampleIndex) > Number(beforeStepSample)
), "step after pause");
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.action === "pause-resume", "toolbar pause button stays task_pauseresume after step");
await click('[data-tool-id="tbtn_pause"]', "AXIS toolbar pause/resume after pause");
await waitState((state) => state.runState === "running" && state.machine.interpState === "reading", "resume after pause");
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.action === "pause-resume", "toolbar pause button stays task_pauseresume after resume");
await click('[data-tool-id="tbtn_pause"]', "AXIS second toolbar pause after resume");
await waitState((state) => (
state.runState === "paused"
&& state.machine.interpState === "paused"
&& state.machine.taskPaused === true
), "second pause after resume");
await click('[data-tool-id="tbtn_pause"]', "AXIS second toolbar resume after pause");
await waitState((state) => state.runState === "running" && state.machine.interpState === "reading", "second resume after pause");
await click('[data-action="stop"]', "AXIS stop after direct run");
await waitState((state) => state.runState === "stopped" && state.machine.mode === "auto", "stop after direct run");
await click('[data-action="tab-manual"]', "AXIS manual tab after direct run");
@@ -329,10 +418,38 @@
const beforeFeed = api.getState().feed.feedOverride;
await click('[data-action="feed-override-up"]', "feed override up");
await waitState((state) => state.feed.feedOverride === Math.min(beforeFeed + 10, 200), "feed override");
await click('[data-action="spindle-forward"]', "spindle forward");
await waitState((state) => state.spindle.direction === "forward" && state.spindle.enabled === true, "spindle forward");
await click('[data-action="spindle-reverse"]', "spindle reverse");
await waitState((state) => (
state.spindle.direction === "reverse"
&& state.spindle.enabled === true
&& state.linuxCncProcessMonitor.spindle.halPins.on === 1
&& state.linuxCncProcessMonitor.spindle.halPins.forward === 0
&& state.linuxCncProcessMonitor.spindle.halPins.reverse === 1
&& state.linuxCncProcessMonitor.spindle.halPins.speedOut > 0
), "spindle reverse");
await waitFor(() => doc.querySelector("[data-active-gcodes]")?.dataset.activeGcodes.includes("M4") ? true : null, "active G-codes reverse spindle");
await click('[data-action="spindle-stop"]', "spindle stop");
await waitState((state) => state.spindle.direction === "stop" && state.spindle.enabled === false, "spindle stop");
await waitState((state) => (
state.spindle.direction === "stop"
&& state.spindle.enabled === false
&& state.linuxCncProcessMonitor.spindle.halPins.on === 0
&& state.linuxCncProcessMonitor.spindle.halPins.forward === 0
&& state.linuxCncProcessMonitor.spindle.halPins.reverse === 0
&& state.linuxCncProcessMonitor.spindle.halPins.speedOut === 0
), "spindle stop");
await waitFor(() => doc.querySelector("[data-active-gcodes]")?.dataset.activeGcodes.includes("M5") ? true : null, "active G-codes stopped spindle");
await click('[data-action="spindle-forward"]', "spindle forward");
await waitState((state) => (
state.spindle.direction === "forward"
&& state.spindle.enabled === true
&& state.linuxCncProcessMonitor.spindle.halPins.on === 1
&& state.linuxCncProcessMonitor.spindle.halPins.forward === 1
&& state.linuxCncProcessMonitor.spindle.halPins.reverse === 0
&& state.linuxCncProcessMonitor.spindle.halPins.speedOut > 0
), "spindle forward");
await waitFor(() => doc.querySelector("[data-active-gcodes]")?.dataset.activeGcodes.includes("M3") ? true : null, "active G-codes forward spindle");
await click('[data-action="spindle-stop"]', "spindle stop after forward");
await waitState((state) => state.spindle.direction === "stop" && state.spindle.enabled === false, "spindle stop after forward");
await click('[data-action="toggle-flood"]', "flood coolant");
await waitState((state) => state.coolant.flood === true, "flood coolant");
await click('[data-action="block-delete"]', "block delete");

View File

@@ -313,11 +313,15 @@ for (const action of [
"run",
"pause",
"resume",
"pause-resume",
"step",
"stop",
"home-all",
"jog-plus",
"jog-minus",
"spindle-reverse",
"spindle-stop",
"spindle-forward",
"kins-identity",
"kins-tcp",
"kins-userk",
@@ -335,14 +339,61 @@ buttonStore.dispatch({ type: "TOGGLE_POWER" });
assert.equal(buttonStore.getState().machine.powerOn, true);
buttonStore.dispatch({ type: "HOME" });
assert.equal(buttonStore.getState().machine.allHomed, true);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.x, 43);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.y, -32.15);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.z, -11.306);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.x, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.y, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.z, 10);
await buttonStore.dispatch({ type: "RUN_FROM_OPERATOR" });
assert.equal(buttonStore.getState().machine.mode, "auto");
assert.equal(["running", "complete"].includes(buttonStore.getState().runState), true);
assert.equal(Boolean(buttonStore.getState().programRuntimeFeedback), true);
if (buttonStore.getState().runState === "running") {
buttonStore.dispatch({ type: "PAUSE_RESUME" });
assert.equal(buttonStore.getState().runState, "paused");
assert.equal(buttonStore.getState().machine.interpState, "paused");
buttonStore.dispatch({ type: "PAUSE_RESUME" });
assert.equal(buttonStore.getState().runState, "running");
assert.equal(buttonStore.getState().machine.interpState, "reading");
buttonStore.dispatch({ type: "PAUSE_RESUME" });
assert.equal(buttonStore.getState().runState, "paused");
assert.equal(buttonStore.getState().machine.interpState, "paused");
const beforeStepSample = buttonStore.getState().programExecutionSampleIndex;
buttonStore.dispatch({ type: "STEP" });
assert.equal(buttonStore.getState().runState, "stepping");
assert.equal(buttonStore.getState().machine.interpState, "paused");
assert.equal(buttonStore.getState().programExecutionSampleIndex > beforeStepSample, true);
buttonStore.dispatch({ type: "RESUME" });
assert.equal(buttonStore.getState().runState, "running");
assert.equal(buttonStore.getState().machine.interpState, "reading");
buttonStore.dispatch({ type: "PAUSE" });
assert.equal(buttonStore.getState().runState, "paused");
assert.equal(buttonStore.getState().machine.interpState, "paused");
buttonStore.dispatch({ type: "RESUME" });
assert.equal(buttonStore.getState().runState, "running");
assert.equal(buttonStore.getState().machine.interpState, "reading");
}
buttonStore.dispatch({ type: "STOP" });
const idleInterpRunningStore = createSimulationStore({
runState: "running",
machine: {
powerOn: true,
estopActive: false,
taskState: "on",
mode: "auto",
interpState: "idle",
interpResumeState: "reading",
taskPaused: false,
allHomed: true,
noForceHoming: false,
},
});
idleInterpRunningStore.dispatch({ type: "PAUSE" });
assert.equal(idleInterpRunningStore.getState().runState, "paused");
assert.equal(idleInterpRunningStore.getState().machine.interpState, "paused");
assert.equal(idleInterpRunningStore.getState().machine.taskPaused, true);
idleInterpRunningStore.dispatch({ type: "RESUME" });
assert.equal(idleInterpRunningStore.getState().runState, "running");
assert.equal(idleInterpRunningStore.getState().machine.interpState, "reading");
buttonStore.dispatch({ type: "SET_MODE", mode: "manual" });
buttonStore.dispatch({ type: "SET_MODE", mode: "mdi" });
buttonStore.dispatch({ type: "RUN_MDI", command: "M428" });
@@ -370,10 +421,28 @@ const feedBefore = buttonStore.getState().feed.feedOverride;
buttonStore.dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: 10 });
assert.equal(buttonStore.getState().feed.feedOverride, feedBefore + 10);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.feed.feedOverridePercent, feedBefore + 10);
buttonStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "reverse" });
assert.equal(buttonStore.getState().spindle.direction, "reverse");
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.enabled, true);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 1);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 1);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 1600);
buttonStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" });
assert.equal(buttonStore.getState().spindle.direction, "stop");
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.enabled, false);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 0);
buttonStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" });
assert.equal(buttonStore.getState().spindle.direction, "forward");
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.enabled, true);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.actualRpm, 1600);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 1);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 1);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 1600);
buttonStore.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
assert.equal(buttonStore.getState().coolant.flood, true);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.coolant.flood, true);