bench: the stale-origin drill goes, drills report their verdict, the pgood gate follows the pin

The faultpos live-fire drill armed and commanded a cut at an origin it
called stale to test a refusal the design decided not to gate: its only
outcome was an emission at an unknown position. Removed from the script
and the bench page.

live_fire_drills.py discarded every drill's return value, so the bench
page recorded a failed live-fire drill as OK. The exit status is the
drill's.

laser_pgood is the supply's power-good, high on every healthy machine;
fire_test.py and the K3 drill aborted on it and pgood_probe.py inverted
it. The latch-unlock drills now gate on the safety chain holding HV off
(charge-pump watchdog dead, pulse engine idle), as the kernel suite
does, and the probe reports the pin as the kernel publishes it.

motion.deadman: the controller resumed from its hang recovers on $X and
moves again without a restart (the stream's fault acknowledgment).
This commit is contained in:
ScottW514
2026-09-02 08:08:19 -04:00
parent 8cf2212e63
commit a2bc4233d5
6 changed files with 59 additions and 46 deletions
+14 -3
View File
@@ -34,6 +34,17 @@ def rd(attr):
return f.read().strip()
# The safety chain asserts HV_ENABLE only while a run feeds the charge-pump
# watchdog: a dead watchdog and an idle pulse engine mean HV off. That is
# the gate for a latch unlock; laser_pgood is the supply's power-good,
# high on every healthy machine, and says nothing about HV.
def hv_off_reason():
alive = rd('cnc/charge_pump_alive')
state = rd('cnc/state')
if alive != '0' or state != 'idle':
return 'charge_pump_alive=%s state=%s' % (alive, state)
return None
def rd_pos():
with open('/sys/glowforge/cnc/position', 'rb') as f:
raw = f.read(32)
@@ -61,9 +72,9 @@ stream = (
print('phase %s: stream %d bytes = %.3f s' % (mode, len(stream), len(stream) / TICK_HZ))
if unlock:
pgood = rd('cnc/laser_pgood')
if pgood != '0':
print('ABORT: laser_pgood=%s (HV supply reports good) - refusing latch unlock' % pgood)
why = hv_off_reason()
if why is not None:
print('ABORT: the safety chain is not holding HV off (%s) - refusing latch unlock' % why)
sys.exit(1)
snap('pre ')
+14 -3
View File
@@ -54,6 +54,17 @@ def rd(attr):
return f.read().strip()
# The safety chain asserts HV_ENABLE only while a run feeds the charge-pump
# watchdog: a dead watchdog and an idle pulse engine mean HV off. That is
# the gate for a latch unlock; laser_pgood is the supply's power-good,
# high on every healthy machine, and says nothing about HV.
def hv_off_reason():
alive = rd('cnc/charge_pump_alive')
state = rd('cnc/state')
if alive != '0' or state != 'idle':
return 'charge_pump_alive=%s state=%s' % (alive, state)
return None
def rd_pos():
with open('/sys/glowforge/cnc/position', 'rb') as f:
raw = f.read(32)
@@ -209,9 +220,9 @@ def drill_k2():
def drill_k3():
pgood = rd('cnc/laser_pgood')
if pgood != '0':
print('ABORT: laser_pgood=%s (HV supply reports good) - refusing latch unlock' % pgood)
why = hv_off_reason()
if why is not None:
print('ABORT: the safety chain is not holding HV off (%s) - refusing latch unlock' % why)
return 1
stream = POWER0 + FIRE * (3 * TICK_HZ) + PAD * (TICK_HZ // 2)
print('K3: %d bytes = %.1f s of FIRE bits; ramp_rate 10000 Hz/s '
+3 -28
View File
@@ -23,10 +23,6 @@ Drills (pass a name):
hold Phase 4 G-10: arm + start a longer job, feed-hold mid-run,
then hold. PASS: the disarm grace counts down in Hold and
the window closes (armed -> false) without the job resuming.
faultpos Phase 6 G-2/G-3: after a run that was stopped by an
underrun (position no longer trusted), a subsequent armed
job must refuse to cut at the stale origin - the sender
alarms and re-home is required. Reads homed via /status.
ircut Lid-IR fire characterization at cutting power: a 30 mm
square at S<power> (default 1000 = full) and F<feed>
(default 300) on scrap, sampled like `witness`. Prints the
@@ -492,27 +488,6 @@ def drill_hold(g):
g.cmd('$X')
def drill_faultpos(g):
print('=== Phase 6 G-2/G-3 drill: stale origin refused after underrun ===')
s = get_json('/status')
print('homed=%s (an underrun should have cleared this)' % s.get('homed'))
if s.get('homed'):
print('NOTE: homed is still true - run the SIGSTOP/underrun drill '
'first, then re-run this to confirm the refusal.')
return
print('attempting an armed cut at the stale origin - it must refuse/alarm')
prepare(g)
arm_cue()
r = g.cmd('M4 S400', timeout=2)
r2 = g.cmd('G1 X10 F300', timeout=3)
st = g.state()
print('controller response: %s / %s state=%s' % (r, r2, st))
print('FAULTPOS %s' % ('PASS (refused/alarm at stale origin)'
if ('error' in (r + r2).lower() or 'Alarm' in st) else
'REVIEW - cut was accepted; check G-3 anchor invalidation'))
g.cmd('M5', timeout=1)
def drill_ircut(g):
power = int(sys.argv[2]) if len(sys.argv) > 2 else 1000
feed = int(sys.argv[3]) if len(sys.argv) > 3 else 300
@@ -3239,7 +3214,7 @@ def drill_ctrlstart(g):
def main():
drill = sys.argv[1] if len(sys.argv) > 1 else ''
drills = {'witness': drill_witness, 'hold': drill_hold,
'faultpos': drill_faultpos, 'ircut': drill_ircut,
'ircut': drill_ircut,
'pthresh': drill_pthresh, 'dladder': drill_dladder,
'pcurve': drill_pcurve, 'm5dark': drill_m5dark,
'dpatch': drill_dpatch, 'flowload': drill_flowload,
@@ -3254,14 +3229,14 @@ def main():
return drills[drill](None) or 0 # no controller connection needed
g = Grbl(HOST, PORT)
try:
drills[drill](g)
rc = drills[drill](g)
finally:
# Always leave the laser commanded off.
try:
g.cmd('M5', timeout=1)
except Exception:
pass
return 0
return rc or 0 # the bench page's verdict is this exit status
if __name__ == '__main__':
+4 -4
View File
@@ -12,9 +12,9 @@ against what the chain and the supply were doing: idle, a dry run
hv_current), a pause and a resume, a lid open. hv_current comes from the
PIC at a lower rate and rides along as a range.
PGOOD is reported as the RAW PIN LEVEL (the kernel's laser_pgood attribute
is the logical, inverted value: 1 = pin low). Everything else is the
kernel's logical value.
PGOOD is the raw pin level as the kernel publishes it (laser_pgood is the
supply's power-good: 1 = the supply reports its outputs within spec).
Everything else is the kernel's logical value.
The loop must not hog the CPU: single core, the protocol thread is
SCHED_OTHER, so the sampler sleeps between passes and reports its worst gap.
@@ -33,7 +33,7 @@ import time
CNC = '/sys/glowforge/cnc/'
ATTRS = [ # name, attribute, invert-to-raw
('PGOOD', CNC + 'laser_pgood', True),
('PGOOD', CNC + 'laser_pgood', False),
('LASER_ON', CNC + 'laser_on', False),
('FIRE', CNC + 'laser_enable', False),
('CP_ALIVE', CNC + 'charge_pump_alive', False),