mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 01:01:12 -07:00
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.
This commit is contained in:
@@ -132,6 +132,27 @@ absence of an explicit `MHD_OPTION_CONNECTION_LIMIT` + per-IP cap is
|
|||||||
still the deferred half; the default ceiling held here but a per-IP cap
|
still the deferred half; the default ceiling held here but a per-IP cap
|
||||||
remains the right hardening.
|
remains the right hardening.
|
||||||
|
|
||||||
|
**Idle-CPU diagnosis + pacing fix (2026-08-14).** The controller was
|
||||||
|
found at ~28% CPU while the machine appeared idle. Traced to grblHAL
|
||||||
|
being parked in the safety-door state (`Door:0`) — entered when the lid
|
||||||
|
was opened for inspection between drills, and held there awaiting a
|
||||||
|
cycle-start even after the lid closed. In any state other than
|
||||||
|
`STATE_IDLE`/`STATE_ALARM` the driver's `serial_wait` took the 200 µs
|
||||||
|
segment-production pace, so a parked Door (or Hold) busy-spun the
|
||||||
|
protocol thread. Not a regression in the audit work; the parked-state
|
||||||
|
pacing had always been tight. Fixed in grblHAL `b2cad8d`
|
||||||
|
(`motion_parked()`): a completed feed hold, a parked door (ajar or
|
||||||
|
closed), and sleep now take the coarse idle poll, while the motion
|
||||||
|
sub-phases (`Hold_Pending` decel, `Parking_Retracting`/`Resuming`) keep
|
||||||
|
the tight pace. Hot-deployed; pin bumped and fetch-verified.
|
||||||
|
**Bench-validated dry (`scripts/bench/pacing_test.py`):** idle 2.7%,
|
||||||
|
active move 35% (tight, segments flowing), parked `Hold:0` 2.7% (was
|
||||||
|
~28%), parked `Door:1`/`Door:0` 3.0% (was ~28%), and a mid-move
|
||||||
|
feed-hold→resume preserved position exactly (30.000 mm, no lost steps —
|
||||||
|
the feeder never starved through the decel and resume ramps). This pin
|
||||||
|
bump also rides P10's grblHAL CI/tests and the mlockall-root-only change
|
||||||
|
into the next image.
|
||||||
|
|
||||||
**Live-fire drills PASS (operator armed, S400/40% vector marks on
|
**Live-fire drills PASS (operator armed, S400/40% vector marks on
|
||||||
scrap, `scripts/bench/live_fire_drills.py`):**
|
scrap, `scripts/bench/live_fire_drills.py`):**
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ PV = "0.1.0"
|
|||||||
# upstream).
|
# upstream).
|
||||||
SRC_URI = "gitsm://github.com/ScottW514/grblHAL-glowforge.git;protocol=https;branch=main"
|
SRC_URI = "gitsm://github.com/ScottW514/grblHAL-glowforge.git;protocol=https;branch=main"
|
||||||
# Pinned; bump deliberately after pushing grblHAL-glowforge changes.
|
# Pinned; bump deliberately after pushing grblHAL-glowforge changes.
|
||||||
SRCREV = "e74f53f0b13edf039b58432bda56bcf8281d84f9"
|
SRCREV = "b2cad8d8acc49d0f317550f7c5961b76eae63a70"
|
||||||
|
|
||||||
SRC_URI += "file://grblhal.init"
|
SRC_URI += "file://grblhal.init"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#!/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()
|
||||||
Reference in New Issue
Block a user