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_<name>.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.
This commit is contained in:
ScottW514
2026-10-17 19:06:48 -04:00
parent 84a231163a
commit a3211ba1ea
+13 -5
View File
@@ -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: