mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
Run the depth witness under either dose model
The dpatch drill reads laser_power_model and runs row B at 100/80/60/45/30 percent duty under analog, with the thermopile labels from the analog ladder, so the analog dose response can be judged on material the same way as the density one. The report names the model's unit in every line. Bench-only tool; no acceptance catalog consequence.
This commit is contained in:
@@ -32,7 +32,7 @@ page's takeover does that; from a host, stop them first.
|
||||
| `gate_a_kernel_drills.py` | Kernel laser-safety drills (run on the board with forgectrl stopped so the pulse device is free): `K1` controlled-stop deceleration floor, `K2` resume waypoint honors the locked latch, `K3` a mid-ramp latch unlock never re-arms the FIRE drive. Software witnesses (`cnc/state`, `laser_enable`, `laser_on`, `laser_on_sampled`, interlock bit 3) plus the PSU-connector LASER_ON scope point; K3 refuses to run if HV reports good. |
|
||||
| `laser_stream_test.py` | Host-side laser pulse-stream emission harness: runs the native null-sink controller with `GFSINK_DUMP`, drives small laser jobs over TCP, and checks the dumped bytes against the kernel feeder contract (leading power byte, no back-to-back power bytes, FIRE only inside cutting moves, every stream ends FIRE-clear, no FIRE on a stepless gap, no FIRE leak across cycle churn, the rapids after an M5 executed at idle ship dark, and the next job in the same process fires at the level the previous one ended at). Runs in the grblHAL repo's CI. |
|
||||
| `laser_lifecycle_test.py` | Host-side operator-armed-window lifecycle harness (null-sink controller): arm once per job with M5/M3 persistence, the M2 close, sender-change re-consent, the disarm grace counting down in Hold, and arm refusal under a blocking cooling verdict. Runs in the grblHAL repo's CI. |
|
||||
| `live_fire_drills.py` | **LIVE LASER** drills, on the board (the bench page) or from a LAN host (`GF_HOST`): `live_fire_drills.py <drill> [S] [F]` - `witness` (emission witness, lid-IR peaks vs the ambient baseline, HV current, job-based disarm on M2), `hold` (disarm grace in Hold), `faultpos` (armed job refuses a stale origin after an underrun), `ircut` (lid-IR characterization cut at S/F), `pthresh` (laser power-threshold ladder: 13 constant-power rungs from 2 % to 30 % of full on scrap; the lowest rung that marks is the tube's striking threshold and reads directly as the `$35` value - requires `$35` = 0 for the run), `dladder` (density ladder at a chosen base period), `pcurve` (laser performance-curve ladder: one 100 mm line per level at 10 mm/s under M3, the laser off between rungs and a mid-ladder rung repeated at the end; reads `pic/hv_current` and the head thermopile `head/beam_detect_analog` (a scatter detector in the beam path upstream of the final mirror, so it sees the beam, not the material) from sysfs at ~25 Hz on the board, brackets each rung on the controller's Run/Idle states, and reports per rung the current with a clipped-at-1023 flag, the thermopile delta over its laser-off baseline and in-line drift, then the normalized curve, monotonicity, a line fit with its threshold intercept and the repeat-rung drift; JSON record with the raw trace in the bench data directory; rungs follow `laser_power_model`, a comma list overrides; a curve measurement wants `$35` = 0), `dpatch` (depth witness for the density curve: two rows of small serpentine-filled patches, row A CW at feeds giving relative doses 1.0 to 0.25, row B density 100/80/60/45/30 % at F600; the operator matches each row-B patch to the row-A patch of equal depth, which reads the density's light fraction off the material beside the thermopile's prediction; JSON record), `m5dark` (the rapids after an M5 ship dark: one 20 mm line at M3 S400, M5, dwell, rapid back, dwell, rapid forward; PASS when the 25 Hz current trace shows one discharge segment and reads dark after the M5 and `laser_on_sampled` never re-lights; the catalog's `laser.m5-rapid-dark` is its port), `flowload` (cooling under laser load, one armed run per invocation, the conf keys it writes put back at the end, the pump never commanded off: `t1` reproduces the flow-check trip with the check on at its defaults and two 30 x 4 mm CW fills at F1500 starting on the press with no dark dwell, and reports the engine's rise/dT verdict beside the 25 Hz trace of both coolant sensors, the current, the digital witness and the heater output in 5 s bins across the window, with the shape at fire start; `t2 <secs> [pct]` runs with the check off and one fill of about `secs` lit seconds at CW or at `pct` density, and reports the lag to each sensor, the rise per raw-second of `hv_current` and what a full 50 s window would add against the 1.6 C margin; `fit` fits rise against dose over every t2 record; JSON records), `expstop` (armed kill on the expected-stop path; needs the panel token - `GF_TOKEN`, or the board's token file) and `ctrlstart` (the separate controller restart after it). Every drill waits for the operator's physical arm press; eye protection, fire watch, extinguisher, and exhaust are mandatory. |
|
||||
| `live_fire_drills.py` | **LIVE LASER** drills, on the board (the bench page) or from a LAN host (`GF_HOST`): `live_fire_drills.py <drill> [S] [F]` - `witness` (emission witness, lid-IR peaks vs the ambient baseline, HV current, job-based disarm on M2), `hold` (disarm grace in Hold), `faultpos` (armed job refuses a stale origin after an underrun), `ircut` (lid-IR characterization cut at S/F), `pthresh` (laser power-threshold ladder: 13 constant-power rungs from 2 % to 30 % of full on scrap; the lowest rung that marks is the tube's striking threshold and reads directly as the `$35` value - requires `$35` = 0 for the run), `dladder` (density ladder at a chosen base period), `pcurve` (laser performance-curve ladder: one 100 mm line per level at 10 mm/s under M3, the laser off between rungs and a mid-ladder rung repeated at the end; reads `pic/hv_current` and the head thermopile `head/beam_detect_analog` (a scatter detector in the beam path upstream of the final mirror, so it sees the beam, not the material) from sysfs at ~25 Hz on the board, brackets each rung on the controller's Run/Idle states, and reports per rung the current with a clipped-at-1023 flag, the thermopile delta over its laser-off baseline and in-line drift, then the normalized curve, monotonicity, a line fit with its threshold intercept and the repeat-rung drift; JSON record with the raw trace in the bench data directory; rungs follow `laser_power_model`, a comma list overrides; a curve measurement wants `$35` = 0), `dpatch [F] [pitch] [length]` (depth witness for the dose curve of the configured `laser_power_model`: two rows of small serpentine-filled patches, row A CW at feeds giving relative doses 1.0 to 0.25 of the reference feed, row B at the reference feed at 100/80/60/45/30 % density or duty; the operator matches each row-B patch to the row-A patch of equal depth, which reads the model's light fraction off the material beside the thermopile's prediction; JSON record), `m5dark` (the rapids after an M5 ship dark: one 20 mm line at M3 S400, M5, dwell, rapid back, dwell, rapid forward; PASS when the 25 Hz current trace shows one discharge segment and reads dark after the M5 and `laser_on_sampled` never re-lights; the catalog's `laser.m5-rapid-dark` is its port), `flowload` (cooling under laser load, one armed run per invocation, the conf keys it writes put back at the end, the pump never commanded off: `t1` reproduces the flow-check trip with the check on at its defaults and two 30 x 4 mm CW fills at F1500 starting on the press with no dark dwell, and reports the engine's rise/dT verdict beside the 25 Hz trace of both coolant sensors, the current, the digital witness and the heater output in 5 s bins across the window, with the shape at fire start; `t2 <secs> [pct]` runs with the check off and one fill of about `secs` lit seconds at CW or at `pct` density, and reports the lag to each sensor, the rise per raw-second of `hv_current` and what a full 50 s window would add against the 1.6 C margin; `fit` fits rise against dose over every t2 record; JSON records), `expstop` (armed kill on the expected-stop path; needs the panel token - `GF_TOKEN`, or the board's token file) and `ctrlstart` (the separate controller restart after it). Every drill waits for the operator's physical arm press; eye protection, fire watch, extinguisher, and exhaust are mandatory. |
|
||||
| `pacing_test.py` | Protocol-loop pacing check (runs on the board, dry motion): idle and parked-in-Hold states are coarse-paced, active motion is tight-paced, and a feed-hold/resume mid-move preserves position with no feeder starve. |
|
||||
| `gfbench.py` | Not a tool: the helper the board/host tools share - `HOST`/`LOCAL` from `GF_HOST`, `board(cmd)` (local `sh -c` or ssh), the factory coolant conversion `degc()`, `data_path()` (`FORGETEST_BENCH_DATA` or next to the tool), forgectrl's HTTP API with the panel token, `setting(key)` (from forgectrl, or from `/data/forgefirm.conf` on the board while forgectrl is stopped). |
|
||||
| `fan_test.py` | Fan/coolant bench (board or host; controller running): snapshots fan PWMs/tachs/temps, drives M8 → cut fans, M9 → cooldown → idle, verifying via tach readbacks. |
|
||||
|
||||
@@ -96,14 +96,16 @@ Drills (pass a name):
|
||||
nonzero again after its first zero past the line. Prints the
|
||||
9 s after the line at 40 ms steps. The catalog's
|
||||
laser.m5-rapid-dark is its port.
|
||||
dpatch Depth witness for the density curve: two rows of small
|
||||
engraved patches (serpentine G1 fills) on the stock. Row A
|
||||
is CW (S1000) at feeds giving relative doses 1.0 to 0.25;
|
||||
row B is density 100/80/60/45/30 %% at F600. Match each
|
||||
row-B patch to the row-A patch of equal depth by eye: that
|
||||
reads the density's light fraction off the material, next
|
||||
to what the thermopile says it should be. Samples sysfs at
|
||||
25 Hz like pcurve; JSON record in the bench data directory.
|
||||
dpatch Depth witness for the dose curve of the configured
|
||||
laser_power_model: two rows of small engraved patches
|
||||
(serpentine G1 fills) on the stock. Row A is CW (S1000) at
|
||||
feeds giving relative doses 1.0 to 0.25 of the reference
|
||||
feed; row B is 100/80/60/45/30 %% density or duty at the
|
||||
reference feed. Match each row-B patch to the row-A patch
|
||||
of equal depth by eye: that reads the model's light
|
||||
fraction off the material, next to what the thermopile
|
||||
says it should be. Samples sysfs at 25 Hz like pcurve;
|
||||
JSON record in the bench data directory.
|
||||
dpatch [F] [pitch] [length] e.g. dpatch 1500 0.3 30
|
||||
flowload Cooling under laser load, one armed run per invocation, the
|
||||
conf keys it writes put back when the run ends; the pump is
|
||||
@@ -1373,8 +1375,11 @@ DPATCH_SETTLE_S = 2.0 # laser off before each patch
|
||||
# re-check (cool_flow_recheck_s, 150 s by default) has to be pushed past
|
||||
# the run's length for the session, or it lands on a patch the same way.
|
||||
DPATCH_ARM_DWELL_S = 60.0
|
||||
# The period-20 thermopile curve, light as a fraction of CW, for the labels.
|
||||
# The thermopile curves, light as a fraction of CW, for the labels: the
|
||||
# period-20 density ladder (E3) and the analog duty ladder (E1, whose
|
||||
# means above 50 % duty did not settle inside a line, so they are a prior).
|
||||
DPATCH_TP_P20 = {100: 1.0, 80: 0.53, 60: 0.37, 45: 0.21, 30: 0.07}
|
||||
DPATCH_TP_ANALOG = {100: 1.0, 80: 0.72, 60: 0.52, 45: 0.37, 30: 0.07}
|
||||
|
||||
|
||||
def dpatch_gcode(feed, width, pitch, n):
|
||||
@@ -1400,20 +1405,23 @@ def drill_dpatch(g):
|
||||
print('usage: dpatch [F mm/min >= 60] [pitch 0.05..2 mm] [length 5..200 mm]')
|
||||
return 2
|
||||
model = conf_get('laser_power_model') or 'density'
|
||||
if model != 'density':
|
||||
print('this witness is for the density model (laser_power_model is %s)' % model)
|
||||
if model not in ('density', 'analog'):
|
||||
print('unknown laser_power_model %s' % model)
|
||||
return 2
|
||||
tp_curve = DPATCH_TP_P20 if model == 'density' else DPATCH_TP_ANALOG
|
||||
unit = 'density' if model == 'density' else 'duty'
|
||||
levels, (rpm_max, rpm_min, floor, ceil) = pcurve_levels(g, DPATCH_DENSITY_PCT)
|
||||
if levels is None:
|
||||
print('PRECONDITION FAILED: cannot read $30/$31/$35/$36 (%s/%s/%s/%s)'
|
||||
% (rpm_max, rpm_min, floor, ceil))
|
||||
return 2
|
||||
n_lines = int(round(DPATCH_H / pitch))
|
||||
period = conf_get('laser_pulse_ticks') or 'driver default'
|
||||
print('=== depth witness: CW patches at speed against density patches at F%d ==='
|
||||
% feed_ref)
|
||||
print('mapping: $30=%g $31=%g $35=%g $36=%g; density base period %s ticks'
|
||||
% (rpm_max, rpm_min, floor, ceil, period))
|
||||
period = (conf_get('laser_pulse_ticks') or 'driver default') if model == 'density' else None
|
||||
print('=== depth witness: CW patches at speed against %s patches at F%d ==='
|
||||
% (unit, feed_ref))
|
||||
print('mapping: $30=%g $31=%g $35=%g $36=%g; model %s%s'
|
||||
% (rpm_max, rpm_min, floor, ceil, model,
|
||||
(', base period %s ticks' % period) if period else ''))
|
||||
patches = []
|
||||
for dose in DPATCH_CW_DOSES:
|
||||
feed = int(round(feed_ref / dose))
|
||||
@@ -1422,9 +1430,9 @@ def drill_dpatch(g):
|
||||
'label': 'CW, dose %.2f' % dose})
|
||||
for pct, sval, level in levels:
|
||||
patches.append({'row': 'B', 'pct': pct, 's': sval, 'feed': feed_ref,
|
||||
'dose': None, 'tp_says': DPATCH_TP_P20.get(pct),
|
||||
'label': '%d%% density (level %.2f%%), thermopile says %.2f of CW'
|
||||
% (pct, 100.0 * level, DPATCH_TP_P20.get(pct, float('nan')))})
|
||||
'dose': None, 'tp_says': tp_curve.get(pct),
|
||||
'label': '%d%% %s (level %.2f%%), thermopile says %.2f of CW'
|
||||
% (pct, unit, 100.0 * level, tp_curve.get(pct, float('nan')))})
|
||||
n_a = len(DPATCH_CW_DOSES)
|
||||
print('patches %g x %g mm, %d lines at %g mm pitch, %g mm apart along +X;'
|
||||
% (width, DPATCH_H, n_lines, pitch, DPATCH_GAP_X))
|
||||
@@ -1536,10 +1544,10 @@ def drill_dpatch(g):
|
||||
if p['row'] != 'B' or p['tp_says'] is None:
|
||||
continue
|
||||
nearest = min(doses, key=lambda d: abs(d[0] - p['tp_says'])) if doses else None
|
||||
print(' B%d (%d%% density): the thermopile says it should match A%d (dose %.2f); '
|
||||
print(' B%d (%d%% %s): the thermopile says it should match A%d (dose %.2f); '
|
||||
'deeper than that and the tube gives more than the sensor reads'
|
||||
% (i + 1, p['pct'], nearest[1], nearest[0]) if nearest else
|
||||
' B%d (%d%% density): no row A to match' % (i + 1, p['pct']))
|
||||
% (i + 1, p['pct'], unit, nearest[1], nearest[0]) if nearest else
|
||||
' B%d (%d%% %s): no row A to match' % (i + 1, p['pct'], unit))
|
||||
record = {
|
||||
'drill': 'dpatch', 'date': time.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||
'model': model, 'period': period, 'feed_ref': feed_ref,
|
||||
|
||||
Reference in New Issue
Block a user