diff --git a/forgetest/forgetest/bench.py b/forgetest/forgetest/bench.py index 67c935d..c283f9b 100644 --- a/forgetest/forgetest/bench.py +++ b/forgetest/forgetest/bench.py @@ -247,8 +247,11 @@ TOOLS = [ {"id": "z-envelope-test", "title": "Z envelope harness", "script": "z_envelope_test.py", "safety": "dry", "where": "host", "ported": False, "args": [], "desc": "The Z soft limit belongs to the driver, not to $20: an unreferenced Z is collapsed to where the " - "lens stands, and neither a $20 nor a $132 write frees it. A CI harness (the grblHAL repo): needs " - "the host-built null-sink controller, not the machine, so it is not a bench-page tool."}, + "lens stands, and neither a $20 nor a $132 write frees it. A referenced lens (forgectrl's marker " + "and the lens settings) opens it to the window the settings hold, the fallback or the focus " + "card's stops: the ends of the reach run, two half-steps past either end alarms. A CI harness " + "(the grblHAL repo): needs the host-built null-sink controller, not the machine, so it is not a " + "bench-page tool."}, {"id": "raster-dry", "title": "Dry top-speed raster per XY microstep mode", "script": "raster_dry.py", "safety": "dry", "where": "board", "ported": True, "args": [_arg("modes", "choice", "8 16 32", "the modes to run, in order", ["8", "16", "32", "8 16 32"])], diff --git a/forgetest/forgetest/suite/commission_sheet.py b/forgetest/forgetest/suite/commission_sheet.py index fd66f92..6725e92 100644 --- a/forgetest/forgetest/suite/commission_sheet.py +++ b/forgetest/forgetest/suite/commission_sheet.py @@ -21,7 +21,7 @@ from ..catalog import test from .commission_dark import run_check, Restore SHEET_COVERS = [("forgectrl", "src/wizlive.*"), ("forgectrl", "src/wizrun.h"), - ("forgectrl", "src/sheet.*"), + ("forgectrl", "src/lens.*"), ("forgectrl", "src/sheet.*"), ("forgectrl", "src/font_hershey.*"), ("forgectrl", "src/jobstream.*"), ("forgectrl", "src/curverec.*"), ("forgectrl", "src/wizdark.*"), ("forgectrl", "src/wiz.*"), ("forgectrl", "src/commission.*"), @@ -71,6 +71,22 @@ def preview_ok(ctx, wid): ctx.check(st == 200 and re.search(rb"^M[34] S\d", body or b"", re.M) is not None, "no program for %s (%s)", wid, st) ctx.evidence["program_lines"] = body.count(b"\n") + # Every Z the program commands is a height the head reaches in the + # window the machine holds now: the focus card's own numbers once it + # has them, and the conservative fallback on a head whose travel is + # not known yet. The controller refuses a Z outside it with ALARM:2, + # and it refuses it while the body is still burning, so the card dies + # where the parser is and not where the bad line is. + lens = (fc.status() or {}).get("lens") or {} + lo, hi = lens.get("reach_min"), lens.get("reach_max") + zs = [float(m) for m in re.findall(rb"^G0 Z(-?[0-9.]+)", body or b"", re.M)] + ctx.evidence.setdefault("program_z", {})[wid] = { + "z": zs, "reach": [lo, hi], "stops_found": lens.get("stops_found")} + ctx.check(lo is not None and hi is not None, "/status carries no lens reach: %s", lens) + if lo is not None and hi is not None: + out = [z for z in zs if z < lo - 0.01 or z > hi + 0.01] + ctx.check(not out, "%s commands Z outside the lens window %.2f..%.2f: %s", + wid, lo, hi, out) def emission_ok(ctx, last): @@ -89,6 +105,61 @@ def file_card(ctx, wid): ev.setdefault("cards", {})[wid] = {k: ev.pop(k) for k in CARD_EVIDENCE if k in ev} +# The lens settings: the head's numbers, which a fresh machine does not +# have. Cleared before the cards so the run is a fresh machine's. +LENS_SETTINGS = CARD_SETTINGS[:3] +FALLBACK_WINDOW = (10, 12) + + +def fresh_lens(ctx): + """The lens as a fresh machine has it: the head's numbers unknown, so + the frame runs in the fallback window and the focus card has to find + the stops and write them before its ladder burns. The machine's own + numbers come back with the restore at the end of the run.""" + fc = ctx.forgectrl + for k in LENS_SETTINGS: + st, _ = fc.post("/settings", params={k: ""}) + ctx.check(st == 200, "clearing %s -> %s", k, st) + lens = (fc.status() or {}).get("lens") or {} + ctx.evidence["lens_fresh"] = lens + ctx.check(lens.get("stops_found") is False and (lens.get("below"), lens.get("above")) == FALLBACK_WINDOW, + "the lens window is not the fallback after the clear: %s", lens) + + +def focus_window_ok(ctx): + """The window the focus card burned its ladder in is the one it wrote + before its controller started, and the one the machine holds now: the + result's window is the stops it found (or the fallback), the settings + hold that window, every ladder height lies in its reach, and the + program served now agrees with /status.""" + fc = ctx.forgectrl + r = ((ctx.evidence.get("cards") or {}).get("laser.focus") or {}).get("result") or {} + w, stops = r.get("window") or {}, r.get("stops") or {} + s = fc.settings() or {} + held = (str(s.get("lens_stop_below_steps")), str(s.get("lens_stop_above_steps"))) + ctx.check(held == (str(w.get("below")), str(w.get("above"))), + "the settings hold the window %s, the ladder ran in %s/%s", held, w.get("below"), w.get("above")) + if stops.get("found"): + ctx.check((w.get("below"), w.get("above")) == (stops.get("below"), stops.get("above")), + "the ladder's window %s is not the stops found %s", w, stops) + else: + ctx.check((w.get("below"), w.get("above")) == FALLBACK_WINDOW, + "stops not found (%s), but the ladder's window is %s", stops.get("why"), w) + zs = r.get("ladder_z") or [] + lo, hi = w.get("reach_min"), w.get("reach_max") + out = [z for z in zs if lo is None or hi is None or z < lo - 0.01 or z > hi + 0.01] + ctx.check(len(zs) == 12 and not out, "ladder heights outside the reach %s..%s: %s", lo, hi, out or zs) + ev = ctx.evidence + before = dict(ev.get("program_z") or {}).get("laser.focus") + preview_ok(ctx, "laser.focus") + ev["focus_window"] = {"window": w, "stops": stops, "ladder_z": zs, + "settings": {k: s.get(k) for k in LENS_SETTINGS}, + "program_z_before": before, + "program_z_after": (ev.get("program_z") or {}).get("laser.focus"), + "preview_bytes": ev.pop("preview_bytes", None), + "program_lines": ev.pop("program_lines", None)} + + def thickness_answer(ctx): """The thickness the placement is answered with, in the machine's units (the number prompts read ui_units), and the millimeters the @@ -167,15 +238,22 @@ def burn(ctx, wid, answers, want): "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."], - description="One run over the sheet's seven wizards on one piece of wood. " + description="One run over the sheet's seven wizards on one piece of wood, as a fresh " + "machine runs them: the three lens settings are cleared first, so the frame " + "runs in the fallback window and the focus card has to find the stops and write " + "them before its ladder burns. " "POST /wiz/sheet.place/start references the lens, takes the controller in " "loopback posture, jogs +X 10 and -X 10, and sets the origin at the head " "(Full sheet; the thickness answered in the machine's units, 3.2 mm or " "0.125 in). Then, each after the press and each with its preview " - "and program served by GET /wiz/sheet.svg and /wiz/sheet.gcode: the frame " + "and program served by GET /wiz/sheet.svg and /wiz/sheet.gcode, every Z the " + "program commands inside the reach /status reports: the frame " "(180 x 130 mm) and the header band at the mark dose; the focus card (the " - "lens homed on its bottom stop, the hall edge counted, twelve lines down the " - "travel; the pick is line 11, the thickness kept); the floor card (twelve " + "lens on its hall edge, the stops found by the head accelerometer and written " + "as the window before the controller starts, twelve lines from the top of the " + "window to the bottom; the pick is line 11, the thickness kept; after it the " + "settings, the result's window, and the served program must hold the window " + "the ladder ran in); the floor card (twelve " "rungs from 2 to 24 percent with the floor and the curve off for the job; the " "pick is 8, the floor written is 10); the dose card (seven rungs sampled at " "25 Hz, the curve fit on 20 s of dark); the corner card (five gammas reloaded " @@ -191,6 +269,9 @@ def sheet(ctx): "each. The bench presses when the button lights.") place(ctx) with Restore(ctx, CARD_SETTINGS): + fresh_lens(ctx) 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") + if wid == "laser.focus": + focus_window_ok(ctx) + ctx.log("PASS: the sheet's seven wizards ran on one piece as a fresh machine's; settings restored") diff --git a/forgetest/tests/test_commission_sheet.py b/forgetest/tests/test_commission_sheet.py index f3def76..1471d05 100644 --- a/forgetest/tests/test_commission_sheet.py +++ b/forgetest/tests/test_commission_sheet.py @@ -17,7 +17,44 @@ from forgetest.runner import Context, Run # noqa: E402 from forgetest.suite import commission_sheet # noqa: E402 LIVE = ["sheet.frame", "laser.focus", "laser.floor", "laser.dose-curve", "laser.corner", "cooling.flow-load"] +# The focus card's program is read again after it ran: the served ladder +# must then lie in the window the card wrote. +PREVIEWS = ["sheet.frame", "laser.focus", "laser.focus", "laser.floor", "laser.dose-curve", "laser.corner", + "cooling.flow-load"] EMISSION = {"hv_max": 610, "laser_on_samples": 900, "thermopile_delta": 600, "lit_s": 30.0} +SPM = 2.922 # the lens screw, half-steps per mm ($102) +EDGE_DEFAULT = 3.35 + + +def lens_block(s): + """The /status lens block as the daemon builds it from the settings: a + stop count from 1 to 40 stands, anything else is the fallback.""" + def count(key, fallback): + try: + n = float(s.get(key, "")) + except ValueError: + return fallback, False + return (int(n), True) if 1 <= n <= 40 else (fallback, False) + try: + edge = float(s.get("lens_hall_edge_z_mm", "")) + except ValueError: + edge = EDGE_DEFAULT + below, a = count("lens_stop_below_steps", 10) + above, b = count("lens_stop_above_steps", 12) + return {"edge_z": round(edge, 2), "below": below, "above": above, "stops_found": a and b, + "reach_min": round(edge - below / SPM, 2), "reach_max": round(edge + above / SPM, 2)} + + +def ladder_z(edge, below, above): + """The focus ladder the daemon builds: twelve whole half-steps from + the top of the window to the bottom.""" + return [round(edge + round(above - (above + below) * i / 11) / SPM, 2) for i in range(12)] + + +# The window the focus card finds and writes before its controller starts. +STOPS = {"lens_stop_below_steps": "14", "lens_stop_above_steps": "20"} +FOCUS_WINDOW = {"below": 14, "above": 20, "reach_min": round(EDGE_DEFAULT - 14 / SPM, 2), + "reach_max": round(EDGE_DEFAULT + 20 / SPM, 2)} # Each wizard as the daemon runs it: its prompts in order (id, kind, # options) and the result it ends with. A "jog" prompt repeats until the @@ -37,7 +74,9 @@ SCRIPT = { "steps_per_mm": 2.922, "max_height_mm": 13.81, "focus_range_mm": {"min": 2.17, "max": 13.81}, "stops": {"found": True, "below": 14, "above": 20, "contact_below": 16, - "contact_above": 22, "why": ""}, "emission": EMISSION}), + "contact_above": 22, "why": ""}, + "window": dict(FOCUS_WINDOW), "ladder_z": ladder_z(EDGE_DEFAULT, 14, 20), + "emission": EMISSION}), "laser.floor": ([("floor-arm", "continue", ["Continue"]), ("floor-pick", "choice", ["2", "4", "6", "8", "None"])], {"faintest_density": 8.0, "floor_density": 10.0, "emission": EMISSION}), @@ -50,10 +89,11 @@ SCRIPT = { {"lit_s": 30.0, "dose_raw_s": 12000, "peak_c": 0.72, "k_density": 2.14e-5, "k_cw": 2.78e-5, "emission": EMISSION}), } -# What each card writes when it ends. +# What a card writes when it starts (the focus card's window, before its +# controller starts) and when it ends. +START_WRITES = {"laser.focus": dict(STOPS)} WRITES = { - "laser.focus": {"lens_hall_edge_z_mm": "6.96", "lens_stop_below_steps": "14", - "lens_stop_above_steps": "20"}, + "laser.focus": {"lens_hall_edge_z_mm": "6.96"}, "laser.floor": {"laser_floor_density": "10"}, "laser.dose-curve": {"laser_dose_curve": "10:0.44,100:100"}, "laser.corner": {"laser_corner_gamma": "1.5"}, @@ -115,9 +155,11 @@ class ScriptedDaemon: self.versions = {} self.answers = {} # wid -> [values] self.order = [] # the wizards as started + self.settings_at_start = {} # wid -> the settings as the wizard found them self.previews = [] self.queue = [] self.seq = 0 + self.stray_z = None # a Z the served program commands beyond the reach, when set fake.on_get = self.on_get fake.on_post = self.on_post @@ -130,6 +172,10 @@ class ScriptedDaemon: return True def on_get(self, path, q): + s = self.fake.state["settings"] + if path == "/status": + # the lens block follows the settings, as the daemon's does + return 200, dict(self.fake.state["status"], lens=lens_block(s)) if path == "/wiz/dark": return 200, dict(self.dark) if path == "/wiz": @@ -140,8 +186,18 @@ class ScriptedDaemon: return 404, {"error": "no such card"} return 200, b"", "image/svg+xml" if path == "/wiz/sheet.gcode": - # the frame and the text are M4 at the mark dose, as the daemon serves them - return 200, b"; mock\nM4 S400\nG0 X60 Y42\nG1 X120 Y42 F3000\nM5\n", "text/plain" + # the frame and the text are M4 at the mark dose, as the daemon + # serves them; the head's Z is the edge, the tail goes back to + # it, and the focus card's ladder spans the window the settings + # hold now + lens = lens_block(s) + lines = ["; mock", "G0 Z%.2f" % lens["edge_z"], "M4 S400", "G0 X60 Y42", "G1 X120 Y42 F3000"] + if q.get("card") == "laser.focus": + lines += ["G0 Z%.2f" % z for z in ladder_z(lens["edge_z"], lens["below"], lens["above"])] + if self.stray_z is not None: + lines.append("G0 Z%.2f" % self.stray_z) + lines += ["M5", "G0 Z%.2f" % lens["edge_z"], "G0 X0 Y0", "M2"] + return 200, ("\n".join(lines) + "\n").encode(), "text/plain" return None def on_post(self, path, form): @@ -151,6 +207,8 @@ class ScriptedDaemon: if wid not in SCRIPT: return 404, {"error": "no such wizard"} self.order.append(wid) + self.settings_at_start[wid] = dict(s) + s.update(START_WRITES.get(wid, {})) self.queue = list(SCRIPT[wid][0]) self.dark.update({"id": wid, "running": True, "phase": "starting", "result": None, "error": ""}) self.open_next() @@ -199,7 +257,7 @@ class SheetRunTests(unittest.TestCase): self.assertEqual(d.answers["laser.focus"], ["Continue", "11", "Keep"]) self.assertEqual(d.answers["laser.floor"], ["Continue", "8"]) self.assertEqual(d.answers["laser.corner"], ["Continue", "1.50"]) - self.assertEqual(d.previews, LIVE) + self.assertEqual(d.previews, PREVIEWS) cards = run.evidence["cards"] self.assertEqual(sorted(cards), sorted(["sheet.place"] + LIVE)) for wid in LIVE: @@ -208,6 +266,24 @@ class SheetRunTests(unittest.TestCase): self.assertNotIn("emission", run.evidence) self.assertEqual(cards["sheet.place"]["result"]["origin_x"], 100) self.assertEqual(run.evidence["settings_before"], FOUND) + # A fresh machine's run: the lens settings were gone before the + # frame, the frame's program ran in the fallback window, the focus + # card's window was in the settings when the floor card started, + # and the window check filed what it compared. + for k in commission_sheet.LENS_SETTINGS: + self.assertNotIn(k, d.settings_at_start["sheet.frame"], k) + self.assertEqual(run.evidence["lens_fresh"]["stops_found"], False) + self.assertEqual(run.evidence["program_z"]["sheet.frame"]["reach"], [-0.07, 7.46]) + floor_start = d.settings_at_start["laser.floor"] + self.assertEqual({k: floor_start[k] for k in commission_sheet.LENS_SETTINGS}, + {"lens_hall_edge_z_mm": "6.96", "lens_stop_below_steps": "14", "lens_stop_above_steps": "20"}) + fw = run.evidence["focus_window"] + self.assertEqual(fw["window"], FOCUS_WINDOW) + self.assertEqual(fw["settings"]["lens_stop_below_steps"], "14") + self.assertEqual(len(fw["ladder_z"]), 12) + self.assertEqual(fw["program_z_before"]["reach"], [-0.07, 7.46]) # the fallback ladder + self.assertEqual(fw["program_z_after"]["reach"], [2.17, 13.8]) # the written window at the new edge + self.assertEqual(len(fw["program_z_after"]["z"]), 14) # head, twelve lines, tail for k, v in FOUND.items(): self.assertEqual(self.fake.state["settings"][k], v, k) # restored @@ -261,6 +337,44 @@ class SheetRunTests(unittest.TestCase): finally: SCRIPT["laser.floor"] = (SCRIPT["laser.floor"][0], dict(SCRIPT_DARK, emission=EMISSION)) + def test_a_focus_window_the_settings_do_not_hold_fails_the_run(self): + # The defect the bench found: the focus card burned its ladder over + # the stops it found while the controller's Z limit stood on the + # settings, which did not hold them. A daemon whose focus result + # names a window the settings do not hold fails the run before the + # floor card, and the settings still go back. + from forgetest.runner import Failed + saved = dict(START_WRITES["laser.focus"]) + START_WRITES["laser.focus"] = {} + try: + t = catalog.load_suite()["commission.sheet"] + run = Run("test", t.id, t.title) + run.unattended = True + with self.assertRaises(Failed) as cm: + t.fn(Context(run, None, t)) + self.assertIn("the settings hold", str(cm.exception)) + self.assertEqual(self.daemon.order, ["sheet.place", "sheet.frame", "laser.focus"]) + for k, v in FOUND.items(): + self.assertEqual(self.fake.state["settings"][k], v, k) + finally: + START_WRITES["laser.focus"] = saved + + def test_a_program_z_beyond_the_reach_fails_before_the_burn(self): + # A served program that commands a Z the lens does not reach is + # refused before the card starts: nothing burns. + from forgetest.runner import Failed + self.daemon.stray_z = 10.6 + t = catalog.load_suite()["commission.sheet"] + run = Run("test", t.id, t.title) + run.unattended = True + with self.assertRaises(Failed) as cm: + t.fn(Context(run, None, t)) + self.assertIn("outside the lens window", str(cm.exception)) + self.assertIn("10.6", str(cm.exception)) + self.assertEqual(self.daemon.order, ["sheet.place"]) + for k, v in FOUND.items(): + self.assertEqual(self.fake.state["settings"][k], v, k) + if __name__ == "__main__": unittest.main() diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc index c0fd75b..1b4f3be 100644 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc +++ b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc @@ -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 = "a9e7073b2325c8c9d89d490fe6d067abfac22da5" -PV = "0.1.17" +SRCREV = "f8ddb17762ff3eb194c01e13579793d712ce70fc" +PV = "0.1.18" diff --git a/scripts/bench/z_envelope_test.py b/scripts/bench/z_envelope_test.py index af60ab3..65c3d3a 100644 --- a/scripts/bench/z_envelope_test.py +++ b/scripts/bench/z_envelope_test.py @@ -25,6 +25,19 @@ no root) and drives it over TCP: does not free Z either 6. X and Y are still free after both writes +A referenced lens opens the envelope to the window the shared settings +hold, the same keys the daemon's wizards build their Z from. forgectrl +leaves the lens on its hall edge and a marker in the state directory +before the controller starts; the driver takes it as it loads its +settings and places the edge on its step grid: + + 7. the marker with the edge alone: Z sits at the edge's grid step, the + fallback window (10 below, 12 above) runs to its ends, and two + half-steps past either end alarms + 8. the marker with the focus card's stops (14 below, 20 above): the + wider window runs to its ends, two half-steps past either alarms + 9. a stop count out of range (41) falls back on its side alone + The binary keeps its settings in EEPROM.DAT in the working directory, so each run starts from defaults in a temporary directory and leaves nothing behind. @@ -53,13 +66,24 @@ def fail(msg): class Session: - """One null-sink controller process with its own settings store.""" + """One null-sink controller process with its own settings store. + `conf` (key: value) is the shared config the driver reads; with + `referenced` the lens marker forgectrl leaves is in the state + directory, so the driver takes the lens reference at its start.""" - def __init__(self): + def __init__(self, conf=None, referenced=False): self.workdir = tempfile.mkdtemp(prefix="z-envelope-") env = dict(os.environ, GF_STATE_DIR=self.workdir, FFLOG_STDERR="1") for key in ("GFSINK", "GF_SWITCH_FILE", "GF_VERDICT_FILE"): env.pop(key, None) + conf_path = os.path.join(self.workdir, "forgefirm.conf") + with open(conf_path, "w") as f: + for k, v in (conf or {}).items(): + f.write("%s = %s\n" % (k, v)) + env["GFHOME_CONF"] = conf_path + if referenced: + with open(os.path.join(self.workdir, "lens.home"), "w") as f: + f.write("edge 0 3 3\n") self.proc = subprocess.Popen([BIN, "-p", str(PORT)], cwd=self.workdir, env=env, stdout=subprocess.DEVNULL, @@ -113,6 +137,23 @@ class Session: m = re.search(r"<(\w+)", self.send("?")) return m.group(1) if m else "?" + def mpos_z(self): + """The reported machine Z (no work offset is set in a session).""" + m = re.search(r"[MW]Pos:(-?[\d.]+),(-?[\d.]+),(-?[\d.]+)", self.send("?")) + if not m: + fail("no position in the status report") + return float(m.group(3)) + + def wait_idle(self, timeout=15.0): + """A move that ran plays out before the next line is judged: the + soft limit on a moving machine holds first and alarms after.""" + end = time.time() + timeout + while time.time() < end: + if self.state() == "Idle": + return + time.sleep(0.1) + fail("the controller did not return to Idle") + def move(self, gcode): """Run one move. Returns True when the soft limit blocked it. An alarm needs a reset and an unlock before the next move.""" @@ -160,6 +201,60 @@ def free_in_xy(s, when): check(ran, "X and Y free %s" % when, "an X or Y move was blocked %s" % when) +def window_holds(s, steps, below, above, what): + """The envelope of a referenced lens whose edge sits on grid step + `steps`: the ends of the reach (`below` and `above` half-steps from + the edge) run, two half-steps past either end alarms. The controller + keeps a half-step of slack beyond the reach, so one past is not + judged here; two is outside on every head.""" + spm = float(s.setting("$102")) + top, bottom = (steps + above) / spm, (steps - below) / spm + s.wait_idle() + check(not s.move("G0 Z%.3f" % top), "%s: the top of the reach, Z %.3f, runs" % (what, top), + "%s: Z %.3f, the top of the reach, was blocked" % (what, top)) + s.wait_idle() + check(not s.move("G0 Z%.3f" % bottom), "%s: the bottom of the reach, Z %.3f, runs" % (what, bottom), + "%s: Z %.3f, the bottom of the reach, was blocked" % (what, bottom)) + over, under = (steps + above + 2) / spm, (steps - below - 2) / spm + s.wait_idle() + check(s.move("G0 Z%.3f" % over), "%s: two half-steps over the top, Z %.3f, alarms" % (what, over), + "%s: Z %.3f, two half-steps over the top, ran" % (what, over)) + s.wait_idle() + check(s.move("G0 Z%.3f" % under), "%s: two half-steps under the bottom, Z %.3f, alarms" % (what, under), + "%s: Z %.3f, two half-steps under the bottom, ran" % (what, under)) + + +def referenced_cases(): + """The lens referenced by forgectrl's marker: the envelope is the + window the shared settings hold, as the daemon's wizards read it.""" + edge = 3.35 + conf = {"lens_hall_edge_z_mm": "%.2f" % edge} + s = Session(conf=conf, referenced=True) + try: + spm = float(s.setting("$102")) + steps = int(round(edge * spm)) + z = s.mpos_z() + check(abs(z - steps / spm) < 0.002, + "referenced: Z sits at the edge's grid step, %.3f" % z, + "referenced: Z reads %.3f, not the edge's grid step %.3f" % (z, steps / spm)) + window_holds(s, steps, 10, 12, "the fallback window") + free_in_xy(s, "with the lens referenced") + finally: + s.close() + + s = Session(conf=dict(conf, lens_stop_below_steps="14", lens_stop_above_steps="20"), referenced=True) + try: + window_holds(s, steps, 14, 20, "the focus card's window") + finally: + s.close() + + s = Session(conf=dict(conf, lens_stop_below_steps="41", lens_stop_above_steps="20"), referenced=True) + try: + window_holds(s, steps, 10, 20, "a below count out of range") + finally: + s.close() + + def main(): if not os.path.isfile(BIN): fail("no controller binary at %s" % BIN) @@ -189,6 +284,7 @@ def main(): free_in_xy(s, "after the settings writes") finally: s.close() + referenced_cases() print("PASS z_envelope_test")