From e1d1c1b84eb79d3fe3843841deafbf7339fc5554 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sat, 17 Oct 2026 19:10:40 -0400 Subject: [PATCH] forgetest: motion.jog-roundtrip's ramp comment follows the kernel's stepped ramp The soft-reset check's comment described the kernel's ramp with the continuous formula: (f0^2 - 900^2) / (2 a) ticks, 12687 over 223 ms at the x32 tick, and a part-way run-out at f0 * sqrt(1 - 2 (a / f0) Q). The kernel's ramp is stepped (cnc.c, ramp_update_tasklet_fn): the tick holds for the first 10 ms after the stop, then every 10 ms drops by ramp_rate / 100 until it reaches the 900 Hz floor, which plays 10 ms before the stop. At 112640 Hz and 500000 Hz/s that is 5000 Hz off every 10 ms: 24 intervals, 240 ms, about 13270 ticks, 118 ms of the tick. The comment now says so, with the figures that follow: the rate at a run-out is 24% of the tick with 110 ms of queue left, 38% with 100 ms, 51% with 90 ms; the tail's mean over the 25 to 30 ms the poll samples is 32%, 45% and 56% (61% over the 50 ms window bound), not 21% to 56%; the 70% bound is 14 points above the slowest ramped tail and 30 below a run-out; the time to leave running is the 100 ms hold plus the run's end on the ramp, about 240 ms with 100 ms left, 270 ms with 110 ms, 340 ms at the floor. The evidence's ramp_ticks and ramp_s stay the continuous figures, and the comment says they are, about 4% and 7% under the stepped ramp's. No judge changes: the 70% bound, the 25 ms window and the 50 ms gap stay. The Alarm wait's failure text names a sampling stall: when the 5 ms sampling loop runs past 2 s after the ^X, the wait has no time to read a state, and the failure read "not in Alarm within 2 s (state None)". It now says the state is unread because the sampling loop ran past the 2 s. The condition is the same; the text is the change. 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"), with the machine side stood in for as before: with a model kernel that ramps from the controller's cnc/stop write, the test passes, MPos X 3502.07 steps against 3502, tail 22% of the tick; with a model kernel that ramps as cnc.c does, 5000 Hz off every 10 ms from 110 ms of queue, the test passes, MPos X 3496.95 steps against 3497, tail 33% of the tick, the figure the comment gives for a run-out from 110 ms of queue. Acceptance: the comment and the message text are inside the test's own function, 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. --- forgetest/forgetest/suite/motion.py | 47 +++++++++++++++++------------ 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/forgetest/forgetest/suite/motion.py b/forgetest/forgetest/suite/motion.py index adccd77..3a661ce 100644 --- a/forgetest/forgetest/suite/motion.py +++ b/forgetest/forgetest/suite/motion.py @@ -284,7 +284,8 @@ def jog_roundtrip(ctx): if not (state or "").startswith("Alarm"): if (state or "").startswith("Idle"): ctx.fail("the move had ended before the ^X: the controller is Idle with no alarm") - ctx.fail("the controller was not in Alarm within 2 s of the ^X (state %s)", state) + ctx.fail("the controller was not in Alarm within 2 s of the ^X (state %s)", + state or "unread: the 5 ms sampling loop ran past the 2 s") # The read-back ends inside a second of the reset, and the core's # re-init flushes what it has read: the unlock goes after both. @@ -327,27 +328,35 @@ def jog_roundtrip(ctx): # The ramp: the kernel decelerated. Nothing but its ramp lowers the # tick rate once a run is up to speed, so the bytes it played over # the run's last 25 to 50 ms, against the tick, say whether the run - # ended at speed or decelerated. The queue at cnc/stop is about 110 ms of - # ticks: the ring holds 200 ms and the producer leads by 10 ms, and - # the core's alarm hold plays 100 ms of it at the tick before the - # driver's stop. The ramp to the kernel's 900 Hz floor consumes - # (f0^2 - 900^2) / (2 a) ticks, 112.6 ms of the tick (12687 ticks - # at the x32 tick of 112640 Hz and its 500000 Hz/s ramp; the ramp - # scales with the tick, so every mode gives the same fractions). - # So the run ends at the floor, or out of data part way down the - # ramp at f0 * sqrt(1 - 2 (a / f0) Q): 15% of the tick with 110 ms - # left, 33% with 100 ms, 45% with 90 ms. The tail's mean is that - # plus a W / 2: at most 56% of the tick over 50 ms with 90 ms left, - # 21% to 39% over 25 ms with 100 to 110 ms left. A run that ends at - # the tick (a run-out with no stop) reads 100%, within the - # sampling's few percent over 25 ms. The bound is 70%: 25% - # above the slowest ramped tail and 30% below a run-out. + # ended at speed or decelerated. The queue at cnc/stop is about + # 110 ms of ticks: the ring holds 200 ms and the producer leads by + # 10 ms, and the core's alarm hold plays 100 ms of it at the tick + # before the driver's stop. The kernel's ramp is stepped (cnc.c, + # ramp_update_tasklet_fn): the tick holds for the first 10 ms, + # then every 10 ms it drops by ramp_rate / 100 until it reaches + # the 900 Hz floor, which plays 10 ms before the stop. At the x32 + # tick of 112640 Hz and its 500000 Hz/s ramp that is 5000 Hz off + # every 10 ms: 24 intervals, 240 ms, about 13270 ticks, 118 ms of + # the tick (the ramp scales with the tick, so every mode gives the + # same fractions). So the run ends at the floor, or out of data + # part way down the ramp, on the interval its queue runs out in: + # 24% of the tick with 110 ms left, 38% with 100 ms, 51% with + # 90 ms. The tail's mean over the 25 to 30 ms the poll samples is + # higher, since it spans the intervals before: 32% with 110 ms + # left, 45% with 100 ms, 56% with 90 ms (61% over the 50 ms window + # bound), and a few percent when the run reaches the floor. A run + # that ends at the tick (a run-out with no stop) reads 100%, + # within the sampling's few percent over 25 ms. The bound is 70%: + # 14 points above the slowest ramped tail and 30 below a run-out. # The other readings are recorded, not judged: with the ramp about # the size of the queue, the steps that never played can be none, # the bytes played match a run-out within a few percent, and the - # time to leave running (about 250 ms with 100 ms left, 290 ms with - # 110 ms) is within 10% of the 100 ms hold plus three quarters of - # the 223 ms ramp (267 ms). + # time to leave running is the 100 ms hold plus the run's end on + # the ramp: about 240 ms with 100 ms left, 270 ms with 110 ms, + # 340 ms when the run reaches the floor. The evidence's ramp_ticks + # and ramp_s are the continuous figures, (f0^2 - 900^2) / (2 a) + # and (f0 - 900) / a: 12687 ticks and 223 ms at x32, about 4% and + # 7% under the stepped ramp's. running = [s for s in samples if s[1] == "running"] tail = None if running: