Files
forgefirm/scripts/bench/pwm_sweep.py
T
ScottW514 cd8a01a3d9 bench: every board-runnable tool ported to the bench page
The remaining bench diagnostics run from forgetest's #bench tab. The
tools that also run from a LAN host share scripts/bench/gfbench.py:
GF_HOST names a remote machine (host mode, sysfs through ssh, Grbl and
forgectrl over the LAN); unset, the tool runs on the board itself
(local mode, sysfs directly, everything on 127.0.0.1), which is how the
page runs them - with GF_HOST=127.0.0.1, the panel token in GF_TOKEN
and their data files under <data>/bench/ (FORGETEST_BENCH_DATA). The
helper also reads a machine setting from forgectrl, or from the settings
file on the board while forgectrl is stopped.

Ported: pwm_sweep / pwm_hold (scope = a takeover; the latch relocked,
the write refused if FIRE or LASER_ON reads active), pwm_stream_test
(PASS/FAIL exit), flow_characterize, flow_recheck_char,
flow_warm_validate and flow_matrix (takeovers: forgectrl owns the
thermal hardware, so the page's takeover replaces the tools' own
controller stop/restart, whose command line predated the supervisor;
results and logs in the bench data directory), flow_sustained,
fan_test, temp_calibrate (dry; watch bounded in seconds; the threshold
and the coolant conversion from the shared code), flow_escalate_drill
(cool_confirm_max_s shortened through forgectrl's settings for the
drill and restored; the setting's minimum is the default budget), and
live_fire_drills (<drill> [S] [F], all six drills, host from GF_HOST,
token from the board). flow_matrix joins the registry. What stays
unported cannot run against the machine at all: the two null-sink CI
harnesses and the .puls decoder.

Runner: a scope tool runs inside the takeover wrapper; the bench
environment above is passed to every tool. Tests: test_bench_registry
(registry <-> scripts/bench consistency, every ported tool builds its
command line, every script compiles, gfbench host/local modes) and the
server test (scope tool takeover, the environment reaching the tool).
Local mode smoke-run on the bench (temp_calibrate watch, setting, token)
from /tmp, removed after.

No catalog consequence: bench tools are not image components (dev-only
forgetest); the acceptance catalog is unchanged.
2026-08-16 16:28:39 -04:00

103 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""LASER_PWM scope test - runs ON the board.
check: read-only - safety-chain readbacks + PWM2 register dump.
sweep: step the PWM duty register through known values with pauses so
the scope can capture each, then restore the original sample.
The duty register (PWMSAR) is the laser power SETPOINT only. No motion
subsystem is touched (steppers are disabled before this runs), no
stream runs, the laser latch is locked, FIRE is never asserted.
Locked state only: run with the controller and forgectrl stopped (the
bench page's takeover), the pulse device closed. The latch is relocked
here as well, and the sweep refuses to write if the FIRE line reads
driven or LASER_ON reads active. Usage: pwm_sweep.py [check|sweep]
"""
import mmap, struct, sys, time
PWM2_BASE = 0x02084000
PERCLK_HZ = 66_000_000
SAR_OFF = 0x0C
def rd(name):
try:
with open('/sys/glowforge/' + name) as f:
return f.read().strip()
except OSError as e:
return '<%s>' % e
def dump_regs(m):
cr, sr, ir, sar, pr = struct.unpack('<5I', m[:20])
divider = ((cr >> 4) & 0xFFF) + 1
counts = pr + 2
freq = PERCLK_HZ / (divider * counts) if counts else 0
print('PWMCR=0x%08x PWMSAR=%d PWMPR=%d enabled=%s divider=%d counts=%d carrier=%.2f kHz'
% (cr, sar, pr, bool(cr & 1), divider, counts, freq / 1000))
return sar, pr
def wr(name, val):
with open('/sys/glowforge/' + name, 'w') as f:
f.write(str(val))
def locked_state():
"""The latch commanded locked, FIRE not driven, no emission."""
try:
wr('cnc/laser_latch', 1)
except OSError as e:
print('!! could not lock the laser latch: %s' % e)
return False
time.sleep(0.2)
en, on = rd('cnc/laser_enable'), rd('cnc/laser_on')
if en != '0' or on != '0':
print('!! not the locked state: laser_enable=%s laser_on=%s' % (en, on))
return False
return True
def safety_readback():
print('cnc/state =', rd('cnc/state'))
print('cnc/laser_on =', rd('cnc/laser_on'))
print('cnc/laser_enable =', rd('cnc/laser_enable'))
print('cnc/laser_pgood =', rd('cnc/laser_pgood'))
print('cnc/interlock_circuit =', rd('cnc/interlock_circuit'),
'(b0 LASER_ON b1 LASER_ENABLE b2 BUTTON_LATCH b3 LASER_LATCH b4 ILK_RESET)')
print('cnc/laser_on_sampled =', rd('cnc/laser_on_sampled'))
mode = sys.argv[1] if len(sys.argv) > 1 else 'check'
with open('/dev/mem', 'r+b') as f:
m = mmap.mmap(f.fileno(), 4096, mmap.MAP_SHARED,
mmap.PROT_READ | mmap.PROT_WRITE, offset=PWM2_BASE)
print('--- safety readback (before)')
safety_readback()
print('--- PWM2 registers')
sar0, pr = dump_regs(m)
if mode == 'sweep':
if not locked_state():
m.close()
sys.exit(2)
period = pr + 2
steps = [(64, '50%'), (32, '25%'), (96, '75%'), (8, '6%'), (127, '100%')]
print('--- duty sweep: 4 s per step, watch the scope')
for sar, label in steps:
m[SAR_OFF:SAR_OFF + 4] = struct.pack('<I', sar)
time.sleep(0.1)
cur = struct.unpack('<I', m[SAR_OFF:SAR_OFF + 4])[0]
print(' PWMSAR=%-3d (%s of %d counts) readback=%d' % (sar, label, period, cur))
time.sleep(4)
m[SAR_OFF:SAR_OFF + 4] = struct.pack('<I', sar0)
print('--- restored PWMSAR=%d' % sar0)
print('--- safety readback (after)')
safety_readback()
dump_regs(m)
m.close()