From a3211ba1eaad9b408caf52399723cc9f5cef77d8 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sat, 17 Oct 2026 19:06:48 -0400 Subject: [PATCH] forgetest: motion.jog-roundtrip's read-back judge allows half a step, so a one-step miss always fails The read-back judge of the soft-reset check compared MPos X in steps with the kernel's X count to one step, and the Y and Z judges allowed one step of MPos motion. One step is the smallest miss the read-back can make: the core's kill step for the pulse start that never came, which a position kept as the core counted it carries. MPos is reported to 0.001 mm, so the difference of two readings carries up to 0.21 step at x32 (0.001 mm at $100 = 213.333; 0.05 step at x8), and a one-step miss lands between 0.79 and 1.21 steps: on either side of a one-step bound, at random. The bound is now half a step for X, Y and Z: a correct read-back lands within 0.21 step, a one-step miss at 0.79 or more. The comment and the description say so. With the committed judges the parent of the read-back work fails the check on four counts, not two: the kernel is still running 1.0 s after the ^X (the parent writes no cnc/stop), the tail rate is 100% of the tick (the run ends at speed), the position is not read back (MPos X one step past the kernel), and the homed mask goes 4 -> 0. Proof: a compile and pyflakes 4.0.3 give no message on forgetest/suite/motion.py, with no undefined name in forgetest or tests. forgetest unit tests, python3 -B -m unittest discover -s tests -p test_.py, in a private PID namespace: test_manifest.py 39 OK, test_queue.py 15 OK, test_campaign.py 13 OK. Coverage lint on the tree manifest from scripts/manifest-from-tree.py (python3 -B -m forgetest.coverage --manifest ../tree-manifest.json --enforce): 0 uncovered, 0 empty, 0 host-tested row problems, 43 tests. Host dry runs of the whole test against the null-sink build of the grblHAL-glowforge default branch head ("serial_test: the scan's exempt arguments each match once in the sources"), configured as its CI configures it (-DGFMCODE_WAIT_S=5.0), with the head accelerometer, the kernel's state and bytes-played counter, and forgectrl stood in for and the step counters taken from the shipped stream: with a model kernel that ramps from the controller's cnc/stop write, the test passes, MPos X 3489.06 steps against 3489, tail 23% of the tick, mask 4 kept, drift 0.000 mm; with a model kernel that ramps as cnc.c does (the tick held 10 ms, then 5000 Hz off every 10 ms to the 900 Hz floor), the test passes, MPos X 3509.97 steps against 3510, tail 32%. Against the null-sink build of the parent of "Stream engine: a soft reset during motion gets the kernel's ramp, and the position is read back from the kernel's counters" (the commit "Test hooks: honored only in a null-sink run, through one gate"), under the ramp model, the read-back judge fails 5 runs in 5, MPos X one step past the kernel each time (1.06, 1.06, 1.09, 0.99 and 1.10 steps); with the one-step judge the same parent passed the read-back 3 runs in 5 (misses of 1.09, 0.95, 0.90, 0.98 and 1.09 steps). Every parent run also fails on the kernel still running 1.0 s after the ^X, the tail at 100%, and the mask 4 -> 0. The real counters and the machine's timing are read on the machine only. Acceptance: the change is inside the test's own function and its decorator, so the shared text of suite/motion.py does not change. The fingerprint of motion.jog-roundtrip moves, and no other test's: of the implementation hashes of the 43 catalog tests, before and after, only that one differs. The check runs on the bench in the next campaign as part of motion.jog-roundtrip. --- forgetest/forgetest/suite/motion.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/forgetest/forgetest/suite/motion.py b/forgetest/forgetest/suite/motion.py index e45cbd7..1f57776 100644 --- a/forgetest/forgetest/suite/motion.py +++ b/forgetest/forgetest/suite/motion.py @@ -202,7 +202,7 @@ def pacing(ctx): "Then a soft reset (^X) during a 60 mm G1 at F3000: the kernel ramps the motion " "down (its tick rate falls before the run ends) and leaves running with no " "underrun and no fault, the ring is empty after the reset, MPos matches the " - "kernel's step counters within a step after $X, the homed mask is kept, the sender " + "kernel's step counters within half a step after $X, the homed mask is kept, the sender " "is not told the position is lost, and a jog back by the MPos distance returns " "the kernel's X counter to its start within a step. " "No jog refused, every move returns to Idle, position drift within 0.05 mm, and " @@ -386,16 +386,24 @@ def jog_roundtrip(ctx): "ended at speed, not on the ramp" % (100 * tail["fraction"], f0, 1e3 * tail["window_s"])) if rec["ring_after"]: fails.append("the ring still holds %d bytes after the reset: the clear did not run" % rec["ring_after"]) - # The read-back is exact. The kernel counts X_DIR set as -X, so - # the sign of its X count is the one this +X move gave it. + # The read-back is exact, and the judge allows half a step. The + # smallest miss the read-back can make is one step: the core's + # kill step for the pulse start that never came, which a position + # kept as the core counted it carries. MPos is reported to + # 0.001 mm, so the difference of two readings carries up to 0.21 + # step at x32 (0.05 at x8): a correct read-back lands within + # that, a one-step miss at 0.79 step or more, and a one-step + # tolerance would let the miss land on either side of it. The + # kernel counts X_DIR set as -X, so the sign of its X count is + # the one this +X move gave it. if abs(kd[0]) < spm[0] or md[0] <= 0: fails.append("the move did not reach the kernel or MPos: kernel X %+d steps, MPos X %+.1f steps" % (kd[0], md[0])) - elif abs(abs(kd[0]) - md[0]) > 1.0: + elif abs(abs(kd[0]) - md[0]) > 0.5: fails.append("MPos X moved %.1f steps and the kernel played %d: the position was not read back " "from the kernel's counters" % (md[0], abs(kd[0]))) for a, name in ((1, "Y"), (2, "Z")): - if kd[a] or abs(md[a]) > 1.0: + if kd[a] or abs(md[a]) > 0.5: fails.append("%s moved under an X move: kernel %+d steps, MPos %+.1f steps" % (name, kd[a], md[a])) # The reference is kept. if h1 != h0: