mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 09:11:11 -07:00
forgetest: the latch-unlock gate waits for the safety chain to release
The gate that refuses a latch unlock while the chain may hold HV_ENABLE up (charge_pump_alive or a pulse engine not idle) ran at the start of phases B, U and K3 of kernel.fire-line, within a second of the previous phase's run. A run feeds the charge-pump watchdog every 200 ms and the one-shot holds ALIVE for 0.45 s after the last feed, so the gate read alive=1 and refused: the first bench run of the gate (forgefirm 64f552fc; the laser_pgood gate before it was vacuous) failed phase B on image 20260902144848. wait_hv_off() polls the chain for up to 3 s before it refuses, logs the release when it was not immediate and records every wait in the evidence (hv_release_s). require_hv_off and check_hv_off use it. The bench scripts that copy the gate (fire_test.py per phase, gate_a_kernel_drills.py K3 after K2) get the same wait. Bench: kernel.fire-line PASS on 20260902144848 with the chain released after 0.41 s at each of the three phase boundaries. Host: tests/test_kernel_suite.py covers release inside the window, a chain held past it, and a chain already off. Catalog consequence: the kernel.* implementation hashes move (the suite file changed); the kernel set re-ran and passed.
This commit is contained in:
@@ -164,10 +164,34 @@ def hv_off():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
HV_RELEASE_S = 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def wait_hv_off(ctx, timeout_s=HV_RELEASE_S):
|
||||||
|
"""hv_off_reason() once the chain has released, or the reason that still
|
||||||
|
stands after timeout_s. A run feeds the charge-pump watchdog every
|
||||||
|
200 ms and the one-shot holds ALIVE for 0.45 s after the last feed (the
|
||||||
|
feed's soft timer can add one more feed after the state leaves
|
||||||
|
running), so a phase that follows a run finds the chain still up for
|
||||||
|
under a second. The wait covers that release and nothing more; the
|
||||||
|
time it took is logged when it was not immediate and kept in the
|
||||||
|
evidence."""
|
||||||
|
t0 = time.time()
|
||||||
|
why = hv_off_reason()
|
||||||
|
while why is not None and time.time() - t0 < timeout_s:
|
||||||
|
ctx.sleep(0.05)
|
||||||
|
why = hv_off_reason()
|
||||||
|
dt = round(time.time() - t0, 2)
|
||||||
|
ctx.evidence.setdefault("hv_release_s", []).append(dt)
|
||||||
|
if why is not None or dt >= 0.1:
|
||||||
|
ctx.log("hv off: %s after %.2f s", "released" if why is None else "still held", dt)
|
||||||
|
return why
|
||||||
|
|
||||||
|
|
||||||
def require_hv_off(ctx):
|
def require_hv_off(ctx):
|
||||||
"""The same rule at the start of the run (the precheck ran a moment
|
"""The same rule at the start of the run (the precheck ran a moment
|
||||||
earlier; the machine must still agree)."""
|
earlier; the machine must still agree)."""
|
||||||
why = hv_off_reason()
|
why = wait_hv_off(ctx)
|
||||||
ctx.evidence["charge_pump_alive"] = rd("cnc/charge_pump_alive")
|
ctx.evidence["charge_pump_alive"] = rd("cnc/charge_pump_alive")
|
||||||
ctx.evidence["kernel_state"] = rd("cnc/state")
|
ctx.evidence["kernel_state"] = rd("cnc/state")
|
||||||
ctx.check(why is None, "%s - refusing the latch unlock", why)
|
ctx.check(why is None, "%s - refusing the latch unlock", why)
|
||||||
@@ -175,7 +199,7 @@ def require_hv_off(ctx):
|
|||||||
|
|
||||||
def check_hv_off(ctx):
|
def check_hv_off(ctx):
|
||||||
"""The hard check right before an unlock (no prompt: forgectrl is down)."""
|
"""The hard check right before an unlock (no prompt: forgectrl is down)."""
|
||||||
why = hv_off_reason()
|
why = wait_hv_off(ctx)
|
||||||
ctx.check(why is None, "%s - refusing the latch unlock", why)
|
ctx.check(why is None, "%s - refusing the latch unlock", why)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""wait_hv_off against a scripted charge-pump readback: a chain that
|
||||||
|
releases inside the window passes, a chain held past it is the refusal,
|
||||||
|
and a chain already off costs no wait."""
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from forgetest.suite import kernel
|
||||||
|
|
||||||
|
|
||||||
|
class Ctx:
|
||||||
|
def __init__(self):
|
||||||
|
self.evidence = {}
|
||||||
|
self.lines = []
|
||||||
|
|
||||||
|
def sleep(self, s):
|
||||||
|
time.sleep(s)
|
||||||
|
|
||||||
|
def log(self, fmt, *a):
|
||||||
|
self.lines.append(fmt % a)
|
||||||
|
|
||||||
|
|
||||||
|
def scripted(alive_for_s):
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
def rd(path):
|
||||||
|
if path == "cnc/charge_pump_alive":
|
||||||
|
return "1" if time.time() - t0 < alive_for_s else "0"
|
||||||
|
if path == "cnc/state":
|
||||||
|
return "idle"
|
||||||
|
return None
|
||||||
|
return rd
|
||||||
|
|
||||||
|
|
||||||
|
class WaitHvOffTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self._rd = kernel.rd
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
kernel.rd = self._rd
|
||||||
|
|
||||||
|
def test_release_inside_the_window_passes(self):
|
||||||
|
kernel.rd = scripted(0.3)
|
||||||
|
ctx = Ctx()
|
||||||
|
t0 = time.time()
|
||||||
|
self.assertIsNone(kernel.wait_hv_off(ctx, timeout_s=2.0))
|
||||||
|
self.assertGreaterEqual(time.time() - t0, 0.25)
|
||||||
|
self.assertTrue(any("released" in l for l in ctx.lines))
|
||||||
|
self.assertGreaterEqual(ctx.evidence["hv_release_s"][0], 0.25)
|
||||||
|
|
||||||
|
def test_chain_held_past_the_window_is_the_refusal(self):
|
||||||
|
kernel.rd = scripted(99)
|
||||||
|
ctx = Ctx()
|
||||||
|
why = kernel.wait_hv_off(ctx, timeout_s=0.3)
|
||||||
|
self.assertIn("charge_pump_alive=1", why)
|
||||||
|
self.assertTrue(any("still held" in l for l in ctx.lines))
|
||||||
|
|
||||||
|
def test_chain_already_off_costs_no_wait(self):
|
||||||
|
kernel.rd = scripted(0)
|
||||||
|
ctx = Ctx()
|
||||||
|
t0 = time.time()
|
||||||
|
self.assertIsNone(kernel.wait_hv_off(ctx, timeout_s=2.0))
|
||||||
|
self.assertLess(time.time() - t0, 0.1)
|
||||||
|
self.assertEqual(ctx.lines, [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -45,6 +45,18 @@ def hv_off_reason():
|
|||||||
return 'charge_pump_alive=%s state=%s' % (alive, state)
|
return 'charge_pump_alive=%s state=%s' % (alive, state)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def wait_hv_off(timeout_s=3.0):
|
||||||
|
# A run feeds the charge-pump watchdog every 200 ms and the one-shot
|
||||||
|
# holds ALIVE for 0.45 s after the last feed, so a phase that follows a
|
||||||
|
# run finds the chain still up for under a second: wait for the release.
|
||||||
|
t0 = time.time()
|
||||||
|
why = hv_off_reason()
|
||||||
|
while why is not None and time.time() - t0 < timeout_s:
|
||||||
|
time.sleep(0.05)
|
||||||
|
why = hv_off_reason()
|
||||||
|
return why
|
||||||
|
|
||||||
def rd_pos():
|
def rd_pos():
|
||||||
with open('/sys/glowforge/cnc/position', 'rb') as f:
|
with open('/sys/glowforge/cnc/position', 'rb') as f:
|
||||||
raw = f.read(32)
|
raw = f.read(32)
|
||||||
@@ -72,7 +84,7 @@ stream = (
|
|||||||
print('phase %s: stream %d bytes = %.3f s' % (mode, len(stream), len(stream) / TICK_HZ))
|
print('phase %s: stream %d bytes = %.3f s' % (mode, len(stream), len(stream) / TICK_HZ))
|
||||||
|
|
||||||
if unlock:
|
if unlock:
|
||||||
why = hv_off_reason()
|
why = wait_hv_off()
|
||||||
if why is not None:
|
if why is not None:
|
||||||
print('ABORT: the safety chain is not holding HV off (%s) - refusing latch unlock' % why)
|
print('ABORT: the safety chain is not holding HV off (%s) - refusing latch unlock' % why)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -65,6 +65,18 @@ def hv_off_reason():
|
|||||||
return 'charge_pump_alive=%s state=%s' % (alive, state)
|
return 'charge_pump_alive=%s state=%s' % (alive, state)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def wait_hv_off(timeout_s=3.0):
|
||||||
|
# A run feeds the charge-pump watchdog every 200 ms and the one-shot
|
||||||
|
# holds ALIVE for 0.45 s after the last feed, so a phase that follows a
|
||||||
|
# run finds the chain still up for under a second: wait for the release.
|
||||||
|
t0 = time.time()
|
||||||
|
why = hv_off_reason()
|
||||||
|
while why is not None and time.time() - t0 < timeout_s:
|
||||||
|
time.sleep(0.05)
|
||||||
|
why = hv_off_reason()
|
||||||
|
return why
|
||||||
|
|
||||||
def rd_pos():
|
def rd_pos():
|
||||||
with open('/sys/glowforge/cnc/position', 'rb') as f:
|
with open('/sys/glowforge/cnc/position', 'rb') as f:
|
||||||
raw = f.read(32)
|
raw = f.read(32)
|
||||||
@@ -220,7 +232,7 @@ def drill_k2():
|
|||||||
|
|
||||||
|
|
||||||
def drill_k3():
|
def drill_k3():
|
||||||
why = hv_off_reason()
|
why = wait_hv_off()
|
||||||
if why is not None:
|
if why is not None:
|
||||||
print('ABORT: the safety chain is not holding HV off (%s) - refusing latch unlock' % why)
|
print('ABORT: the safety chain is not holding HV off (%s) - refusing latch unlock' % why)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
Reference in New Issue
Block a user