mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
Add the senderchg and overrun bench drills; close BRINGUP item 20
Two live-fire drills in live_fire_drills.py: senderchg drops the sender mid-line with the tube lit, reconnects and expects the next laser-on to prompt again; overrun writes a 93-line fill at once into a running job and expects the report, the alarm, the disarm and a fresh prompt after $X. Both read hv_current and the thermopile for the instant emission ends and open their "nothing lit" window 2.5 s after the event, since laser_on_sampled is a one-second window count. Both passed on image 20260829190323; CAMPAIGN-LOG records the runs. BRINGUP item 20 (the arm skipped on a stale spindle state) closes: fix on the image, host and bench proven, covers in place. Its text moves to the superseded notes; items 21 to 23 are now 20 to 22. No catalog consequence: bench drills and documentation.
This commit is contained in:
+3
-21
@@ -1434,25 +1434,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
(`KPROBES`, `PERF_EVENTS`, `BPF_SYSCALL`, `DEBUG_FS`, `DEVMEM`,
|
||||
`MAGIC_SYSRQ`: no runtime cost unused, root-only exposure, and root can
|
||||
load modules anyway).
|
||||
20. **Arm skipped on a stale spindle state (safety, fix before the next
|
||||
image).** `glowforge_laser.c` arms on the first laser-on of a job only
|
||||
while its own record of the spindle state reads off
|
||||
(`state.on && !cur.on && !laser_ok` in `spindleSetState`), and
|
||||
`gflaser_disarm` does not clear that record. A job whose M5 never
|
||||
executes leaves the record on, and the next job's M3 runs with no arm:
|
||||
no button wait, no run report to forgectrl, no run airflow. Fire stays
|
||||
suppressed at the stream, so no energy leaves the tube, but the head
|
||||
runs the whole job without the operator's consent. Seen on the bench:
|
||||
a sender wrote a 93-line job at once, the RX ring (1023 bytes)
|
||||
overflowed, and the serial layer drops bytes on a full ring (`serial.c`,
|
||||
the overflow flag is set and never read), so the job's M5 and M2 were
|
||||
lost, the window stayed open until the sender disconnected, and the
|
||||
following job ran unarmed. Owed: the driver fix on an image (the arm
|
||||
decided by the window alone, an RX overrun dropping the overrunning
|
||||
line whole and aborting the job), one bench drill of each scenario,
|
||||
and the catalog's `covers` widened to `glowforge_laser.c` and
|
||||
`serial.c`.
|
||||
21. **A sender change while a job runs: discussion.** Today a sender that
|
||||
20. **A sender change while a job runs: discussion.** Today a sender that
|
||||
disconnects mid-job leaves the motion running to the end of what the
|
||||
controller holds, with the window closed and fire suppressed (the
|
||||
consent belonged to the displaced session), so the job finishes dark
|
||||
@@ -1471,7 +1453,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
running on leaves a clean stop position but wastes the piece. Decide
|
||||
with the gapless pause and resume item (17), which owns the resume
|
||||
mechanics.
|
||||
22. **The flow check while the tube is lit.** The arm-time heater check
|
||||
21. **The flow check while the tube is lit.** The arm-time heater check
|
||||
starts at the session open, so with a prompt press the tube is lit
|
||||
for most of its window, and a lit CW window adds about 1.5 C to the
|
||||
rise (0.5 C at 45 % density) against a 1.6 C margin; on top of that the
|
||||
@@ -1493,7 +1475,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
the ADC is the other candidate and a shared path could show both; a
|
||||
scope on the two sensor lines through a session, fans and tube
|
||||
switched separately, decides.
|
||||
23. **Laser power-good: what the line means.** `cnc/laser_pgood` and its
|
||||
22. **Laser power-good: what the line means.** `cnc/laser_pgood` and its
|
||||
sampled count are defined in the UAPI (active low, one sample every
|
||||
~3.9 ms), the facts bank records that the sampled count reads 0 through
|
||||
real cutting, and the cooling engine warns
|
||||
|
||||
@@ -4357,6 +4357,37 @@ on and holds it better with the pump off, so `cool_flow_rise` needs no
|
||||
warm-end value through 25 C; above that is not measured. Records:
|
||||
`bench-data/flow_warm_log_20260829.txt`, `flow_warm_results_20260829.json`.
|
||||
|
||||
## 2026-08-29: the arm-skip and RX-overrun fix on the bench
|
||||
|
||||
Two live-fire drills on image 20260829190323, both new in
|
||||
`scripts/bench/live_fire_drills.py`, both passed.
|
||||
|
||||
**`senderchg`** (record `bench-data/senderchg_20260829-195933.json`): a
|
||||
20 mm line at F60 lit on the press, the connection dropped 5.0 s in with
|
||||
the tube lit, a new session, the move finishing dark, then a fresh M3 and
|
||||
a 5 mm line. The engine's window closed 1.3 s after the drop; `hv_current`
|
||||
read dark within one 40 ms sample of it and the thermopile fell to its
|
||||
floor by 0.4 s; the head ran the remaining 15 mm without a sender (the
|
||||
Grbl expectation, BRINGUP item 21); the fresh M3 prompted for the button
|
||||
again, the second press armed, and the 5 mm line marked. Lit samples:
|
||||
177 on the first line, 0 between the drop and the second press, 31 on the
|
||||
second line. `cnc/laser_on_sampled` is a one-second window count and reads
|
||||
nonzero for up to a second after the beam stops, so the drills open their
|
||||
"nothing lit" window 2.5 s after the event and read `hv_current` and the
|
||||
thermopile for the instant.
|
||||
|
||||
**`overrun`** (record `bench-data/overrun_20260829-200256.json`): the same
|
||||
first line, and 3 s in a 93-line fill (1270 bytes) written at once against
|
||||
the 1023-byte ring. `RX overrun: the sender ignored flow control (Bf:);
|
||||
job aborted` 0.3 s after the blast, `ALARM:3`, the disarm and the reset
|
||||
banner; `hv_current` dark 0.11 s after the blast; the engine's window
|
||||
closed within the second. After `$X` the fresh M3 prompted again and the
|
||||
second press cut the 5 mm line. Lit samples: 134 before the alarm, 0
|
||||
between the alarm and the second press, 87 on the second line. The report
|
||||
and the reset came twice, 0.1 s apart: the tail of the blast arrived after
|
||||
the first reset had flushed the ring and overran it again. Harmless, the
|
||||
job was already stopped.
|
||||
|
||||
## Superseded status notes
|
||||
|
||||
### Shared machine services — remaining polish, as listed 2026-08-13
|
||||
@@ -5761,6 +5792,28 @@ Modules (27): `glowforge`, `wl18xx`, `wlcore`, `wlcore_sdio`, `mac80211`, `cfg80
|
||||
All of 1 through 4 ride one image flash (kernel/BSP changes batch), and every item here is
|
||||
a platform change under the acceptance model.
|
||||
|
||||
### Arm skipped on a stale spindle state (item 20), closed 2026-08-29
|
||||
|
||||
Closed: the arm is decided by the window alone (grblHAL-glowforge a7dcdca), an RX overrun drops the overrunning line whole and stops the job, both on image 20260829190323; proven by `tests/laser_arm_test.c` case H, `tests/serial_test.c`, the null-sink scenarios `sender-change-mid-job` and `rx-overrun`, and the bench drills `senderchg` and `overrun` (the 2026-08-29 entries above). The catalog's `laser.*` covers name `grblhal-glowforge/src/**`, which holds both files. The mid-job sender-change discussion is its own item.
|
||||
20. **Arm skipped on a stale spindle state (safety, fix before the next
|
||||
image).** `glowforge_laser.c` arms on the first laser-on of a job only
|
||||
while its own record of the spindle state reads off
|
||||
(`state.on && !cur.on && !laser_ok` in `spindleSetState`), and
|
||||
`gflaser_disarm` does not clear that record. A job whose M5 never
|
||||
executes leaves the record on, and the next job's M3 runs with no arm:
|
||||
no button wait, no run report to forgectrl, no run airflow. Fire stays
|
||||
suppressed at the stream, so no energy leaves the tube, but the head
|
||||
runs the whole job without the operator's consent. Seen on the bench:
|
||||
a sender wrote a 93-line job at once, the RX ring (1023 bytes)
|
||||
overflowed, and the serial layer drops bytes on a full ring (`serial.c`,
|
||||
the overflow flag is set and never read), so the job's M5 and M2 were
|
||||
lost, the window stayed open until the sender disconnected, and the
|
||||
following job ran unarmed. Owed: the driver fix on an image (the arm
|
||||
decided by the window alone, an RX overrun dropping the overrunning
|
||||
line whole and aborting the job), one bench drill of each scenario,
|
||||
and the catalog's `covers` widened to `glowforge_laser.c` and
|
||||
`serial.c`.
|
||||
|
||||
|
||||
## Reference notes
|
||||
|
||||
|
||||
@@ -124,6 +124,18 @@ Drills (pass a name):
|
||||
margin. `fit` reads every t2 record in the bench data dir
|
||||
and fits rise against dose. JSON records like dpatch.
|
||||
flowload t1 | flowload t2 <secs> [pct] | flowload fit
|
||||
senderchg A sender change mid-job: a 20 mm line at F60 lit on the
|
||||
press, the connection dropped five seconds in, a reconnect,
|
||||
the move finishing dark, then a fresh M3 and a 5 mm line that
|
||||
must prompt for the button again. PASS: two prompts, two arms,
|
||||
no lit sample between the drop and the second press, the
|
||||
second line marks. Two presses; JSON record.
|
||||
overrun An RX overrun mid-job: a 20 mm line at F60 lit on the press,
|
||||
a 93-line fill written at once three seconds in (about 1270
|
||||
bytes against the 1023-byte ring). PASS: the controller
|
||||
reports the overrun, stops in alarm with the window closed
|
||||
and emission ending at the alarm; after $X a fresh M3 prompts
|
||||
for the button again and a 5 mm line marks. Two presses.
|
||||
expstop Armed kill on the EXPECTED-stop path: start a mark job,
|
||||
then mid-burn POST /controller/stop (the supervisor stops
|
||||
the controller: SIGTERM, reap, exit safing). PASS: emission
|
||||
@@ -2338,6 +2350,291 @@ def drill_flowload(g):
|
||||
return 0 if rec['outcome'] == 'done' else 1
|
||||
|
||||
|
||||
# --- a sender change mid-job closes the window; the next laser-on prompts ----
|
||||
|
||||
# The driver arms at a laser-on only while the window is closed, and a
|
||||
# sender change closes it whatever the spindle state (the consent belonged
|
||||
# to the displaced session). This drill lights a 20 mm line at F60, drops
|
||||
# the connection five seconds in with the tube lit, reconnects, lets the
|
||||
# move finish dark (the Grbl expectation: the controller runs what it
|
||||
# holds), then sends a fresh M3 and a 5 mm line: the controller must prompt
|
||||
# for the button again, and nothing may fire between the drop and the
|
||||
# second press. The material is the witness: the first mark ends where the
|
||||
# connection dropped, the second line marks whole.
|
||||
SENDERCHG_S = 400
|
||||
SENDERCHG_LINE1 = 'G1 X20 F60' # 20 s of motion
|
||||
SENDERCHG_DROP_S = 5.0 # lit seconds before the drop
|
||||
SENDERCHG_LINE2 = 'G1 X5 F600'
|
||||
|
||||
|
||||
def senderchg_wait_reply(g, needle, timeout):
|
||||
"""Poll the socket until a reply containing `needle` arrives."""
|
||||
end = time.time() + timeout
|
||||
seen = len(g.log)
|
||||
while time.time() < end:
|
||||
g.drain()
|
||||
for _t, ln in g.log[seen:]:
|
||||
if needle in ln:
|
||||
return True
|
||||
seen = len(g.log)
|
||||
time.sleep(0.1)
|
||||
return False
|
||||
|
||||
|
||||
def drill_senderchg(g):
|
||||
print('=== sender change mid-job: the next laser-on must prompt again ===')
|
||||
print('connect: %s' % prepare(g))
|
||||
print('spindle off: %s' % g.cmd('M5'))
|
||||
pre = sample_forgectrl()
|
||||
if pre is None or pre.get('armed'):
|
||||
print('REFUSED: the armed window is still open (or forgectrl is unreachable)')
|
||||
return 2
|
||||
sampler = Sampler(PCURVE_SAMPLE_HZ)
|
||||
poller = CoolPoller()
|
||||
sampler.start()
|
||||
poller.start()
|
||||
arm_cue()
|
||||
print('>>> Scrap with 25 mm of free +X travel from the head. TWO presses: one')
|
||||
print('>>> for the first line, and a second one after the reconnect, when the')
|
||||
print('>>> prompt comes again. The line goes dark at the drop and the head')
|
||||
print('>>> finishes the move without a sender; that is expected.\n')
|
||||
print('G91/G21: %s / %s' % (g.cmd('G91'), g.cmd('G21')))
|
||||
t_m3 = time.time()
|
||||
for ln in ('M3 S%d' % SENDERCHG_S, SENDERCHG_LINE1, 'M5'):
|
||||
g.s.sendall(ln.encode() + b'\n')
|
||||
prompt1 = senderchg_wait_reply(g, 'press the button', 10)
|
||||
print(' first prompt: %s' % prompt1)
|
||||
st = g.wait_state('Run', FLOWLOAD_PRESS_WAIT_S)
|
||||
if not st.startswith('Run'):
|
||||
print('ABORTED: the first job never ran (%s)' % st)
|
||||
g.rt(b'\x18')
|
||||
sampler.stop()
|
||||
poller.stop()
|
||||
return 1
|
||||
t_run1 = time.time()
|
||||
time.sleep(SENDERCHG_DROP_S)
|
||||
t_drop = time.time()
|
||||
replies_a = list(g.log)
|
||||
g.s.close() # the sender goes away, mid-line, lit
|
||||
print(' connection dropped %.1f s into the first line' % (t_drop - t_run1))
|
||||
time.sleep(1.0)
|
||||
g2 = Grbl(HOST, PORT) # a new session
|
||||
t_reconnect = time.time()
|
||||
st = g2.wait_state('Idle', 40)
|
||||
t_idle1 = time.time()
|
||||
print(' reconnected; the first move ended dark %.1f s after the drop (state %s)'
|
||||
% (t_idle1 - t_drop, st))
|
||||
cs = sample_forgectrl() or {}
|
||||
print(' engine after the drop: armed=%s verdict=%s' % (cs.get('armed'), cs.get('verdict')))
|
||||
outcome = 'done'
|
||||
t_run2 = None
|
||||
try:
|
||||
for ln in ('M3 S%d' % SENDERCHG_S, SENDERCHG_LINE2, 'M5'):
|
||||
g2.s.sendall(ln.encode() + b'\n')
|
||||
prompt2 = senderchg_wait_reply(g2, 'press the button', 10)
|
||||
print(' second prompt after the reconnect: %s' % prompt2)
|
||||
if not prompt2:
|
||||
outcome = 'no-prompt'
|
||||
g2.rt(b'\x18')
|
||||
else:
|
||||
st = g2.wait_state('Run', FLOWLOAD_PRESS_WAIT_S)
|
||||
if not st.startswith('Run'):
|
||||
outcome = 'no-run'
|
||||
g2.rt(b'\x18')
|
||||
else:
|
||||
t_run2 = time.time()
|
||||
g2.wait_state('Idle', 30)
|
||||
print(' M2: %s' % g2.cmd('G90\nM2', timeout=10))
|
||||
finally:
|
||||
try:
|
||||
g2.cmd('M5', timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(3.0)
|
||||
sampler.stop()
|
||||
poller.stop()
|
||||
tr = sampler.samples
|
||||
lit = [s['t'] for s in tr if (s.get('lon') or 0) > 0 or (s.get('hv') or 0) > HV_DARK_MAX]
|
||||
# laser_on_sampled is a one-second window count: it reads nonzero for
|
||||
# up to a second after the beam stops, so the gap opens 2.5 s after
|
||||
# the drop; hv_current and the thermopile answer within a sample.
|
||||
lit1 = [t for t in lit if t_run1 - 1 <= t <= t_drop + 2.5]
|
||||
lit_gap = [t for t in lit if t_drop + 2.5 < t < (t_run2 or time.time()) - 0.5]
|
||||
lit2 = [t for t in lit if t_run2 and t >= t_run2] if t_run2 else []
|
||||
hv_off = [s['t'] - t_drop for s in tr if s['t'] > t_drop and (s.get('hv') or 0) <= HV_DARK_MAX]
|
||||
print(' hv_current read dark %.2f s after the drop' % (hv_off[0] if hv_off else -1))
|
||||
print('\n--- replies (t from the first M3) ---')
|
||||
for t, ln in replies_a + g2.log:
|
||||
if ln != 'ok' and not re.match(r'\$\d+=', ln):
|
||||
print(' %+7.1f s %s' % (t - t_m3, ln))
|
||||
print('--- engine armed transitions ---')
|
||||
last = None
|
||||
for p in poller.samples:
|
||||
if 'armed' in p and p['armed'] != last:
|
||||
print(' %+7.1f s armed=%s' % (p['t'] - t_m3, p['armed']))
|
||||
last = p['armed']
|
||||
print('--- emission (lit samples at 25 Hz) ---')
|
||||
print(' first line, press to drop: %d samples (%.1f s)' % (len(lit1), len(lit1) / 25.0))
|
||||
print(' drop to second press: %d samples <- must be 0' % len(lit_gap))
|
||||
print(' second line: %d samples (%.1f s)' % (len(lit2), len(lit2) / 25.0))
|
||||
ok = outcome == 'done' and prompt1 and len(lit1) > 25 and not lit_gap and len(lit2) > 5
|
||||
print('\n%s: %s' % ('PASS' if ok else 'FAIL',
|
||||
'the drop closed the window, nothing fired until the second press, '
|
||||
'and the second laser-on prompted and armed' if ok else outcome))
|
||||
rec = {'drill': 'senderchg', 'date': time.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||
't_m3': t_m3, 't_run1': t_run1, 't_drop': t_drop, 't_reconnect': t_reconnect,
|
||||
't_run2': t_run2, 'outcome': outcome, 'pass': bool(ok),
|
||||
'replies': replies_a + g2.log, 'poll': poller.samples, 'trace': tr}
|
||||
ddir = os.environ.get('FORGETEST_BENCH_DATA') or ('/tmp' if sampler.local else os.getcwd())
|
||||
path = os.path.join(ddir, 'senderchg_%s.json' % time.strftime('%Y%m%d-%H%M%S'))
|
||||
try:
|
||||
with open(path, 'w') as f:
|
||||
json.dump(rec, f, indent=1)
|
||||
print('record: %s' % path)
|
||||
except OSError as e:
|
||||
print('record not written: %s' % e)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
# --- an RX overrun mid-job stops the job and closes the window ---------------
|
||||
|
||||
# A sender that writes past the RX ring (Bf: is the contract) loses lines,
|
||||
# and the driver now drops the overrunning line whole, reports the overrun
|
||||
# and stops the job the way a ^X does: alarm, latch relocked, window
|
||||
# closed. This drill lights a 20 mm line at F60 on the press, writes a
|
||||
# 93-line fill at once three seconds in (about 1270 bytes against the
|
||||
# 1023-byte ring), and expects the report, the alarm and the disarm, with
|
||||
# emission ending at the alarm; then $X, a fresh M3 and a 5 mm line must
|
||||
# prompt for the button again and mark.
|
||||
OVERRUN_S = 400
|
||||
OVERRUN_LINE1 = 'G1 X20 F60'
|
||||
OVERRUN_BLAST_S = 3.0 # lit seconds before the blast
|
||||
OVERRUN_LINE2 = 'G1 X5 F600'
|
||||
|
||||
|
||||
def drill_overrun(g):
|
||||
print('=== RX overrun mid-job: report, alarm, disarm; the next laser-on prompts ===')
|
||||
print('connect: %s' % prepare(g))
|
||||
print('spindle off: %s' % g.cmd('M5'))
|
||||
pre = sample_forgectrl()
|
||||
if pre is None or pre.get('armed'):
|
||||
print('REFUSED: the armed window is still open (or forgectrl is unreachable)')
|
||||
return 2
|
||||
lines, _dx, _dy = dpatch_gcode(1500, 30.0, 0.3, 46)
|
||||
blast = ''.join(ln + '\n' for ln in lines + ['M5']).encode()
|
||||
sampler = Sampler(PCURVE_SAMPLE_HZ)
|
||||
poller = CoolPoller()
|
||||
sampler.start()
|
||||
poller.start()
|
||||
arm_cue()
|
||||
print('>>> Scrap with 25 mm of free +X and 15 mm of +Y travel from the head.')
|
||||
print('>>> TWO presses: one for the first line, and a second one after the')
|
||||
print('>>> overrun alarm has been cleared, when the prompt comes again. The')
|
||||
print('>>> blast is %d bytes written at once %.0f s into the line.\n'
|
||||
% (len(blast), OVERRUN_BLAST_S))
|
||||
print('G91/G21: %s / %s' % (g.cmd('G91'), g.cmd('G21')))
|
||||
t_m3 = time.time()
|
||||
for ln in ('M3 S%d' % OVERRUN_S, OVERRUN_LINE1):
|
||||
g.s.sendall(ln.encode() + b'\n')
|
||||
prompt1 = senderchg_wait_reply(g, 'press the button', 10)
|
||||
print(' first prompt: %s' % prompt1)
|
||||
st = g.wait_state('Run', FLOWLOAD_PRESS_WAIT_S)
|
||||
if not st.startswith('Run'):
|
||||
print('ABORTED: the first job never ran (%s)' % st)
|
||||
g.rt(b'\x18')
|
||||
sampler.stop()
|
||||
poller.stop()
|
||||
return 1
|
||||
t_run1 = time.time()
|
||||
time.sleep(OVERRUN_BLAST_S)
|
||||
t_blast = time.time()
|
||||
g.s.sendall(blast)
|
||||
reported = senderchg_wait_reply(g, 'RX overrun', 10)
|
||||
t_report = time.time()
|
||||
st = g.wait_state('Alarm', 10)
|
||||
t_alarm = time.time()
|
||||
print(' overrun reported: %s (%.1f s after the blast); state %s' % (reported, t_report - t_blast, st))
|
||||
outcome = 'done'
|
||||
t_run2 = None
|
||||
prompt2 = False
|
||||
try:
|
||||
if not reported or not st.startswith('Alarm'):
|
||||
outcome = 'no-report' if not reported else 'no-alarm'
|
||||
g.rt(b'\x18')
|
||||
else:
|
||||
time.sleep(2.0)
|
||||
g.drain()
|
||||
print(' unlock: %s' % g.cmd('$X'))
|
||||
for ln in ('M3 S%d' % OVERRUN_S, OVERRUN_LINE2, 'M5'):
|
||||
g.s.sendall(ln.encode() + b'\n')
|
||||
prompt2 = senderchg_wait_reply(g, 'press the button', 10)
|
||||
print(' second prompt after the alarm: %s' % prompt2)
|
||||
if not prompt2:
|
||||
outcome = 'no-prompt'
|
||||
g.rt(b'\x18')
|
||||
else:
|
||||
st = g.wait_state('Run', FLOWLOAD_PRESS_WAIT_S)
|
||||
if not st.startswith('Run'):
|
||||
outcome = 'no-run'
|
||||
g.rt(b'\x18')
|
||||
else:
|
||||
t_run2 = time.time()
|
||||
g.wait_state('Idle', 30)
|
||||
print(' M2: %s' % g.cmd('G90\nM2', timeout=10))
|
||||
finally:
|
||||
try:
|
||||
g.cmd('M5', timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(3.0)
|
||||
sampler.stop()
|
||||
poller.stop()
|
||||
tr = sampler.samples
|
||||
lit = [s['t'] for s in tr if (s.get('lon') or 0) > 0 or (s.get('hv') or 0) > HV_DARK_MAX]
|
||||
# laser_on_sampled is a one-second window count (see senderchg): the
|
||||
# gap opens 2.5 s after the alarm; hv_current answers within a sample.
|
||||
lit1 = [t for t in lit if t_run1 - 1 <= t <= t_alarm + 2.5]
|
||||
lit_gap = [t for t in lit if t_alarm + 2.5 < t < (t_run2 or time.time()) - 0.5]
|
||||
lit2 = [t for t in lit if t_run2 and t >= t_run2] if t_run2 else []
|
||||
hv_off = [s['t'] - t_blast for s in tr if s['t'] > t_blast and (s.get('hv') or 0) <= HV_DARK_MAX]
|
||||
print(' hv_current read dark %.2f s after the blast' % (hv_off[0] if hv_off else -1))
|
||||
last_lit_after_blast = max([t for t in lit if t <= (t_run2 or time.time())], default=t_blast)
|
||||
print('\n--- replies (t from the first M3) ---')
|
||||
for t, ln in g.log:
|
||||
if ln != 'ok' and not re.match(r'\$\d+=', ln):
|
||||
print(' %+7.1f s %s' % (t - t_m3, ln))
|
||||
print('--- engine armed transitions ---')
|
||||
last = None
|
||||
for p in poller.samples:
|
||||
if 'armed' in p and p['armed'] != last:
|
||||
print(' %+7.1f s armed=%s' % (p['t'] - t_m3, p['armed']))
|
||||
last = p['armed']
|
||||
print('--- emission (lit samples at 25 Hz) ---')
|
||||
print(' first line, press to alarm: %d samples (%.1f s); last lit sample %.2f s after the blast'
|
||||
% (len(lit1), len(lit1) / 25.0, last_lit_after_blast - t_blast))
|
||||
print(' alarm to second press: %d samples <- must be 0' % len(lit_gap))
|
||||
print(' second line: %d samples (%.1f s)' % (len(lit2), len(lit2) / 25.0))
|
||||
ok = outcome == 'done' and prompt1 and prompt2 and reported and not lit_gap and len(lit2) > 5
|
||||
print('\n%s: %s' % ('PASS' if ok else 'FAIL',
|
||||
'the overrun was reported, the job stopped in alarm with the window '
|
||||
'closed, nothing fired until the second press, and the next laser-on '
|
||||
'prompted and armed' if ok else outcome))
|
||||
rec = {'drill': 'overrun', 'date': time.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||
't_m3': t_m3, 't_run1': t_run1, 't_blast': t_blast, 't_report': t_report,
|
||||
't_alarm': t_alarm, 't_run2': t_run2, 'blast_bytes': len(blast),
|
||||
'outcome': outcome, 'pass': bool(ok),
|
||||
'replies': g.log, 'poll': poller.samples, 'trace': tr}
|
||||
ddir = os.environ.get('FORGETEST_BENCH_DATA') or ('/tmp' if sampler.local else os.getcwd())
|
||||
path = os.path.join(ddir, 'overrun_%s.json' % time.strftime('%Y%m%d-%H%M%S'))
|
||||
try:
|
||||
with open(path, 'w') as f:
|
||||
json.dump(rec, f, indent=1)
|
||||
print('record: %s' % path)
|
||||
except OSError as e:
|
||||
print('record not written: %s' % e)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
# --- the rapids after an M5 ship dark ----------------------------------------
|
||||
|
||||
# One constant-power line, M5, then two rapids over it with dwells between,
|
||||
@@ -2528,6 +2825,7 @@ def main():
|
||||
'pthresh': drill_pthresh, 'dladder': drill_dladder,
|
||||
'pcurve': drill_pcurve, 'm5dark': drill_m5dark,
|
||||
'dpatch': drill_dpatch, 'flowload': drill_flowload,
|
||||
'senderchg': drill_senderchg, 'overrun': drill_overrun,
|
||||
'expstop': drill_expstop, 'ctrlstart': drill_ctrlstart}
|
||||
if drill not in drills:
|
||||
print(__doc__)
|
||||
|
||||
Reference in New Issue
Block a user