diff --git a/forgetest/forgetest/suite/setup_burns.py b/forgetest/forgetest/suite/setup_burns.py index 74425ef..265d608 100644 --- a/forgetest/forgetest/suite/setup_burns.py +++ b/forgetest/forgetest/suite/setup_burns.py @@ -43,8 +43,11 @@ BURN_COVERS = [("forgectrl", "src/wizlive.*"), ("forgectrl", "src/wizrun.h"), # Every burn asks first that the gantry stops are installed and the head # is at its home against them; the bench's head stands there, and every -# burn's tail brings it back. +# burn's tail brings it back. head_at_home reads the head's place from +# /status before the wizard starts and judges it at X0 Y0, within +# HOME_TOL_MM, when a manual home references X and Y. HOME = "The head is home" +HOME_TOL_MM = 0.05 EDGE_KEY = "lens_hall_edge_z_mm" FOCUS_KEYS = ["thickness_mm", "z_passes", "ladder_z", "window", "emission", "pick", "picked_lines", "pick_half_steps", "edge_z_mm", "steps_per_mm", "max_height_mm", "focus_range_mm", "summary"] @@ -80,6 +83,45 @@ class Witnesses: return {"laser_on_max": self.laser_on_max, "hv_max": self.hv_max, "reads": self.reads} +def head_at_home(ctx): + """The head's place on record before the wizard starts, read from + /status under the campaign's own controller: the wizard restarts the + controller before its home prompt, and a fresh controller drops the + X/Y reference, so a read at that prompt places nothing. Judged at + X0 Y0 within HOME_TOL_MM when X and Y are referenced by a manual + home, the one provider that declares X0 Y0 at the gantry stops. + Unreferenced, the head's place rests on the operator's word at the + ready gate, and the record says so.""" + fc = ctx.forgectrl + read = {} + + def served(): + st = fc.status() + if not isinstance(st.get("pos"), dict): + return False + read.update(st) + return True + took = ctx.wait_for(served, 10) + ctx.check(took is not None, "/status served no head position within 10 s: the head's place cannot " + "go on record before the burn") + pos = read.get("pos") or {} + axes = read.get("homed_axes") + source = read.get("home_source") + referenced = isinstance(axes, int) and (axes & 3) == 3 and source == "manual" + ctx.evidence["head_at_start"] = {"pos": pos, "homed_axes": axes, "home_source": source, + "judged": referenced} + if not referenced: + ctx.log("the head's place rests on the operator's word at the ready gate (X and Y unreferenced: " + "homed_axes %s, home_source %s, pos %s)", axes, source, pos) + return + x, y = pos.get("x"), pos.get("y") + at_home = (isinstance(x, (int, float)) and isinstance(y, (int, float)) + and abs(x) <= HOME_TOL_MM and abs(y) <= HOME_TOL_MM) + ctx.check(at_home, "the head is not at its home against the gantry stops: /status reads pos %s with X " + "and Y referenced by a manual home; home it there before the burn", pos) + ctx.log("the head is home on record: pos %s, X and Y referenced by a manual home", pos) + + def preview_ok(ctx, wid): """The page's preview and the program for the burn are served.""" fc = ctx.forgectrl @@ -267,9 +309,12 @@ def focus_window_ok(ctx, r, before): "the button again at the arm, as the page asks.", "Nothing else: the test answers every prompt itself and puts back the setting the " "burn writes."], - description="The focus burn as a fresh head runs it: the edge setting is cleared first, so " - "the burn starts from the bench reference's edge, while the lens window stays " - "the one the Lens travel check wrote (or the fallback). POST " + description="The focus burn as a fresh head runs it. Before anything starts, the head's " + "place is read from /status and recorded: judged at X0 Y0 when a manual home " + "references X and Y, else left to the operator's word at the ready gate. The " + "edge setting is cleared, so the burn starts from the bench reference's edge, " + "while the lens window stays the one the Lens travel check wrote (or the " + "fallback). POST " "/wiz/laser.focus/start is answered that the gantry stops are installed, " "references the lens, takes the controller in loopback posture, is answered that " "the head is home and that the scrap is in the corner, and takes the thickness " @@ -328,6 +373,7 @@ def focus(ctx): ctx.log("the controller could not be restarted on the settings as found: %s", e) ctx.ready("The focus burn: twelve lines on the scrap after a press. The bench presses when the " "button lights.") + head_at_home(ctx) try: with Restore(ctx, [EDGE_KEY] + LENS_WINDOW_KEYS): # A fresh head's focus: the edge unknown, so the burn runs from the diff --git a/forgetest/tests/test_setup_burns.py b/forgetest/tests/test_setup_burns.py index be4bfb6..7d9e9bc 100644 --- a/forgetest/tests/test_setup_burns.py +++ b/forgetest/tests/test_setup_burns.py @@ -290,6 +290,9 @@ class BurnRunTests(unittest.TestCase): self.saved_root = os.environ.get("GF_SYSFS_ROOT") os.environ["GF_SYSFS_ROOT"] = self.sysfs + "/" self.fake = helpers.FakeForgectrl().start() + # the head at its home against the gantry stops, X and Y referenced + # by a manual home (the fake's pos is X0 Y0 Z0) + self.fake.state["status"].update({"homed": True, "homed_axes": 7, "home_source": "manual"}) self.daemon = ScriptedDaemon(self.fake) self.poll = setup_dark.POLL_S setup_dark.POLL_S = 0.01 @@ -333,6 +336,10 @@ class BurnRunTests(unittest.TestCase): self.assertNotIn("lens_hall_edge_z_mm", start) self.assertEqual((start["lens_stop_below_steps"], start["lens_stop_above_steps"]), ("14", "20")) self.assertEqual(run.evidence["result"]["emission"]["hv_max"], 610) + # the head's place on record before the start, judged at X0 Y0 + self.assertEqual(run.evidence["head_at_start"], + {"pos": {"x": 0.0, "y": 0.0, "z": 0.0}, "homed_axes": 7, "home_source": "manual", + "judged": True}) # the witnesses read from here at every poll: each at its peak seen = run.evidence["emission_seen"] self.assertEqual((seen["laser_on_max"], seen["hv_max"]), (118, 431)) @@ -429,6 +436,27 @@ class BurnRunTests(unittest.TestCase): self.assertIn("the tube current never rose", str(cm.exception)) self.assert_found() + def test_a_head_off_its_home_fails_before_any_start(self): + # X and Y referenced by a manual home, and the head 12.5 mm from + # the stops: nothing starts, no controller lever is touched. + self.fake.state["status"]["pos"] = {"x": 12.5, "y": 0.0, "z": 0.0} + with self.assertRaises(Failed) as cm: + self.run_test("setup.focus") + self.assertIn("not at its home", str(cm.exception)) + self.assertEqual(self.daemon.order, []) + self.assertEqual(self.daemon.lever_posts(), []) + self.assert_found() + + def test_an_unreferenced_head_runs_on_the_operators_word(self): + # A fresh controller's counters-only position places nothing: the + # read is recorded, not judged, and the burn runs. + self.fake.state["status"].update({"homed": False, "homed_axes": 4, "home_source": "startup", + "pos": {"x": 12.5, "y": 0.0, "z": 0.0}}) + run = self.run_test("setup.focus") + self.assertEqual(run.evidence["head_at_start"]["judged"], False) + self.assertEqual(run.evidence["head_at_start"]["home_source"], "startup") + self.assertEqual(self.daemon.order, ["laser.focus"]) + def test_a_burn_the_test_itself_never_saw_fails_and_still_restores(self): # The burn's result reports a lit burn, and the test's own reads # of the two witnesses never leave zero: the burn is judged on