diff --git a/forgetest/forgetest/baseline.py b/forgetest/forgetest/baseline.py index 0c19c7b..a29a6ee 100644 --- a/forgetest/forgetest/baseline.py +++ b/forgetest/forgetest/baseline.py @@ -133,6 +133,13 @@ GRBL_PORT_S = 30 # the Grbl port after the supervisor reports XY_STEPS_PER_MM = 53.333 # boards/glowforge.h (x8 microstepping) RETURN_MAX_MM = 100.0 # a displaced head is jogged back at most this far +# The counters count steps; the controller's own returns land within a +# tenth of a millimeter of where they started, not on the step. A +# difference under this is that quantization, not a leftover: the head is +# still put back, so nothing accumulates over a campaign, but the test +# that made it does not fail for it. X and Y only - Z is the lens, which +# the return never moves, so any Z difference is still a leftover. +POSITION_DEADBAND_MM = 0.1 # The XY microstep mode (the xy_microsteps setting: 8, 16 or 32; unset = # 8). The GRBL controller reads it at its start and derives its scale, @@ -198,6 +205,19 @@ def read_position(): return None +def position_quantized(was, now): + """True when two step-counter readings differ by no more than the + dead band on X and Y and not at all on Z: the step quantization of a + move that landed where it meant to, rather than a leftover. A cancel + that returns the head to the job start lands within a few hundredths + of a millimeter, which is a step or four, and whether that rounds to + the same integer is chance.""" + if was is None or now is None or now[2] != was[2]: + return False + return (abs(now[0] - was[0]) / XY_STEPS_PER_MM <= POSITION_DEADBAND_MM and + abs(now[1] - was[1]) / XY_STEPS_PER_MM <= POSITION_DEADBAND_MM) + + def read_ring_residue(): """Unplayed bytes queued in the kernel pulse ring: total written minus processed (the cnc/position byte counters), or None when unreadable. @@ -677,7 +697,12 @@ class Baseline: was = captured.get("position") now = read_position() if was is not None and now is not None and now != was and not self.cloud_mode(): - left.append(Leftover("position", now, was, self._return_head(was, now))) + act = self._return_head(was, now) + if position_quantized(was, now): + self.log("position: %s (expected %s) -> %s; inside the %.2f mm dead band, " + "not a leftover" % (now, was, act, POSITION_DEADBAND_MM)) + else: + left.append(Leftover("position", now, was, act)) was = captured.get("settings") if was: st, body = self.fc_get("/settings") diff --git a/forgetest/tests/test_baseline.py b/forgetest/tests/test_baseline.py index 1318432..f27f8ca 100644 --- a/forgetest/tests/test_baseline.py +++ b/forgetest/tests/test_baseline.py @@ -354,6 +354,31 @@ if __name__ == "__main__": unittest.main() +class PositionDeadbandTests(unittest.TestCase): + """The counters count steps and a controller's own return lands within + a few hundredths of a millimeter, not on the step: a difference that + small is quantization, not a leftover. Found on the bench reference, + where motion.lid-cancel-home returned the head twice, each within + 0.04 mm, and failed the hand-back on the four steps left over.""" + + def test_a_few_steps_of_xy_are_the_quantization(self): + self.assertTrue(baseline.position_quantized([0, 0, 0], [4, 0, 0])) + self.assertTrue(baseline.position_quantized([0, 0, 0], [-4, 4, 0])) + self.assertTrue(baseline.position_quantized([10, 20, 30], [10, 20, 30])) + + def test_past_the_dead_band_is_a_leftover(self): + over = int(baseline.POSITION_DEADBAND_MM * baseline.XY_STEPS_PER_MM) + 2 + self.assertFalse(baseline.position_quantized([0, 0, 0], [over, 0, 0])) + self.assertFalse(baseline.position_quantized([0, 0, 0], [0, over, 0])) + + def test_z_is_exact_because_the_return_never_moves_it(self): + self.assertFalse(baseline.position_quantized([0, 0, 0], [0, 0, 1])) + + def test_an_unreadable_reading_is_never_quantization(self): + self.assertFalse(baseline.position_quantized(None, [0, 0, 0])) + self.assertFalse(baseline.position_quantized([0, 0, 0], None)) + + class BaselineModeTests(BaselineTests): """The baseline against a fake forgectrl: what the mode in force owns. Reuses the fake sysfs tree of BaselineTests; only the new