mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
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:
@@ -207,10 +207,10 @@ TOOLS = [
|
||||
# -- laser (live) --------------------------------------------------------------
|
||||
{"id": "live-fire", "title": "LIVE laser drills", "script": "live_fire_drills.py",
|
||||
"safety": "live", "where": "board", "ported": True,
|
||||
"args": [_arg("drill", "choice", "witness", "witness / hold / faultpos / ircut / expstop / ctrlstart",
|
||||
["witness", "hold", "faultpos", "ircut", "expstop", "ctrlstart"]),
|
||||
"args": [_arg("drill", "choice", "witness", "witness / hold / ircut / expstop / ctrlstart",
|
||||
["witness", "hold", "ircut", "expstop", "ctrlstart"]),
|
||||
_arg("power", "int", 1000, "ircut: S value"), _arg("feed", "int", 300, "ircut: F value")],
|
||||
"desc": "Emission witness, disarm grace in Hold, stale-origin refusal, lid-IR characterization cut, armed "
|
||||
"desc": "Emission witness, disarm grace in Hold, lid-IR characterization cut, armed "
|
||||
"kill on the expected-stop path (+ the separate controller restart). The operator's arm press is "
|
||||
"required for every drill; eye protection, fire watch, extinguisher, exhaust."},
|
||||
{"id": "resume-dark-lead", "title": "Pause / resume chain timing (dark lead)", "script": "resume_dark_lead.py",
|
||||
|
||||
@@ -501,7 +501,8 @@ def _return_x(ctx, delta_mm):
|
||||
description="SIGKILL of the controller mid-move: the supervisor reaps it, safes (cnc/stop, "
|
||||
"latch relocked - it never unlocked), and respawns within seconds. SIGSTOP (a "
|
||||
"hang) mid-move: the ring drains into a kernel underrun (fast halt, latch "
|
||||
"locked); the hung process is killed and the supervisor respawns. forgectrl "
|
||||
"locked); the process resumed from the hang recovers on $X and moves again "
|
||||
"without a restart. forgectrl "
|
||||
"restart mid-move: the busy controller finishes the move unmanaged and the new "
|
||||
"daemon retakes supervision at idle. After each drill the head is jogged back "
|
||||
"by the kernel-measured distance.")
|
||||
@@ -584,12 +585,27 @@ def deadman(ctx):
|
||||
"underruns": hw.sysfs_int("cnc/underruns", 0)}
|
||||
ctx.log("after SIGSTOP: kernel %s in %s s, latch locked %s, underruns %s -> %s",
|
||||
kstate, halt_s, latch_locked(), underruns0, ev["sigstop"]["underruns"])
|
||||
_os.kill(pid1, _signal.SIGKILL) # the hung controller cannot recover itself
|
||||
# The controller comes back from the hang to find its stream
|
||||
# faulted and the core alarmed. An unlock ($X) is the operator's
|
||||
# acknowledgment: it must restore a controller that moves again,
|
||||
# without a restart (the position is not trusted until a re-home,
|
||||
# so the move is a jog).
|
||||
_os.kill(pid1, _signal.SIGCONT)
|
||||
ctx.sleep(1.0)
|
||||
st = g.status_report()["state"]
|
||||
ev["sigstop"]["state_after_cont"] = st
|
||||
ctx.log("controller resumed: state %s", st)
|
||||
g.command("$X")
|
||||
g.command("$J=G91X-5F1200")
|
||||
peak, states, st = wait_idle(ctx, g, 15)
|
||||
ev["sigstop"]["recovery_states"] = states
|
||||
ctx.check("TIMEOUT" not in states and not st.startswith("Alarm"),
|
||||
"the controller did not move again after $X (states %s)", states)
|
||||
ctx.check(kstate == "underrun", "the ring did not drain into a kernel underrun (state %s)", kstate)
|
||||
ctx.check(latch_locked(), "latch unlocked after the underrun")
|
||||
m2 = wait_running(30, not_pid=pid1)
|
||||
ev["sigstop"]["respawn"] = m2
|
||||
ctx.check(m2 and m2.get("pid") != pid1, "supervisor did not respawn after the hang")
|
||||
m2 = wait_running(10)
|
||||
ev["sigstop"]["after"] = m2
|
||||
ctx.check(m2 and m2.get("pid") == pid1, "the recovered controller was replaced (%s)", m2)
|
||||
ctx.sleep(3)
|
||||
x1 = _kernel_x_mm(ctx)
|
||||
_return_x(ctx, (x1 - x0) if (x0 is not None and x1 is not None) else None)
|
||||
|
||||
@@ -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 ')
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user