forgetest: the button's commanded level, and one app cancel instead of two

laser.arm-wait-lid failed its first bench run on the witness, not the
machine: the cancel relocked, disarmed and emitted nothing, but the
"button dark" check read the LEDs' brightness the instant after, and
the smooth trigger fades it. The controller writes `target`, so that is
what hw.button_leds() reads now (brightness where no target exists),
and check_button_dark waits a few seconds for the command to land.

The operator's campaign showed cloud.oversize-stream and
cloud.pause-cancel-paths both cancelling a print from the app. A print
longer than the ring has to be ended that way, and the operator is at
the app for it anyway, so the app cancel lives there and is judged in
full (the abort tail: stop, park to the job start, relock, disarm, the
button dark, ':cancelled'); cloud.pause-cancel-paths loses its second
print and becomes cloud.paused-lid-cancel - one print, one arm press.
Replays follow. Catalog consequence: the two cloud tests and the laser
module are re-required; the count stays 43.
This commit is contained in:
ScottW514
2026-08-22 17:47:26 -04:00
parent 9139e923b1
commit 296fd686e1
3 changed files with 63 additions and 73 deletions
+12 -6
View File
@@ -264,14 +264,20 @@ def leds_root():
def button_leds():
"""The three button LED brightnesses, or None where unreadable."""
"""The three button LEDs' commanded levels (the smooth trigger's
`target`, which the controller writes; `brightness` follows it with
a fade and is read where no target exists), or None where unreadable."""
out = []
for name in BUTTON_LEDS:
try:
with open(leds_root() + name + "/brightness") as f:
out.append(int(f.read().strip()))
except (OSError, ValueError):
out.append(None)
val = None
for attr in ("target", "brightness"):
try:
with open(leds_root() + name + "/" + attr) as f:
val = int(f.read().strip())
break
except (OSError, ValueError):
continue
out.append(val)
return out
+43 -65
View File
@@ -1007,51 +1007,62 @@ def oversize_stream(ctx):
ctx.check(hw.sysfs_int("cnc/underruns", 0) == before,
"the pause or the resume starved the ring")
# The app's cancel is how a print this long ends, and it is the one
# place the service-side cancel is exercised: the same tail as a lid
# or interlock abort - stop, park back to the job start, relock,
# disarm, ':cancelled' - judged in full.
ctx.notice("Now cancel the print from the app. The test watches for the cancel.")
svc_stop = "action cancelled mid-run; stopping motion"
try:
fin = wait_action_finished(ctx, offset, "print", 300)
got = wait_log(ctx, offset, [svc_stop, "start return home", "return home complete"], 300)
fin = wait_action_finished(ctx, offset, "print", 60)
finally:
ctx.clear_notice()
ev["service_cancel"] = {k: message(v) for k, v in got.items()}
ev["print_finished"] = message(fin)
ctx.check(fin, "the cancelled print did not finish within 300 s")
ctx.check(CANCELLED in fin, "the cancelled print did not report cancelled: %s", message(fin))
for k, v in ev["service_cancel"].items():
ctx.log(" [cancel] %s: %s", k, "seen" if v else "MISSING")
ctx.check(got[svc_stop], "the app's cancel did not stop the run")
ctx.check(got["return home complete"], "the cancelled print did not park to completion")
judge_abort_tail(ctx, ev, offset, "app", fin)
ctx.check(hw.sysfs_int("cnc/streaming", 0) == 0,
"the device was left in live-feed mode after the job ended")
ctx.check(hw.sysfs_int("cnc/underruns", 0) == before,
"the cancel produced an underrun")
ev["button_dark"] = hw.button_lit()
ctx.check(ev["button_dark"] is False, "the button is still lit after the cancel (%s)", ev["button_dark"])
settle_cloud(ctx, offset)
ctx.log("PASS: a job longer than the ring ran live-fed, total grew, no underrun, clean cancel")
ctx.log("PASS: a job longer than the ring ran live-fed, total grew, no underrun; the app's cancel "
"stopped it, parked, relocked and reported ':cancelled'")
@test("cloud.pause-cancel-paths", title="A paused cloud print is cancelled by the lid, and a running one "
"by the app",
subsystem="cloud", kind="live", est_min=12,
@test("cloud.paused-lid-cancel", title="A paused cloud print is cancelled by the lid",
subsystem="cloud", kind="live", est_min=6,
covers=_CLOUD_COVERS, requires=["cloud.pause-resume", "cloud.lid-interlock-abort"],
actions=["button", "lid"],
steps=[CLOUD_STEP, LID_STEP,
"The app open in a browser; scrap on the bed and a small engrave/score job "
"ready - the test runs TWO prints.",
"Print 1: press the button to start; when asked, press it again (pause), then open "
"the lid and leave it open until the head is back; close it when told.",
"Print 2: press the button to start, then cancel the print from the app when asked."],
description="The two ways a print ends other than finishing, each from the state the factory ends "
"it in: a job paused on the button is cancelled by the lid - there is no resume past a "
"lid open - and a running job is cancelled from the app. Both take the same tail: the "
"motion stops, the head parks back at the job start, the latch relocks and the armed "
"window closes, and the print ends ':cancelled'.")
def pause_cancel_paths(ctx):
"The app open in a browser; scrap on the bed and a small engrave/score job ready.",
"Press the button to start; when asked, press it again (pause), then open the lid and "
"leave it open until the head is back; close it when told."],
description="A job paused on the button is cancelled by the lid, from the state the factory "
"ends it in - there is no resume past a lid open. The same tail as every abort: "
"the motion stops, the head parks back at the job start, the latch relocks and "
"the armed window closes, the button goes dark, and the print ends ':cancelled'. "
"(The app's own cancel of a running print is exercised where a print has to be "
"ended that way: cloud.oversize-stream.)")
def paused_lid_cancel(ctx):
ev = ctx.evidence
offset = enter_cloud(ctx)
# -- print 1: paused on the button, then the lid ------------------------
# -- paused on the button, then the lid ---------------------------------
ctx.instruct(APP_PRINT_CUE)
got = wait_print_running(ctx, offset, 300)
ctx.check(got, "print 1 never reached its run within 300 s (not started, or the button not pressed)")
ctx.check(got, "the print never reached its run within 300 s (not started, or the button not pressed)")
ctx.act("button", "press", text="The head is moving: the press pauses the print.",
until=lambda: log_has(offset, PAUSE_LINES[0]), timeout=PRESS_TIMEOUT_S, fail=False)
got = wait_log(ctx, offset, list(PAUSE_LINES), 10)
ev["paused"] = {k: bool(v) for k, v in got.items()}
ctx.check(got["button pressed mid-run; pausing"], "the press did not pause print 1")
ctx.check(got["button pressed mid-run; pausing"], "the press did not pause the print")
ctx.check(got["paused at"], "the pause did not settle (no 'paused at')")
ctx.act("lid", "open", text="The print is paused: leave the lid open until the head has returned "
"to the corner.", timeout=60)
@@ -1061,57 +1072,24 @@ def pause_cancel_paths(ctx):
ev["lid_from_pause"] = {k: message(v) for k, v in got.items()}
ev["lid_from_pause"]["print finished"] = message(fin1)
for k, v in ev["lid_from_pause"].items():
ctx.log(" [print 1] %s: %s", k, "seen" if v else "MISSING")
ctx.log(" [print] %s: %s", k, "seen" if v else "MISSING")
ctx.check(got[lid_stop], "the lid did not end the paused print")
ctx.check(got["return home complete"], "the paused print did not park to completion")
ctx.check(fin1 and CANCELLED in fin1, "print 1 did not end ':cancelled': %s",
ctx.check(fin1 and CANCELLED in fin1, "the print did not end ':cancelled': %s",
message(fin1) or "no finish line")
ctx.check(ctx.forgectrl.wait_idle(15, abort=ctx.aborted), "machine not idle after print 1's park")
ctx.check(ctx.forgectrl.wait_idle(15, abort=ctx.aborted), "machine not idle after the park")
kpos = read_position()
ev["counters_after_print1"] = kpos
ev["counters_after"] = kpos
ctx.check(kpos is not None and abs(kpos[0]) <= 3 and abs(kpos[1]) <= 3,
"print 1 did not come back to the job start (kernel counters %s)", kpos)
"the head did not come back to the job start (kernel counters %s)", kpos)
st, cs = ctx.forgectrl.get("/cool/status")
ev["armed_after_print1"] = cs.get("armed") if isinstance(cs, dict) else None
ev["latch_locked_after_print1"] = latch_locked()
ctx.check(not ev["armed_after_print1"], "armed window still open after the paused print was cancelled")
ctx.check(ev["latch_locked_after_print1"],
ev["armed_after"] = cs.get("armed") if isinstance(cs, dict) else None
ev["latch_locked_after"] = latch_locked()
ctx.check(not ev["armed_after"], "armed window still open after the paused print was cancelled")
ctx.check(ev["latch_locked_after"],
"kernel latch not locked after the paused print was cancelled")
ctx.act("lid", "close")
settle_cloud(ctx, offset)
# -- print 2: cancelled from the app ------------------------------------
offset = log_size(GFCLOUD_LOG)
ctx.instruct(APP_PRINT_CUE)
got = wait_print_running(ctx, offset, 300)
ctx.check(got, "print 2 never reached its run within 300 s (not started, or the button not pressed)")
ctx.notice("The head is moving: cancel the print from the app now. The test watches for it.")
svc_stop = "action cancelled mid-run; stopping motion"
try:
got = wait_log(ctx, offset, [svc_stop, "start return home", "return home complete"], 120)
finally:
ctx.clear_notice()
fin2 = wait_action_finished(ctx, offset, "print", 60)
ev["service_cancel"] = {k: message(v) for k, v in got.items()}
ev["service_cancel"]["print finished"] = message(fin2)
for k, v in ev["service_cancel"].items():
ctx.log(" [print 2] %s: %s", k, "seen" if v else "MISSING")
ctx.check(got[svc_stop], "the app's cancel did not stop the run")
ctx.check(got["return home complete"], "the cancelled print did not park to completion")
ctx.check(fin2 and CANCELLED in fin2, "print 2 did not end ':cancelled': %s",
message(fin2) or "no finish line")
ctx.check(ctx.forgectrl.wait_idle(15, abort=ctx.aborted), "machine not idle after print 2's park")
kpos = read_position()
ev["counters_after_print2"] = kpos
ctx.check(kpos is not None and abs(kpos[0]) <= 3 and abs(kpos[1]) <= 3,
"print 2 did not come back to the job start (kernel counters %s)", kpos)
st, cs = ctx.forgectrl.get("/cool/status")
ev["armed_after_print2"] = cs.get("armed") if isinstance(cs, dict) else None
ev["latch_locked_after_print2"] = latch_locked()
ctx.check(not ev["armed_after_print2"], "armed window still open after the app cancel")
ctx.check(ev["latch_locked_after_print2"], "kernel latch not locked after the app cancel")
ev["button_dark"] = hw.button_lit()
ctx.check(ev["button_dark"] is False, "the button is still lit after the cancel (%s)", ev["button_dark"])
ctx.act("lid", "close")
settle_cloud(ctx, offset)
ctx.log("PASS: a paused print cancelled by the lid and a running print cancelled from the app both "
"stopped, parked, relocked and reported ':cancelled'")
ctx.log("PASS: a paused print cancelled by the lid stopped, parked, relocked and reported ':cancelled'")
+8 -2
View File
@@ -102,11 +102,17 @@ def judge_beam(ctx, b, what="the burn"):
"or the detector is not reading)", what, b["idle"], b["peak"])
def check_button_dark(ctx, ev, key="button_dark"):
def check_button_dark(ctx, ev, key="button_dark", settle_s=5.0):
"""The button commanded dark (the LEDs' target level) within a few
seconds: the controller writes it a moment after the cancel or the
disarm it reports."""
dt = ctx.wait_for(lambda: hw.button_lit() is False, settle_s)
lit = hw.button_lit()
ev[key] = lit
ctx.check(lit is not None, "the button LEDs are not readable")
ctx.check(lit is False, "the button is still lit")
ctx.check(lit is False, "the button is still lit %.0f s after it should have gone dark", settle_s)
if dt:
ctx.log("button dark after %.1f s", dt)
def prepare(ctx, g):