From bef1bf99027439097ae31183b5ec8a90a7d2ce0b Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Tue, 13 Oct 2026 16:05:08 -0400 Subject: [PATCH] z_envelope_test: a move refused past the bed ships no step The homed cases asserted that a move past X max, Y max or X min draws ALARM:2, and the harness's own description said "before any motion", but nothing checked the motion. The catalog's motion.soft-limits held that half on the machine, by the kernel counters; it leaves the catalog with this harness as its proof. - Every session mirrors the shipped stream (GFSINK_DUMP) into its work directory; steps() counts the tick bytes with an X, Y or Z step bit, as ctlport_test.py does. - Each refused move is judged by the step count across it: unchanged, or the case fails with the ticks that shipped. - The move inside the bed must ship steps: the control that shows the count sees motion at all. Proof: z_envelope_test passes on a null-sink build of the grblHAL-glowforge working tree under Linux, every case including the three refusals before any motion and the control. Acceptance: none. The harness runs in grblHAL-glowforge's CI. --- scripts/bench/z_envelope_test.py | 38 ++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/scripts/bench/z_envelope_test.py b/scripts/bench/z_envelope_test.py index cd3647a..515e218 100644 --- a/scripts/bench/z_envelope_test.py +++ b/scripts/bench/z_envelope_test.py @@ -96,6 +96,8 @@ class Session: for k, v in (conf or {}).items(): f.write("%s = %s\n" % (k, v)) env["GFHOME_CONF"] = conf_path + self.dump = os.path.join(self.workdir, "stream.bin") + env["GFSINK_DUMP"] = self.dump if referenced: with open(os.path.join(self.workdir, "lens.home"), "w") as f: f.write("edge 0 3 3\n") @@ -180,6 +182,16 @@ class Session: fail("unexpected reply to %r: %r" % (gcode, reply)) return False + def steps(self): + """Step ticks shipped so far: a tick byte (bit 7 clear) with an X, Y + or Z step bit (0x25) set, as ctlport_test.py counts them.""" + try: + with open(self.dump, "rb") as f: + data = f.read() + except OSError: + return 0 + return sum(1 for b in data if not b & 0x80 and b & 0x25) + def recover(self): self.sock.sendall(b"\x18") self.read(1.0) @@ -286,15 +298,19 @@ def homed_cases(): m = re.search(r"MPos:(-?[\d.]+),(-?[\d.]+)", st) check(m and abs(float(m.group(1))) < 0.01 and abs(float(m.group(2))) < 0.01, "homed: X and Y sit at the origin", "homed: X/Y are not at the origin: %r" % st) - check(s.move("G90 G0 X%.1f" % (x_travel + 5)), "homed: X past the bed alarms", - "homed: a move past X max ran") - s.wait_idle() - check(s.move("G90 G0 Y%.1f" % (y_travel + 5)), "homed: Y past the bed alarms", - "homed: a move past Y max ran") - s.wait_idle() - check(s.move("G90 G0 X-1"), "homed: X past the near edge alarms", - "homed: a move past X min ran") - s.wait_idle() + + def refused_still(gcode, what): + # ALARM:2 before any motion: the shipped stream gains no step. + n0 = s.steps() + blocked = s.move(gcode) + n1 = s.steps() + check(blocked and n1 == n0, "homed: %s alarms before any motion" % what, + "homed: %s %s, %d step ticks shipped" % (what, "alarmed" if blocked else "ran", n1 - n0)) + s.wait_idle() + + refused_still("G90 G0 X%.1f" % (x_travel + 5), "a move past X max") + refused_still("G90 G0 Y%.1f" % (y_travel + 5), "a move past Y max") + refused_still("G90 G0 X-1", "a move past X min") reply = s.send("$J=G91X%.1fF1200" % (x_travel + 5), 1.5) check("error:15" in reply, "homed: a jog past the bed is refused with error 15", "homed: a jog past the bed was not refused with error 15: %r" % reply) @@ -302,9 +318,13 @@ def homed_cases(): # until an empty line (or a $ command) clears it: the sender's # acknowledgment at compatibility level 0. s.send("", 1.0) + n0 = s.steps() check(not s.move("G90 G0 X10 Y10"), "homed: a move inside the bed runs", "homed: a move inside the bed was blocked") s.wait_idle() + # The step count the refusals were judged by does see motion. + check(s.steps() > n0, "homed: the move inside the bed shipped steps", + "homed: the move inside the bed shipped no step: the stream dump sees no motion") # A settings write does not drop them: the driver re-applies the # mask after the core clears it. s.write_setting("$20=0")