Files
forgefirm/scripts/bench/pacing_test.py
T
ScottW514 ee25b89799 Bump grblHAL pin to b2cad8d (parked-state pacing fix) + pacing bench test
Pulls in the coarse-pacing fix for parked wait-for-operator states (a
machine left in Door or Hold no longer pins ~28% of the core), plus
P10's grblHAL CI/tests and the mlockall-root-only change. Fetch-verified.
Adds pacing_test.py (dry hold/resume + CPU measurement) and records the
diagnosis and bench validation in BRINGUP.
2026-08-14 22:06:46 -04:00

162 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Pacing-fix bench check - runs ON the board (localhost). Dry motion,
no laser. Verifies:
1. idle CPU is low (baseline - STATE_IDLE was already coarse-paced);
2. a job parked in a completed feed hold (Hold:0) is coarse-paced now,
not busy-spinning at the 200 us motion rate (the fix);
3. active motion is still tight-paced (the loop rate climbs during the
move);
4. a feed-hold mid-move then resume preserves position - i.e. the
decel-into-hold and resume-ramp sub-phases stay tight-paced and the
feeder never starves (the safety guard on the fix).
Usage: pacing_test.py [mm] [feed] (default 30 mm at F600, +X first)
"""
import glob
import os
import socket
import sys
import time
DIST = float(sys.argv[1]) if len(sys.argv) > 1 else 30.0
FEED = float(sys.argv[2]) if len(sys.argv) > 2 else 600.0
HZ = os.sysconf('SC_CLK_TCK')
def controller_pid():
for c in glob.glob('/proc/*/comm'):
try:
if open(c).read().strip() == 'grblHAL_glowfor':
return int(c.split('/')[2])
except Exception:
pass
raise SystemExit('controller not found')
PID = controller_pid()
def cpu_ticks():
s = open('/proc/%d/stat' % PID).read().split()
return int(s[13]) + int(s[14]) # utime + stime (all threads)
def cpu_percent(window):
a = cpu_ticks()
time.sleep(window)
b = cpu_ticks()
return 100.0 * (b - a) / (HZ * window)
class Grbl:
def __init__(self):
self.s = socket.create_connection(('127.0.0.1', 23), timeout=5)
self.s.settimeout(0.4)
time.sleep(0.4)
self.drain()
def drain(self):
out = b''
try:
while True:
d = self.s.recv(4096)
if not d:
break
out += d
except socket.timeout:
pass
return out.decode('ascii', 'replace')
def send(self, line):
self.s.sendall((line + '\n').encode())
time.sleep(0.15)
return self.drain()
def rt(self, ch):
self.s.sendall(ch)
def status(self):
self.s.sendall(b'?')
time.sleep(0.25)
t = self.drain()
i, j = t.rfind('<'), t.rfind('>')
return t[i:j + 1] if i >= 0 and j > i else ''
def state(self):
st = self.status()
return st[1:].split('|')[0] if st else ''
def mpos(self):
st = self.status()
for f in st[1:-1].split('|'):
if f.startswith('MPos:'):
return tuple(float(x) for x in f[5:].split(','))
return None
def wait(self, want, timeout):
end = time.time() + timeout
while time.time() < end:
if self.state().startswith(want):
return True
time.sleep(0.1)
return False
def main():
print('controller pid=%d; dry move %.0f mm at F%.0f' % (PID, DIST, FEED))
g = Grbl()
st = g.state()
if 'Alarm' in st or 'Door' in st or 'Hold' in st:
g.rt(b'\x18') # clean slate
time.sleep(2)
g.drain()
g.send('M5') # laser off, motion only
g.send('G91') # relative
print('\n[1] idle baseline')
idle = cpu_percent(3)
print(' idle CPU = %.1f%% (expect low)' % idle)
print('\n[3] active motion pacing (during the move)')
start = g.mpos()
g.send('G1 X%.3f F%.0f' % (DIST, FEED))
time.sleep(0.6) # let it get moving
moving = cpu_percent(1.0)
st_mv = g.state()
print(' state=%s CPU during move = %.1f%% (tight-paced)' % (st_mv, moving))
print('\n[2] feed-hold -> parked Hold:0 (the fix)')
g.rt(b'!') # feed hold
g.wait('Hold', 5)
time.sleep(1.5) # let the decel complete (Hold:1->0)
held_state = g.status()
parked = cpu_percent(3)
print(' %s' % held_state)
print(' CPU parked in Hold = %.1f%% (fix: expect low, was ~28%%)' % parked)
print('\n[4] resume -> verify no lost steps (safety)')
g.rt(b'~') # resume
if not g.wait('Idle', 30):
print(' WARNING: did not return to Idle')
end = g.mpos()
moved = end[0] - start[0] if (start and end) else None
ok = moved is not None and abs(moved - DIST) < 0.05
print(' start X=%.3f end X=%.3f moved=%.3f (expect %.1f) -> %s'
% (start[0], end[0], moved, DIST, 'PASS' if ok else 'MISMATCH'))
# Return to the starting position.
g.send('G1 X%.3f F%.0f' % (-DIST, FEED))
g.wait('Idle', 30)
g.send('G90')
print('\n--- verdict ---')
print('fix (parked Hold coarse-paced): %s (%.1f%% vs move %.1f%%)'
% ('PASS' if parked < moving * 0.5 and parked < 8 else 'REVIEW',
parked, moving))
print('safety (hold+resume no lost steps): %s' % ('PASS' if ok else 'FAIL'))
if __name__ == '__main__':
main()