mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
Hold the job start across a pause in the lifecycle harness
laser.pause-resume-lid-cancel failed on the bench with the head 14.925 mm along the first leg after the lid cancel: the driver took every resume from a feed hold as a new job start (the core restarts a held cycle through Idle), so the cancel returned the head to the pause point and said "returned to the job start" about it. The driver fix is in grblHAL-glowforge; this is its proof and the record. The null-sink lifecycle harness gains rule 9: a job paused and resumed by the button, then cancelled by the lid, returns to where the job began (reproduces the bench failure to the millimeter on the old driver, X=13.088 instead of 0); and a job abandoned in a hold and soft-reset is over, so the next job from that spot returns to that spot. The campaign log carries the dated account. No catalog consequence: the acceptance test measured exactly this and stays as it is; the harness is the CI side of the same contract.
This commit is contained in:
@@ -3167,6 +3167,39 @@ that only showed up when a real 53 MB job was replayed through a fake ring
|
|||||||
rather than a synthetic one. The job that hung the bench is kept as the
|
rather than a synthetic one. The job that hung the bench is kept as the
|
||||||
regression fixture.
|
regression fixture.
|
||||||
|
|
||||||
|
## 2026-08-21: a lid cancel that went back to the wrong place
|
||||||
|
|
||||||
|
`laser.pause-resume-lid-cancel` failed its first run on the dev image of
|
||||||
|
2026-08-21 with "head not back at the job start (drift 14.925 mm)", and at the
|
||||||
|
bench the job had looked right: the button paused the cut, the button resumed
|
||||||
|
it, the lid stopped it, and the head came back. It came back to the wrong
|
||||||
|
place. The kernel counters agreed with the controller's own position report:
|
||||||
|
Y exactly where the job began, X 14.925 mm along the first leg, which at F200
|
||||||
|
is about four and a half seconds of cutting, the moment the pause landed.
|
||||||
|
|
||||||
|
**The controller had told the truth by its own bookkeeping.** The driver takes
|
||||||
|
the job start as the machine position at the Idle to Cycle transition, and the
|
||||||
|
grblHAL core restarts a held cycle by passing through Idle: `state_await_resume`
|
||||||
|
sets Idle and then Cycle back to back, so every resume from a feed hold was
|
||||||
|
recorded as a new job beginning where the hold had stopped. The lid cancel then
|
||||||
|
returned the head to the pause point and reported "returned to the job start",
|
||||||
|
which was exactly what it had written down.
|
||||||
|
|
||||||
|
**The fix is a definition.** A job is under way from that first transition
|
||||||
|
until the core is Idle with the planner empty (the program ran out, a stop, a
|
||||||
|
reset) or in an alarm. A resume passes through Idle with the planner still
|
||||||
|
loaded, so it is the same job and keeps its start; a job abandoned in a hold
|
||||||
|
and reset is over, and the next one starts where it starts. Both sides are
|
||||||
|
held by the null-sink lifecycle harness now, which reproduced the bench
|
||||||
|
failure to the millimeter (returned to X=13.088, the pause point) before the
|
||||||
|
fix and returns to X=0.000 after it.
|
||||||
|
|
||||||
|
**Why the acceptance test caught it and the eye did not:** the test measures
|
||||||
|
the return against the position it recorded before the job, not against the
|
||||||
|
controller's message. An operator watching the head come back has no such
|
||||||
|
reference, and fifteen millimeters on a forty millimeter square reads as
|
||||||
|
"back". The test stays as it is.
|
||||||
|
|
||||||
## Superseded status notes
|
## Superseded status notes
|
||||||
|
|
||||||
### Shared machine services — remaining polish, as listed 2026-08-13
|
### Shared machine services — remaining polish, as listed 2026-08-13
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ reported messages:
|
|||||||
reset with the position kept (no alarm), the head returns to the
|
reset with the position kept (no alarm), the head returns to the
|
||||||
job start on its own; with lid_policy = hold the stock door hold and
|
job start on its own; with lid_policy = hold the stock door hold and
|
||||||
cycle-start resume apply
|
cycle-start resume apply
|
||||||
|
9. the job start survives a pause: a job paused and resumed by the
|
||||||
|
button, then cancelled by the lid, returns to where the job began,
|
||||||
|
not to where it was paused (the core restarts a held cycle through
|
||||||
|
Idle); a job abandoned in a hold and reset ends there, so the next
|
||||||
|
job's start is captured afresh where it begins
|
||||||
|
|
||||||
The disarm grace is shortened via a temp config (GFHOME_CONF), the
|
The disarm grace is shortened via a temp config (GFHOME_CONF), the
|
||||||
cooling verdict is published hermetically (GF_VERDICT_FILE), the same
|
cooling verdict is published hermetically (GF_VERDICT_FILE), the same
|
||||||
@@ -546,6 +551,77 @@ def test_lid_cancels_and_returns():
|
|||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_pause_then_lid_cancel_returns_to_the_job_start():
|
||||||
|
"""Rule 9: the button pause + resume does not move the job start. The
|
||||||
|
lid cancel after a resume returns the head to where the job began;
|
||||||
|
the pause point is not a job start. Then the other side: a job left
|
||||||
|
in a hold and soft-reset is over, and the next job from that spot
|
||||||
|
returns to that spot, not to the earlier job's start."""
|
||||||
|
s = Session("pause-lid-cancel", disarm_s=60, switches=SW_CLOSED)
|
||||||
|
try:
|
||||||
|
start_armed_move(s, "pause-lid-cancel", gcode="G1 X30 F600")
|
||||||
|
time.sleep(0.6)
|
||||||
|
s.press_button()
|
||||||
|
if not s.wait_state("Hold", 3):
|
||||||
|
fail("[pause-lid-cancel] the press did not feed-hold (state %s)" % s.state())
|
||||||
|
time.sleep(0.5)
|
||||||
|
x_hold = s.mpos_x()
|
||||||
|
if x_hold is None or x_hold < 1.0:
|
||||||
|
fail("[pause-lid-cancel] the hold landed too close to the start to tell (X=%s)" % x_hold)
|
||||||
|
s.press_button()
|
||||||
|
if not s.wait_state("Run", 3):
|
||||||
|
fail("[pause-lid-cancel] the second press did not resume (state %s)" % s.state())
|
||||||
|
time.sleep(0.4)
|
||||||
|
s.set_switches(SW_LID_OPEN)
|
||||||
|
if not wait_for(s.log, "lid opened - job cancelled", 5, s.sock):
|
||||||
|
fail("[pause-lid-cancel] the lid open did not cancel the job")
|
||||||
|
if not wait_for(s.log, "returned to the job start", 15, s.sock):
|
||||||
|
fail("[pause-lid-cancel] the head did not report returning to the job start")
|
||||||
|
if not s.wait_state("Idle", 5):
|
||||||
|
fail("[pause-lid-cancel] not Idle after the return (state %s)" % s.state())
|
||||||
|
x_end = s.mpos_x()
|
||||||
|
if x_end is None or abs(x_end) > 0.05:
|
||||||
|
fail("[pause-lid-cancel] the head returned to X=%s, not to the job start X=0 "
|
||||||
|
"(paused at X=%.3f: the resume was taken as a new job start)" % (x_end, x_hold))
|
||||||
|
print("PASS [pause-lid-cancel]: paused at X=%.3f, resumed, lid cancel returned to X=%.3f"
|
||||||
|
% (x_hold, x_end))
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
# A job abandoned in a hold and reset is over: the next job starts
|
||||||
|
# where it starts.
|
||||||
|
s = Session("hold-reset-restart", disarm_s=60, switches=SW_CLOSED)
|
||||||
|
start_armed_move(s, "hold-reset-restart", gcode="G1 X30 F600")
|
||||||
|
time.sleep(0.6)
|
||||||
|
s.press_button()
|
||||||
|
if not s.wait_state("Hold", 3):
|
||||||
|
fail("[hold-reset-restart] the press did not feed-hold (state %s)" % s.state())
|
||||||
|
time.sleep(0.5)
|
||||||
|
s.sock.sendall(b"\x18")
|
||||||
|
if not wait_for(s.log, "for help]", 5, s.sock):
|
||||||
|
fail("[hold-reset-restart] no reset banner")
|
||||||
|
if not s.wait_state("Idle", 5):
|
||||||
|
fail("[hold-reset-restart] not Idle after the reset (state %s)" % s.state())
|
||||||
|
x_new_start = s.mpos_x()
|
||||||
|
if x_new_start is None or x_new_start < 1.0:
|
||||||
|
fail("[hold-reset-restart] the reset did not keep the position (X=%s)" % x_new_start)
|
||||||
|
read_avail(s.sock, s.log, 0.3)
|
||||||
|
start_armed_move(s, "hold-reset-restart", gcode="G1 X60 F600") # absolute: past the old start
|
||||||
|
time.sleep(0.5)
|
||||||
|
s.set_switches(SW_LID_OPEN)
|
||||||
|
if not wait_for(s.log, "returned to the job start", 15, s.sock):
|
||||||
|
fail("[hold-reset-restart] no return to the job start after the lid cancel")
|
||||||
|
if not s.wait_state("Idle", 5):
|
||||||
|
fail("[hold-reset-restart] not Idle after the return (state %s)" % s.state())
|
||||||
|
x_end = s.mpos_x()
|
||||||
|
if x_end is None or abs(x_end - x_new_start) > 0.05:
|
||||||
|
fail("[hold-reset-restart] the head returned to X=%s, not to this job's start X=%.3f "
|
||||||
|
"(the earlier job's start survived the reset)" % (x_end, x_new_start))
|
||||||
|
print("PASS [hold-reset-restart]: a job reset from a hold ended there; the next job "
|
||||||
|
"returned to its own start X=%.3f" % x_end)
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
def test_lid_policy_hold():
|
def test_lid_policy_hold():
|
||||||
"""lid_policy = hold keeps stock grblHAL behavior: the lid parks the job
|
"""lid_policy = hold keeps stock grblHAL behavior: the lid parks the job
|
||||||
in Door and a cycle start resumes it once closed."""
|
in Door and a cycle start resumes it once closed."""
|
||||||
@@ -580,6 +656,7 @@ def main():
|
|||||||
test_lid_open_in_wait()
|
test_lid_open_in_wait()
|
||||||
test_button_pause_resume()
|
test_button_pause_resume()
|
||||||
test_lid_cancels_and_returns()
|
test_lid_cancels_and_returns()
|
||||||
|
test_pause_then_lid_cancel_returns_to_the_job_start()
|
||||||
test_lid_policy_hold()
|
test_lid_policy_hold()
|
||||||
test_verdict_blocks_arm()
|
test_verdict_blocks_arm()
|
||||||
test_sigterm_mid_job()
|
test_sigterm_mid_job()
|
||||||
|
|||||||
Reference in New Issue
Block a user