mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
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).
282 lines
10 KiB
Python
282 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""GATE A kernel drills - runs ON the board. Usage: gate_a_kernel_drills.py K1|K2|K3
|
|
|
|
The pulse device must be free: stop forgectrl first
|
|
(/etc/init.d/forgectrl stop), run the drill, then restart it. The
|
|
script exits with a clear message if the device is busy.
|
|
|
|
All drills: the first stream byte forces laser duty to zero, the
|
|
stream ends with laser-off bytes, motor_lock=15 (no axis moves), and
|
|
the latch is re-locked unconditionally on exit. K3 refuses to run if
|
|
laser_pgood reports the HV supply good. Software witnesses:
|
|
cnc/state, cnc/laser_enable, cnc/laser_on, cnc/laser_on_sampled, and
|
|
interlock_circuit bit 3 (the commanded latch). Physical witness:
|
|
scope on the PSU-connector LASER_ON pin, as in fire_test.py.
|
|
|
|
K1 - controlled-stop deceleration floor. Pads-only stream at the
|
|
default cloud tick (10 kHz, ramp 125000 Hz/s). A controlled stop
|
|
mid-run must ramp the step frequency down to the floor (tens of
|
|
milliseconds), never consume the tail as a max-rate burst
|
|
(sub-millisecond) or hang. PASS: stop-to-idle time inside the
|
|
controlled-decel band, no fault, no underrun.
|
|
|
|
K2 - resume waypoint honors the locked latch. Stream = pads, an
|
|
X-step section (motors locked), then a 2 s FIRE window. Run with
|
|
the latch LOCKED, controlled-stop inside the initial pads, then
|
|
resume with a positive waypoint - the branch that re-enables the
|
|
laser after the waypoint is reached. PASS: laser_enable stays 0
|
|
through the FIRE window; laser_on/laser_on_sampled stay 0.
|
|
|
|
K3 - mid-run latch unlock does not re-arm FIRE. Stream = FIRE bits
|
|
throughout. Run with the latch LOCKED (the run-start guard makes
|
|
the run laser-less), then write laser_latch=0 during the accel
|
|
ramp (ramp_rate lowered to 10000 Hz/s for a ~0.9 s window). The
|
|
unlock must drive the latch pin (interlock bit 3 reads 0) but
|
|
must NOT restore the FIRE output drive while the run or a ramp
|
|
is in flight. PASS: laser_enable stays 0 for the entire run.
|
|
"""
|
|
import errno, fcntl, os, struct, sys, time
|
|
|
|
TICK_HZ = 10000
|
|
FIRE = b'\x10'
|
|
PAD = b'\x00'
|
|
XSTEP = b'\x01'
|
|
POWER0 = bytes([0x80])
|
|
|
|
|
|
def wr(attr, val):
|
|
with open('/sys/glowforge/' + attr, 'w') as f:
|
|
f.write(str(val))
|
|
|
|
|
|
def rd(attr):
|
|
with open('/sys/glowforge/' + attr) as f:
|
|
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)
|
|
return struct.unpack('<5i', raw[:20])
|
|
|
|
|
|
def snap(tag):
|
|
print('%s: state=%s laser_enable=%s laser_on=%s laser_on_sampled=%s interlock=%s'
|
|
% (tag, rd('cnc/state'), rd('cnc/laser_enable'), rd('cnc/laser_on'),
|
|
rd('cnc/laser_on_sampled'), rd('cnc/interlock_circuit')))
|
|
|
|
|
|
def open_pulsedev():
|
|
try:
|
|
fd = os.open('/dev/glowforge', os.O_WRONLY)
|
|
except OSError as e:
|
|
if e.errno == errno.EBUSY:
|
|
print('ABORT: /dev/glowforge is busy - stop forgectrl first '
|
|
'(/etc/init.d/forgectrl stop), then re-run')
|
|
sys.exit(1)
|
|
raise
|
|
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
return fd
|
|
|
|
|
|
def wait_state(want, timeout, poll=0.05):
|
|
t0 = time.time()
|
|
while time.time() - t0 < timeout:
|
|
s = rd('cnc/state')
|
|
if s == want:
|
|
return s
|
|
time.sleep(poll)
|
|
return rd('cnc/state')
|
|
|
|
|
|
def watch_laser_until_idle(timeout):
|
|
"""Tight-loop laser_enable/laser_on watch; returns (hits, end_state)."""
|
|
hits = []
|
|
t0 = time.time()
|
|
state = 'running'
|
|
while time.time() - t0 < timeout:
|
|
en = rd('cnc/laser_enable')
|
|
on = rd('cnc/laser_on')
|
|
if en != '0' or on != '0':
|
|
hits.append((round(time.time() - t0, 4), en, on))
|
|
state = rd('cnc/state')
|
|
if state != 'running':
|
|
break
|
|
return hits, state
|
|
|
|
|
|
def drill_k1():
|
|
stream = POWER0 + PAD * (6 * TICK_HZ)
|
|
print('K1: %d bytes = %.1f s of pads at %d Hz, ramp 125000 Hz/s'
|
|
% (len(stream), len(stream) / TICK_HZ, TICK_HZ))
|
|
snap('pre ')
|
|
wr('cnc/motor_lock', 15)
|
|
wr('cnc/laser_latch', 1)
|
|
wr('cnc/ramp_rate', 125000)
|
|
wr('cnc/step_freq', TICK_HZ)
|
|
fd = open_pulsedev()
|
|
try:
|
|
os.lseek(fd, 1, os.SEEK_SET)
|
|
wr('cnc/enable', 1)
|
|
time.sleep(0.5)
|
|
os.write(fd, stream)
|
|
wr('cnc/run', 1)
|
|
time.sleep(1.5) # well past the accel ramp
|
|
if rd('cnc/state') != 'running':
|
|
print('FAIL: expected running before the stop, got %s' % rd('cnc/state'))
|
|
return 1
|
|
t0 = time.time()
|
|
wr('cnc/stop', 1)
|
|
while time.time() - t0 < 5:
|
|
if rd('cnc/state') != 'running':
|
|
break
|
|
dt = time.time() - t0
|
|
state = rd('cnc/state')
|
|
print('controlled stop: state=%s after %.4f s' % (state, dt))
|
|
ok = state == 'idle' and 0.02 <= dt <= 3.0 and rd('cnc/faults') == '0'
|
|
if state == 'idle' and dt < 0.02:
|
|
print('FAIL: stop consumed the tail as a burst (%.4f s) - decel floor broken' % dt)
|
|
elif not ok:
|
|
print('FAIL: state=%s dt=%.4f faults=%s' % (state, dt, rd('cnc/faults')))
|
|
else:
|
|
print('PASS: decelerating tail %.4f s, no burst, no fault' % dt)
|
|
# drain the paused remainder laser-less so the device ends clean
|
|
wr('cnc/resume', 0)
|
|
wait_state('running', 2, poll=0.005)
|
|
wait_state('idle', 10)
|
|
return 0 if ok else 1
|
|
finally:
|
|
wr('cnc/laser_latch', 1)
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
os.close(fd)
|
|
|
|
|
|
def drill_k2():
|
|
# "unmask-x" as a second argument runs with motor_lock=14 (X live,
|
|
# ~6 mm of X travel) for boards where the waypoint counter does not
|
|
# advance under a full motor_lock. Park the head mid-bed first.
|
|
lock = 14 if 'unmask-x' in [a.lower() for a in sys.argv[2:]] else 15
|
|
step_sec = (XSTEP + PAD * 4) * 1000 # 1000 X steps at 2 kHz
|
|
stream = (POWER0 + PAD * TICK_HZ + step_sec + PAD * (TICK_HZ // 2)
|
|
+ FIRE * (2 * TICK_HZ) + PAD * TICK_HZ)
|
|
print('K2: %d bytes = %.1f s; latch stays LOCKED; waypoint +200; motor_lock=%d'
|
|
% (len(stream), len(stream) / TICK_HZ, lock))
|
|
snap('pre ')
|
|
wr('cnc/motor_lock', lock)
|
|
wr('cnc/laser_latch', 1)
|
|
wr('cnc/ramp_rate', 125000)
|
|
wr('cnc/step_freq', TICK_HZ)
|
|
fd = open_pulsedev()
|
|
try:
|
|
os.lseek(fd, 1, os.SEEK_SET)
|
|
wr('cnc/enable', 1)
|
|
time.sleep(0.5)
|
|
os.write(fd, stream)
|
|
pos_before = rd_pos()
|
|
wr('cnc/run', 1)
|
|
time.sleep(0.4) # inside the initial pads
|
|
wr('cnc/stop', 1)
|
|
state = wait_state('idle', 5, poll=0.01)
|
|
if state != 'idle':
|
|
print('FAIL: controlled stop did not reach idle (state=%s)' % state)
|
|
return 1
|
|
print('paused inside the pads; resuming with waypoint +200 (latch LOCKED)')
|
|
wr('cnc/resume', 200)
|
|
wait_state('running', 2, poll=0.005)
|
|
hits, state = watch_laser_until_idle(20)
|
|
pos_after = rd_pos()
|
|
print('done: state=%s' % state)
|
|
print('pos before=%s after=%s (informational: does the waypoint counter '
|
|
'advance under motor_lock)' % (pos_before, pos_after))
|
|
snap('post')
|
|
print('laser_on_sampled=%s underruns=%s faults=%s'
|
|
% (rd('cnc/laser_on_sampled'), rd('cnc/underruns'), rd('cnc/faults')))
|
|
if hits:
|
|
print('FAIL: laser asserted with the latch locked: %s' % hits[:10])
|
|
return 1
|
|
if pos_before[:3] == pos_after[:3]:
|
|
print('NOTE: position did not advance under motor_lock, so the '
|
|
'waypoint may not have completed. Re-run as '
|
|
'"gate_a_kernel_drills.py K2 unmask-x" (X live, ~6 mm of '
|
|
'X travel) with the head parked mid-bed.')
|
|
print('PASS: FIRE window replayed after the resume waypoint with '
|
|
'laser_enable/laser_on at 0 throughout')
|
|
return 0
|
|
finally:
|
|
wr('cnc/laser_latch', 1)
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
os.close(fd)
|
|
|
|
|
|
def drill_k3():
|
|
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 '
|
|
'(~0.9 s accel window); unlock at t=+0.15 s'
|
|
% (len(stream), len(stream) / TICK_HZ))
|
|
snap('pre ')
|
|
wr('cnc/motor_lock', 15)
|
|
wr('cnc/laser_latch', 1)
|
|
wr('cnc/step_freq', TICK_HZ)
|
|
wr('cnc/ramp_rate', 10000)
|
|
fd = open_pulsedev()
|
|
try:
|
|
os.lseek(fd, 1, os.SEEK_SET)
|
|
wr('cnc/enable', 1)
|
|
time.sleep(0.5)
|
|
os.write(fd, stream)
|
|
wr('cnc/run', 1)
|
|
time.sleep(0.15) # inside the accel ramp
|
|
wr('cnc/laser_latch', 0)
|
|
print('latch UNLOCKED mid-ramp; interlock=%s (bit 3 should read 0)'
|
|
% rd('cnc/interlock_circuit'))
|
|
hits, state = watch_laser_until_idle(20)
|
|
print('done: state=%s' % state)
|
|
snap('post')
|
|
print('laser_on_sampled=%s underruns=%s faults=%s'
|
|
% (rd('cnc/laser_on_sampled'), rd('cnc/underruns'), rd('cnc/faults')))
|
|
if hits:
|
|
print('FAIL: FIRE drive re-armed by a mid-run unlock: %s' % hits[:10])
|
|
return 1
|
|
print('PASS: laser_enable stayed 0 for the entire run after the '
|
|
'mid-ramp unlock')
|
|
return 0
|
|
finally:
|
|
wr('cnc/laser_latch', 1)
|
|
try:
|
|
wr('cnc/ramp_rate', 125000)
|
|
except OSError:
|
|
print('WARNING: could not restore ramp_rate=125000 - restore it by hand')
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
os.close(fd)
|
|
|
|
|
|
def main():
|
|
mode = sys.argv[1].upper() if len(sys.argv) > 1 else ''
|
|
drills = {'K1': drill_k1, 'K2': drill_k2, 'K3': drill_k3}
|
|
if mode not in drills:
|
|
print(__doc__)
|
|
return 2
|
|
rc = drills[mode]()
|
|
wr('cnc/laser_latch', 1)
|
|
print('exit: state=%s latch=LOCKED' % rd('cnc/state'))
|
|
return rc
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|