mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
Press the button when the machine asks, not when its LED is on
The sheet handed the bench actuator every press after the operator's presence press, and the actuator pressed every time - into nothing. On the bench reference all six cards were pressed before the card asked: the frame by 8 s, the focus by 33, the floor by 10, the dose curve by 8, the corner by 9, and the flow-load card by 69. The operator then pressed all six himself, which is the opposite of what the ready gate promises. arm_press() waits for hw.button_lit(), which is true when any button LED is on, and burn() started that wait before run_check had even started the wizard. The button is lit through parts of a card that are not the arm - the lens reference, the program on its way to the controller - so the wait ended at once, the press landed before the job waited for it, and the thread was gone by the time the real cue came. forgectrl uses the same predicate but only inside the job's own sample callback, with the tube still dark, where a lit button does mean the arm. The machine already says when it wants the press: a live check opens a `press` wait prompt at that moment, and run_check sees every prompt. It now presses there, through a new Ctx.press_now() - no LED read, no waiting thread, no timing guess. That retires the per-card lit-timeout column of CARDS, which existed only to give the flow-load card's coolant settle enough room for a wait that was reading the wrong thing. The four grbl-driven arm_press() callers in laser.py and cooling.py are left as they are: they call it with the machine idle and its LEDs dark, so the level read is the edge they mean. The same shape would bite them if that ever stopped being true. Pin: forgectrl 0.1.15 (6e69479, the flow-load tail ending at the coolant peak and the way out of a finished setup page). Host-proven: 357 forgetest unit tests, including two new ones - the actuator presses on the prompt with button_lit stubbed false throughout, so the LED is provably not consulted, and the press falls to the operator without a takeover.
This commit is contained in:
@@ -424,6 +424,45 @@ class Context:
|
||||
self.log("ACT %s %s: done after %.1f s", channel, state, dt)
|
||||
return dt
|
||||
|
||||
def press_now(self, text="The button is lit white. Press it now: the laser fires after your press."):
|
||||
"""The arm press, at the moment the machine says it is waiting.
|
||||
|
||||
A check that runs a job opens a `press` prompt of its own when
|
||||
it sees the button lit and the tube still dark, which is the
|
||||
machine's own word that the press it wants is this one. Pressing
|
||||
on that is exact; pressing on a button LED read from here is
|
||||
not, because the button is lit through parts of a check that are
|
||||
not the arm (the lens reference, the program on its way to the
|
||||
controller), and a press then lands before the job waits for it
|
||||
and is lost.
|
||||
|
||||
The fixture presses where the bench took this test's presses (or
|
||||
opted in standing); otherwise the notice goes up for a person,
|
||||
who is looking at the same prompt. Returns True when the fixture
|
||||
pressed."""
|
||||
fixture = getattr(self.runner, "fixture", None) if self.runner is not None else None
|
||||
rec = {"channel": "button", "state": "arm", "by": "operator", "ts": now_ts()}
|
||||
self.evidence.setdefault("actions", []).append(rec)
|
||||
opted_in = fixture is not None and fixture.covers("button") and \
|
||||
(fixture.arm_press or self.run.fixture_takeover)
|
||||
if not opted_in:
|
||||
if self.run.fixture_takeover:
|
||||
rec["fixture_lost"] = True
|
||||
self.log("ARM: WARNING the bench actuator took this test's presses and is now "
|
||||
"gone - asking the operator for the arm press")
|
||||
self.notice(text)
|
||||
return False
|
||||
try:
|
||||
fixture.act("button", "press")
|
||||
except _fixture.FixtureError as e:
|
||||
rec["fixture_error"] = str(e)
|
||||
self.log("ARM press: fixture failed (%s) - asking the operator", e)
|
||||
self.notice(text)
|
||||
return False
|
||||
rec["by"] = "fixture"
|
||||
self.log("ARM press by the fixture, at the machine's own press prompt")
|
||||
return True
|
||||
|
||||
def arm_press(self, text="The button lights white: press it to arm. The machine fires after your press.",
|
||||
lit_timeout=60):
|
||||
"""The arm cue of a live test. A person's press by default: a
|
||||
|
||||
@@ -15,6 +15,8 @@ from ..baseline import read_position
|
||||
from .commission import wiz, Restore # noqa: F401 - Restore is re-exported for the sheet
|
||||
|
||||
POLL_S = 1.0
|
||||
# What the machine's own press prompt says, used when it sends no text.
|
||||
PRESS_TEXT = "The button is lit white. Press it now: the laser fires after your press."
|
||||
DARK_COVERS = [("forgectrl", "src/wizdark.*"), ("forgectrl", "src/wizcalc.*"),
|
||||
("forgectrl", "src/wiz.*"), ("forgectrl", "src/commission.*"),
|
||||
("forgectrl", "src/main.c"), ("forgectrl", "src/ui/wizard.*")]
|
||||
@@ -76,6 +78,12 @@ def run_check(ctx, wid, on_prompt, timeout_s):
|
||||
if value is not None:
|
||||
st, body = answer(fc, wid, p, value)
|
||||
ctx.check(st == 200, "the answer to %s -> %s %s", p.get("id"), st, body)
|
||||
elif p.get("id") == "press":
|
||||
# The arm press, asked for by the machine itself: the
|
||||
# check opens this prompt when the button is lit and
|
||||
# the tube is still dark. Nothing answers it - the
|
||||
# press does, and the discharge closes it.
|
||||
ctx.press_now(p.get("text") or PRESS_TEXT)
|
||||
if not d.get("running"):
|
||||
last = d
|
||||
break
|
||||
|
||||
@@ -36,19 +36,18 @@ SHEET_COVERS = [("forgectrl", "src/wizlive.*"), ("forgectrl", "src/wizrun.h"),
|
||||
|
||||
# The live wizards in the order the sheet burns them: the wizard id, the
|
||||
# answers to its prompts (a middle pick where a person would look at the
|
||||
# sheet), the keys its result must carry, and how long the bench actuator
|
||||
# waits for the button to light before the arm press goes to a person.
|
||||
# The flow-load card settles the coolant loop before it lights the button
|
||||
# (60 to 240 s); the others light within seconds of the lens reference.
|
||||
# sheet), and the keys its result must carry. The arm press is not timed
|
||||
# from here: each card opens a press prompt of its own when the button is
|
||||
# lit and the tube still dark, and run_check presses on that.
|
||||
CARDS = [
|
||||
("sheet.frame", {"frame-arm": "Continue", "frame-ok": "Yes"}, ["mark_s", "mark_feed"], 120),
|
||||
("sheet.frame", {"frame-arm": "Continue", "frame-ok": "Yes"}, ["mark_s", "mark_feed"]),
|
||||
("laser.focus", {"focus-arm": "Continue", "focus-pick": "11", "thickness": "Keep"},
|
||||
["pick", "thickness_mm", "pick_half_steps", "edge_z_mm", "steps_per_mm", "max_height_mm",
|
||||
"focus_range_mm", "stops"], 120),
|
||||
("laser.floor", {"floor-arm": "Continue", "floor-pick": "8"}, ["faintest_density", "floor_density"], 120),
|
||||
("laser.dose-curve", {"dose-arm": "Continue"}, ["points", "curve"], 120),
|
||||
("laser.corner", {"corner-arm": "Continue", "corner-pick": "1.50"}, ["gamma"], 120),
|
||||
("cooling.flow-load", {"load-arm": "Continue"}, ["lit_s", "dose_raw_s", "peak_c", "k_density", "k_cw"], 420),
|
||||
"focus_range_mm", "stops"]),
|
||||
("laser.floor", {"floor-arm": "Continue", "floor-pick": "8"}, ["faintest_density", "floor_density"]),
|
||||
("laser.dose-curve", {"dose-arm": "Continue"}, ["points", "curve"]),
|
||||
("laser.corner", {"corner-arm": "Continue", "corner-pick": "1.50"}, ["gamma"]),
|
||||
("cooling.flow-load", {"load-arm": "Continue"}, ["lit_s", "dose_raw_s", "peak_c", "k_density", "k_cw"]),
|
||||
]
|
||||
# Every setting a card writes, restored as found when the run ends.
|
||||
CARD_SETTINGS = ["lens_hall_edge_z_mm", "lens_stop_below_steps", "lens_stop_above_steps",
|
||||
@@ -130,12 +129,12 @@ def place(ctx):
|
||||
file_card(ctx, "sheet.place")
|
||||
|
||||
|
||||
def burn(ctx, wid, answers, want, lit_s):
|
||||
def burn(ctx, wid, answers, want):
|
||||
"""One live card: the preview, the burn after the press with the
|
||||
prompts answered from `answers`, the witnesses, the keys of the
|
||||
result in `want`. The actuator waits `lit_s` for the button to light."""
|
||||
result in `want`. run_check makes the arm press when the card asks
|
||||
for it."""
|
||||
preview_ok(ctx, wid)
|
||||
ctx.arm_press(lit_timeout=lit_s)
|
||||
|
||||
def on_prompt(p):
|
||||
return answers.get(p.get("id"))
|
||||
@@ -161,9 +160,10 @@ def burn(ctx, wid, answers, want, lit_s):
|
||||
"it goes with its top edge at the top of the cut area (the head's home corner); "
|
||||
"lid closed. Eye protection on, exhaust on, extinguisher in reach.",
|
||||
"Start the test and press the machine's button once. That press says you are at the "
|
||||
"machine, and the bench actuator makes every arm press after it. With no actuator "
|
||||
"the start is the Ready answer instead, and you press the button each time it lights "
|
||||
"white: six times, the frame and the five cards.",
|
||||
"machine, and the bench actuator makes every arm press after it, each one when the "
|
||||
"card itself asks for it. With no actuator the start is the Ready answer instead, "
|
||||
"and you press the button each time the page asks: six times, the frame and the "
|
||||
"five cards.",
|
||||
"Nothing else: the test answers every prompt itself and puts back every setting the "
|
||||
"cards write. It runs about half an hour, mostly the coolant settle and the dark "
|
||||
"tails."],
|
||||
@@ -191,6 +191,6 @@ def sheet(ctx):
|
||||
"each. The bench presses when the button lights.")
|
||||
place(ctx)
|
||||
with Restore(ctx, CARD_SETTINGS):
|
||||
for wid, answers, want, lit_s in CARDS:
|
||||
burn(ctx, wid, answers, want, lit_s)
|
||||
for wid, answers, want in CARDS:
|
||||
burn(ctx, wid, answers, want)
|
||||
ctx.log("PASS: the sheet's seven wizards ran on one piece; settings restored")
|
||||
|
||||
@@ -586,6 +586,41 @@ class RoutingTests(unittest.TestCase):
|
||||
finally:
|
||||
runner_mod.hw.button_lit = saved
|
||||
|
||||
def test_the_press_prompt_is_what_the_actuator_presses_on(self):
|
||||
"""The machine's own press prompt, not a button LED read from
|
||||
here: the button is lit through parts of a live check that are
|
||||
not the arm, and a press then lands before the job waits for it
|
||||
and is lost. press_now presses at the moment the check asks, with
|
||||
no LED reading and no waiting thread."""
|
||||
run = Run("test", "r.live", "r.live")
|
||||
ctx = Context(run, self.runner, self.reg["r.live"])
|
||||
self.runner.probe_fixture(force=True)
|
||||
self.stub.arm_press = False
|
||||
run.fixture_takeover = True
|
||||
saved = runner_mod.hw.button_lit
|
||||
runner_mod.hw.button_lit = lambda: False # never consulted
|
||||
try:
|
||||
self.assertTrue(ctx.press_now())
|
||||
self.assertIn(("button", "press"), self.stub.acts)
|
||||
finally:
|
||||
runner_mod.hw.button_lit = saved
|
||||
rec = [r for r in run.evidence["actions"] if r["state"] == "arm"][-1]
|
||||
self.assertEqual(rec["by"], "fixture")
|
||||
|
||||
def test_the_press_prompt_goes_to_the_operator_without_an_actuator(self):
|
||||
"""No actuator, or one that never took this test's presses: the
|
||||
notice goes up for a person, who is looking at the same prompt."""
|
||||
run = Run("test", "r.live", "r.live")
|
||||
ctx = Context(run, self.runner, self.reg["r.live"])
|
||||
self.runner.probe_fixture(force=True)
|
||||
self.stub.arm_press = False
|
||||
run.fixture_takeover = False
|
||||
seen = []
|
||||
run.set_notice = lambda text: seen.append(text)
|
||||
self.assertFalse(ctx.press_now("Press it now."))
|
||||
self.assertNotIn(("button", "press"), self.stub.acts)
|
||||
self.assertIn("Press it now.", seen)
|
||||
|
||||
def test_the_arm_press_waits_as_long_as_the_card_says_for_the_light(self):
|
||||
"""A card that settles the coolant before it lights the button
|
||||
takes minutes; the caller names the wait, and the actuator presses
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# only SRCREV and PV here - the image manifest leaves *-pin.inc out of the
|
||||
# layer content hash because the component entry already identifies the
|
||||
# pinned source (forgefirm-image-manifest.bbclass).
|
||||
SRCREV = "9e5330ffbb27dcf2fb89f414f1e8fc38bfdfb6f7"
|
||||
PV = "0.1.14"
|
||||
SRCREV = "6e694798903df6add92ec97876cda4851b739aa0"
|
||||
PV = "0.1.15"
|
||||
|
||||
Reference in New Issue
Block a user