diff --git a/forgetest/forgetest/suite/laser.py b/forgetest/forgetest/suite/laser.py index 08c438d..478dedb 100644 --- a/forgetest/forgetest/suite/laser.py +++ b/forgetest/forgetest/suite/laser.py @@ -313,15 +313,17 @@ def read_gaps(samples, key, t_end=None): def rapid_place(stretch, x0): """Where the head's reports place the rapid after the M5, judged over - the armed part of the dark stretch alone: its samples up to the first - disarmed one, that one included. Returns (i_out, i_back, i_closed): - the first sample with the head at least 19.5 mm out, the first after - it with the head back at x0 (within 0.05 mm), and the first disarmed - sample; each None when there is none. A window that closes before - the head is back leaves i_back None: the samples after the close - never place the rapid inside the window.""" + the armed part of the dark stretch alone: its samples before the first + disarmed one. Returns (i_out, i_back, i_closed): the first sample with + the head at least 19.5 mm out, the first after it with the head back + at x0 (within 0.05 mm), and the first disarmed sample; each None when + there is none. A window that closes before the head is back leaves + i_back None: only a sample that read the window open places the head + back, and sample_to_disarm reads its X before its armed flag, so the + head was back with the window open. The first disarmed sample does + not tell whether the head came back before the close or after it.""" i_closed = next((i for i, s in enumerate(stretch) if not s["armed"]), None) - part = stretch if i_closed is None else stretch[:i_closed + 1] + part = stretch if i_closed is None else stretch[:i_closed] i_out = next((i for i, s in enumerate(part) if s.get("x") is not None and s["x"] - x0 >= 19.5), None) i_back = (next((i for i in range(i_out + 1, len(part)) if part[i].get("x") is not None and abs(part[i]["x"] - x0) < 0.05), None) @@ -331,14 +333,15 @@ def rapid_place(stretch, x0): def sample_to_disarm(ctx, g, timeout=20.0, sample_hz=8): """Sample until the armed window closes, or the timeout, with the - controller's state and machine X beside each sample ("x"). A G4 dwell - reports Idle, so Idle cannot end this; the window closing at the - program end does, after two more kernel windows and the read gap - allowed: the LASER_ON count for the window in flight at the close - latches up to one window later and is held through the next, so the - sampling runs on until that count has been read. Returns the samples - served and the time the sampling stopped, the trail's end for the - read rule.""" + controller's state and machine X beside each sample ("x"), read just + before the sample: a sample that reads the window open read its X + with the window open (rapid_place). A G4 dwell reports Idle, so Idle + cannot end this; the window closing at the program end does, after + two more kernel windows and the read gap allowed: the LASER_ON count + for the window in flight at the close latches up to one window later + and is held through the next, so the sampling runs on until that + count has been read. Returns the samples served and the time the + sampling stopped, the trail's end for the read rule.""" samples = [] period = 1.0 / sample_hz t0 = time.time() @@ -346,8 +349,8 @@ def sample_to_disarm(ctx, g, timeout=20.0, sample_hz=8): t_closed = None while time.time() < end: ctx.checkpoint() - smp = sample(ctx) rep = g.status_report() + smp = sample(ctx) if smp: smp["gstate"] = rep.get("state") smp["x"] = (rep.get("MPos") or (None,))[0] @@ -474,8 +477,10 @@ def wait_disarm(ctx, timeout): def emission_witness(ctx): ev = ctx.evidence # The rapid after the M5 (G91 is modal from the square): out over the - # first side and back, then the program end. - rapid_tail = ["G0 X20", "G4 P2.5", "G0 X-20", "G4 P0.5", "G90", "M2"] + # first side and back, then the program end. The head stands back for + # 1 s before the M2, so that a sample of a slow sampling loop still + # reads it back with the window open (rapid_place). + rapid_tail = ["G0 X20", "G4 P2.5", "G0 X-20", "G4 P1", "G90", "M2"] with ctx.grbl() as g, LiveJob(ctx, g): prepare(ctx, g) diff --git a/forgetest/tests/test_laser_dwell.py b/forgetest/tests/test_laser_dwell.py index 48a48f8..e01d846 100644 --- a/forgetest/tests/test_laser_dwell.py +++ b/forgetest/tests/test_laser_dwell.py @@ -166,7 +166,8 @@ class RapidTailTests(unittest.TestCase): class Grbl: def status_report(self): - return {"state": "Idle", "MPos": (0.0, 0.0, 0.0)} + # the time of the read, as the head's X + return {"state": "Idle", "MPos": (clock.now, 0.0, 0.0)} saved = laser.time, laser.sample self.addCleanup(setattr, laser, "time", saved[0]) self.addCleanup(setattr, laser, "sample", saved[1]) @@ -177,6 +178,9 @@ class RapidTailTests(unittest.TestCase): self.assertTrue(all(s["armed"] for s in tail[:-1])) self.assertGreaterEqual(t_end - closed["t"], 2 * laser.EMISSION_WINDOW_S + laser.EMISSION_READ_GAP_S) self.assertFalse(covered(laser.read_gaps(tail, "emission", t_end))) + # each sample's X is read before its armed flag: an X that places + # the head was read with the window open + self.assertTrue(all(s["x"] < s["t"] for s in tail)) def place_trail(xs, closes_at): @@ -195,6 +199,10 @@ class RapidPlaceTests(unittest.TestCase): # the window closes in the dwell at +20 mm and the head comes back # after the close: the samples past the close do not place it self.assertEqual(laser.rapid_place(place_trail(self.XS, 4), 0.0), (2, None, 4)) + # the window reads closed at the first sample with the head back: + # that sample does not tell whether the head was back before the + # close, so it does not place it + self.assertEqual(laser.rapid_place(place_trail(self.XS, 6), 0.0), (2, None, 6)) if __name__ == "__main__":