laser power-good: the line characterized, the kernel-drill guard, the probe, the dev image's mmap and ctypes

The supply's power-good line is active high, static across HV enable and emission, and driven; the facts bank and CAMPAIGN-LOG carry the measurement and the item closes. The kernel-drill latch-unlock guard read the old inverted value as HV not good, a check that was vacuous and would refuse every run once the module reads the line correctly; it now uses the chain's own witnesses, the charge-pump watchdog and the engine state. pgood_probe.py watches the line beside the chain through the kernel readbacks and is registered on the bench page. The dev image lists python3-mmap and python3-ctypes again for the pad-level bench tools the python trim had left without them.
This commit is contained in:
ScottW514
2026-09-01 18:48:47 -04:00
parent fb804e32bd
commit 64f552fc28
7 changed files with 250 additions and 43 deletions
+1
View File
@@ -59,6 +59,7 @@ page's takeover does that; from a host, stop them first.
| `puls_profile.py` | Decodes factory `.puls` streams (raw or GF1-headered) into velocity/accel profiles: peak speeds, ramp-slope fits, per-move segments, Z cadence. Runs anywhere (stdlib only). Source of the factory-true grblHAL defaults: 700/590 mm/s² accel, 200 mm/s max rate, 28160 Hz travel tick. |
| `cp_watchdog_timing.py` | HV charge-pump watchdog one-shot timing (runs on the board): latches every CHG_PUMP feed pulse in GPIO3's edge detector (pin 24 only, IMR untouched, ICR2 restored on exit) and polls the `!Q` (`charge_pump_alive`) and `!HV_ENABLE` (`hv_enable`) pads through /dev/mem in a tight loop while it commands short local jogs; prints per-run t_w (last pulse → Q fall), Q → HV_ENABLE delay, priming latency and the feed period, with the loop's worst gap as the resolution. Motion only, laser locked, no other Grbl client attached. |
| `resume_dark_lead.py` | Pause/resume safety-chain timing (runs on the board, as root): samples LASER_ON, FIRE, HV_ENABLE, the charge-pump watchdog, the button and the doors straight off the SoC pads through /dev/mem at ~2 kHz, with motion dated from the kernel step counters, across a pause and a resume driven by the operator's button presses. Reports how long HV_ENABLE survives the stream stopping, how fast the chain re-arms on the resume, and - on `--run live` - the dark lead between FIRE going back on and LASER_ON following it, in milliseconds and in millimeters at the job's feed. `--run dry` (default) commands no laser at all and `--auto P,R` drives the pause and resume with `!`/`~` for an unattended rehearsal; `--run live` needs the arm press, eye protection, fire watch, extinguisher and exhaust. GRBL mode, no other Grbl client attached. |
| `pgood_probe.py` | The supply's power-good line (J1_14, `cnc/laser_pgood`) against the laser chain, without a scope (runs on the board, as root): polls the kernel readbacks (`laser_pgood` reported as the raw pin level, `laser_on`, `laser_enable`, `charge_pump_alive`) and the switch device's HV_ENABLE and doors bits at a few hundred hertz, `hv_current` at 20 Hz, and prints every transition with a timestamp plus a per-line summary. Drive the machine meanwhile (a dry jog, an armed cut, a pause, a lid open); the probe only watches. Feed it over ssh stdin: `ssh root@<board> 'python3 - --secs 90' < pgood_probe.py`. |
| `bench_m2.py` | Motion-quality bench, runs against the board over TCP:23: bounded round-trip jogs (sanity, max-rate, diagonal) + feed-hold/resume mid-move, reporting peak feed, state transitions, and position drift. |
Data files kept beside the tools: `flow_matrix_results.json` /
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""LASER_PGOOD (J1_14) against the rest of the laser chain, through the
kernel's readbacks.
Runs ON the board as root. Polls the kernel's GPIO readbacks (sysfs, one
open descriptor each, re-read with pread) and the switch device's EV_SW
word (EVIOCGSW) as fast as the loop allows, a few hundred hertz, and
reports every transition of the watched lines with a timestamp, plus a
per-line summary, so the meaning of the supply's line can be read off
against what the chain and the supply were doing: idle, a dry run
(HV_ENABLE follows the charge pump), an armed cut (LASER_ON, FIRE,
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.
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.
Usage: pgood_probe.py [--secs N] [--json FILE]
Drive the machine from a sender or the button meanwhile; the probe only
watches. GRBL mode, any state.
"""
import argparse
import fcntl
import json
import os
import struct
import sys
import time
CNC = '/sys/glowforge/cnc/'
ATTRS = [ # name, attribute, invert-to-raw
('PGOOD', CNC + 'laser_pgood', True),
('LASER_ON', CNC + 'laser_on', False),
('FIRE', CNC + 'laser_enable', False),
('CP_ALIVE', CNC + 'charge_pump_alive', False),
]
SWITCH_DEV = '/dev/input/event0'
SW_BITS = [('HV_ENABLE', 4), ('DOORS', 3)] # EV_SW codes on the switch device
EVIOCGSW = (2 << 30) | (8 << 16) | (0x45 << 8) | 0x1b
HV_CURRENT = '/sys/glowforge/pic/hv_current'
STATE = CNC + 'state'
HV_HZ = 20.0
SLEEP_S = 0.001
def rd(fd):
return os.pread(fd, 32, 0).strip()
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--secs', type=float, default=60.0)
ap.add_argument('--json', default='')
args = ap.parse_args()
try:
os.nice(5)
except OSError:
pass
fds = [(n, os.open(p, os.O_RDONLY), inv) for n, p, inv in ATTRS]
sw = os.open(SWITCH_DEV, os.O_RDONLY)
hv_fd = os.open(HV_CURRENT, os.O_RDONLY)
st_fd = os.open(STATE, os.O_RDONLY)
def sample():
s = {}
for n, fd, inv in fds:
v = int(rd(fd) or b'0')
s[n] = (1 - v) if inv else v
buf = fcntl.ioctl(sw, EVIOCGSW, b'\0' * 8)
for n, bit in SW_BITS:
s[n] = (buf[bit >> 3] >> (bit & 7)) & 1
return s
t0 = time.monotonic()
state = sample()
hv = int(rd(hv_fd) or b'0')
print('t=%8.3f start %s hv=%d kstate=%s'
% (0.0, ' '.join('%s=%d' % kv for kv in state.items()), hv, rd(st_fd).decode()))
sys.stdout.flush()
trans = []
hv_log = []
counts = {k: [0, 0] for k in state}
worst_gap = 0.0
n = 0
next_hv = t0
t_prev = t0
while True:
now = time.monotonic()
if now - t0 > args.secs:
break
gap = now - t_prev
if gap > worst_gap:
worst_gap = gap
t_prev = now
new = sample()
n += 1
for k in new:
if new[k] != state[k]:
trans.append((now - t0, k, new[k]))
print('t=%8.3f %-9s -> %d hv=%s kstate=%s'
% (now - t0, k, new[k], rd(hv_fd).decode(), rd(st_fd).decode()))
sys.stdout.flush()
counts[k][new[k]] += 1
state = new
if now >= next_hv:
hv_log.append((now - t0, int(rd(hv_fd) or b'0')))
next_hv = now + 1.0 / HV_HZ
time.sleep(SLEEP_S)
secs = time.monotonic() - t0
print('--- %.1f s, %d passes (%.0f Hz), worst gap %.1f ms' % (secs, n, n / secs, worst_gap * 1e3))
for k, (c0, c1) in counts.items():
print(' %-9s 0: %6.2f %% 1: %6.2f %%' % (k, 100.0 * c0 / n, 100.0 * c1 / n))
if hv_log:
vals = [v for _t, v in hv_log]
lit = [t for t, v in hv_log if v > 20]
print(' hv_current min %d max %d; > 20 raw for %.1f s%s'
% (min(vals), max(vals), len(lit) / HV_HZ,
(' (%.1f .. %.1f s)' % (lit[0], lit[-1])) if lit else ''))
if args.json:
with open(args.json, 'w') as f:
json.dump({'secs': secs, 'passes': n, 'worst_gap_ms': worst_gap * 1e3,
'counts': counts, 'transitions': trans, 'hv': hv_log}, f)
print('record: %s' % args.json)
return 0
if __name__ == '__main__':
sys.exit(main())