Stream harness rules 16 and 17 with their sessions: an M5 executed with
the planner drained and the kernel run over must darken the rapids that
follow it (m5-idle), and a job whose M3 runs at the level the previous
job ended at must still fire its first cut (next-job). The second rule
is the core's contract: set_state records the rpm and the per-segment
update is skipped while it is unchanged, so the driver's set_state is
the only thing that can light that move. Both sessions run under both
dose models; the bench build that went dark on its second job fails
next-job with one fire span.
Bench drills: pcurve (a per-level ladder of 100 mm lines read from the
HV current and the head thermopile at 25 Hz, with the instrument checks
and the JSON record) and m5dark (one line, M5, two rapids, judged on the
current trace and laser_on_sampled until the armed window closes).
Catalog: laser.m5-rapid-dark, a live test of the M5 case (46 tests; the
counts in BRINGUP follow). CAMPAIGN-LOG carries the day's record: the
two curve ladders, the defect pair, the root cause, the host and bench
proof.
forgectrl pin 76115fd: the chassis LM75 and the supply sensor ride /status
as temps (degrees and a raw count), the engine ranges them over every run
session into one run-end line, and a critical fault that clears with its
session yields the reason to the standing hold.
Bench: critical_tier_drill.py (a bench tool now, registered as
critical-tier) sets the ceiling, the resume gate and the critical line a
few tenths above the live upstream reading and lets the engine's own
flow-check heater warm the loop through them inside one M8 session;
temp_calibrate.py gains supply-watch, supply-point and supply-fit for the
supply sensor against a thermometer on its heatsink, the fit shown beside
UAPI.md's unverified guess.
Catalog: cooling.gate-off checks the /status temps fields and the run-end
board-temperature line (the unit fake mirrors both, one new failure
case); cooling.critical-tier checks the reason after a faulted session.
Docs: CAMPAIGN-LOG entries for cooling.critical-tier on dev image
20260822154257 and the warm-loop drill (OVERTEMP at 10 s, CRITICAL at
14 s, the fault ending with the session); BRINGUP item 19, the facts bank
(board temperatures at idle), COOLING section 9, the bench README.
forgectrl pin 47e4256: the airflow floors set from the bench measurement
(exhaust 6400, intake 2290, air assist 6000 rpm, purge current 300, grace
15 s) and the operating-point rule: a fan is judged while the laser is
armed, when a job's profile may raise it but never lower it below the run
duty, or whenever it is commanded at the run duty; the service's hunts,
sent with the extraction fans off, are measured and published unjudged.
Catalog: cloud.mode-switch now waits out the connect-time hunt sampling
/cool/status and requires no AIRFLOW, the exhaust row unjudged and the
exhaust actually off; its covers gain forgectrl src/cool.* and
src/airflow.*. cooling.fan-gate-trips uses an 8 s test grace (the
intakes take 7 s to 90 percent and tripped under the old 2 s at the new
floor) and its off leg waits for the row state as well as gates_off.
fan_floor_measure.py names a reply that is not JSON and calls the purge
readings idle and run (the pump is always on).
Docs: COOLING 3a and the settings table, BRINGUP item 19 and a facts-bank
entry with the measured fan speeds, the bench README, and a CAMPAIGN-LOG
entry for the measurement, the two status-document finds, the hunt find
and the hot-deployed bench runs of both tests.
Two modes. spinup opens a run session with M8 from the engine's idle
posture, samples the four tachometers and the purge-air current once a
second, ends the session with M9 and waits for the engine to leave run;
per fan it reports the steady speed at run duty, the time to 90 percent
of it and the spread over the steady window, the purge current off and
on, and candidate floors at 55 percent of steady. cut sends nothing and
samples while the operator runs a real cut, for the spread the debounce
has to ride over under the only load that matters. Registered on the
bench page as fan-floor. Tooling only; no catalog consequence.
Next-work item 19 carried three open questions about the factory's
operating envelope. All three are answered now, so the item states the
policy instead of the guess.
A fan tach alert during a cut pauses the print, taking the same transition
a user pause takes, and two of the factory's three tach monitors cannot
fire at all: they treat a zero limit as unconfigured and the limits arrive
zero anyway. So a stalled extraction fan is caught by the temperature it
causes, not by its tachometer. Every temperature alert pauses as well, and
a critical fails the machine outright, which is a different state and not a
pause. The scale question behind the header's ceilings is answered by the
coolant family, which the factory carries twice, once in raw ADC counts
where the tag named min is the hot limit, and once in millidegrees. And the
factory does not verify coolant flow at all, so the cooling engine's flow
check is ahead of the factory rather than behind it.
The crash, tilt and beam-detect bullets gain the same grading: an alert
threshold that pauses and a separate abort threshold that aborts.
Also drops references to paths that resolve outside this repo, in both
documents and in two bench scripts, naming the artifact instead. In
CAMPAIGN-LOG that is a wording substitution only; no date, claim or
measurement changes.
Documentation only, so no acceptance catalog consequence.
Bump the grblHAL pin to the real-time producer change.
Add motion.step-timing-under-load: the catalog had nothing that
exercised step generation while userspace competed for the single core,
which is exactly the gap that let the condition go unnoticed - the ring
never runs dry, so cnc/underruns reads 0 through it. The test asserts
the producer and the shipper both hold SCHED_FIFO, then drives
2000 mm/min round trips against a deliberate nice-5 CPU hog and requires
the controller to report no clamped events.
Add the pthresh live-fire drill: a constant-power ladder from 2 % to
30 % of full on scrap. Because $35 is a percent of full duty and the
rungs are percents of $30 with $31 = 0, the lowest rung that marks reads
directly as the $35 value. It needs $35 = 0 for the run, or the floor
lifts every rung and hides the threshold.
Record both open items in BRINGUP. The laser one carries the finding
that the factory never uses duty as a power control - all five firing
jobs in the captured pulse files pin the power byte at 127 and modulate
dose by dithering the FIRE bit at 6.5-18.8 % density - so the captures
cannot supply a $35 default, and the duty to optical-power transfer
function of this supply has never been measured.
Catalog 34 -> 39. Every remaining bench drill of the lid/button parity work
is now a test, with drills that exercise the same path combined:
motion.lid-cancel-home also cancels from a hold - a job paused on the
button is ended by the lid, never resumed - so the
armed window and the hardware button latch stay in
agreement by construction.
motion.cancel-abort also asserts what a sender abort must NOT do: it
stops where it stopped and never returns home. The
return-to-start belongs to the lid policy alone.
motion.interlock-cancel-park (new) the interlock loop cancels like the lid,
and the lid opened during the return home does not
interrupt it.
motion.lid-policy-hold (new) the other policy: Door park, no cancel, no
return, and a cycle start finishes the move. The
setting is restored on the way out.
laser.pause-resume-live (new) the button pause/resume during a live cut:
emission stops, the latch stays UNLOCKED and the
armed window open (a pause is not a cancel), the
next press resumes and the job finishes.
cloud.interlock-abort-park (new) the same interlock/park pair in cloud mode.
cloud.pause-cancel-paths (new) the two non-finishing ends of a print, each
from the state the factory ends it in: paused on the
button then cancelled by the lid, and cancelled from
the app while running.
The shared cancel tail (reason reported, reset without an alarm, position
kept, head back at the job start with the KERNEL counters confirming it) is
now one helper, so every trigger is judged the same way.
scripts/bench/resume_dark_lead.py: samples LASER_ON, FIRE, HV_ENABLE, the
charge-pump watchdog, the button and the doors off the SoC pads at ~2 kHz
through /dev/mem, with motion dated from the kernel step counters, across a
pause and a resume. Levels are taken at idle and everything after is reported
as a change from that baseline, so no polarity assumption is baked in. Dry by
default, with --auto driving the pause and resume through ! / ~ for an
unattended rehearsal; --run live adds the dark lead between FIRE and LASER_ON,
in milliseconds and in millimeters at the job's feed.
Bench registry: argument specs can name a flag (--feed 600) instead of being
positional, and an optional argument with an empty default is left off the
command line entirely.
Host proof: 104 unit tests (20 in the cloud suite - the two new cloud tests
replay the machine's own lid-abort excerpt, with the interlock and the app
cancel substituted for the trigger, and fail for the right reasons), coverage
lint 0 uncovered across 39 tests.
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.
The dev image installs the bench tools under /usr/share/forgetest/bench/,
so flow_matrix.py and flow_warm_validate.py invoke the board-side sampler
from there instead of a hand-copied /data/flow_sampler.py; the README
follows. Bench-tool path change only, no machine behavior involved.
A stdlib-only daemon on the dev image (HTTP :8090) that runs the acceptance
catalog against the machine from a self-contained page, keeps the append-only
result log under /data/forgetest, and exports the release artifact the gate
reads. Tests declare kind (auto / operator / live), hardware (api / takeover),
coverage globs, prerequisites, and core membership; a test's domain
fingerprint is the hash of the manifest files its globs select plus the
platform and its own implementation, so a PASS stays valid exactly while
nothing it covers changed. Campaign rules: a FAIL ends the campaign, the core
(image health, kernel latch and drills, one live emission witness) is never
inherited, invalidate-all forces a full campaign, no SKIP. Live tests need the
operator acknowledgment and the physical arm press through the controller;
takeover tests stop forgectrl for the duration with a crash-recoverable
marker; the tool never touches the laser latch.
Catalog v1: 24 tests ported from the proven bench drills with their recorded
pass criteria (image, kernel K1-K3 and fire A/B/U, forgectrl API and logs,
motion incl. dead-man, cooling, live laser, camera, update, cloud). The bench
tab lists every scripts/bench tool and runs the board-side ones as
subprocesses (takeover tools wrapped). 44 host unit tests, including the gate
verification fixtures. Installed only by forgefirm-image-dev, with the bench
scripts under /usr/share/forgetest/bench.
The host-side coolant and fan tools call the ssh on PATH, or the client
named by GF_SSH (a wrapper such as a WSL distro's ssh), instead of a
fixed wrapper. The bench README lists every committed tool and data
file, and explains how the coolant-flow fire-gate threshold was
derived from the committed flow-matrix data and how to re-run that
derivation on another machine.
The head lis2hh12 (i2c-3 0x1e, direct-I2C at ~530 Hz - st_accel sysfs one-shots are ~6 Hz and the kernel has no IIO triggers) sees rail contact as a 20-40x jolt over the creep baseline within ~4 ms. bump_seek.py: 3/3 detected hits, zero false positives over ~180 mm of creep, jog-cancel stop and back-off. BRINGUP carries the full record and the driver integration design.
Sources live at github.com/ScottW514/forgectrl (history extracted, layout src/ + init/); the recipe pins SRCREV and installs the init script from the repo. Adds the build-forgectrl.sh bench cross-compile script.
Adds the design matrix and its supporting tools, and records in BRINGUP
what the 60-run matrix overturned: sub-40-percent duty mimics flow
(three of five dead-pump trials looked healthier than a working pump),
the operating point and threshold now rest on 25 pooled observations,
periodic re-checks are thermally free with the fans running, and the
settle gate closes a bench-proven miss. Also records what is NOT yet
validated - warm-loop baselines and behaviour under laser heating - as
first-light commissioning items.
Records why the temperature formula changed (and that everything
derived from the old one had to be re-derived), the flow
characterization data at both heater duties, the false negative that
killed the dT-threshold design, and the downstream-rise check that
replaced it. Adds flow_characterize.py to the bench kit.
BRINGUP records the gate as complete with tach-verified results;
LIGHTBURN.md documents the per-layer Air Assist toggle driving the
cut-profile ventilation; fan_test.py joins the bench kit.
Direct PWMSAR duty steps with the machine in the locked state
(controller stopped, cnc disabled so steppers are unpowered, laser
latch locked, lid closed; laser_on_sampled stayed 0 throughout).
Measured on the pin: 25.0 us period / 40 kHz stable across the full
duty range; 50/25/75 percent confirmed visually; low end
cursor-measured 6.4 vs 6.3 percent commanded (PWMSAR=8), clean pulse.
Matches the register-level audit numbers (divider 13 x 127 counts).
Adds pwm_sweep.py / pwm_hold.py to the bench kit and records the two
remaining pre-live-fire gates (1-tick laser drop at underrun via the
stream path with FIRE observable under a locked latch; interlock
readback semantics) in BRINGUP.
The step backend now lives in github.com/ScottW514/grblHAL-glowforge
(grblHAL driver convention: core submodule, driver.c HAL, board header).
Add build-glowforge.sh, update the BRINGUP runbook (no -t throttle,
EEPROM path, kill-before-scp, producer stats line, reset/disconnect
semantics), and teach bench_m2.py to parse the F: status field (no
spindle registered = no FS: field). Settings-write crash fix is
upstream as grblHAL/core PR 999.
Extract the factory motion profile from the _RESOURCES pulse streams
(new puls_profile.py): 700/590 mm/s2 X/Y accel on v2.6.0 firmware,
202 mm/s travel peak at the 28160 Hz travel tick, PIC currents X 135/33
and Y 22/5 run/hold, decay mode 1. Add the bench_m2.py round-trip jog +
feed-hold suite (all green 2026-08-02: sustained 200 mm/s, exact
returns, clean hold/resume, zero underruns) and update BRINGUP.md:
board moved to a fixed lease at 172.16.1.97, analog config now applied
by the sink itself, $RST=$ note for stale stored settings.
BRINGUP.md is the cold-start reference: project status (audit phases
0-5 complete, Phase 6 spike achieved - first grblHAL-commanded motion
2026-07-26), bench/board access, build+deploy procedures, the step
backend runbook incl. the required analog machine config, the measured
hardware facts bank, and the ordered next-work list. scripts/bench/
preserves the hardware-verification tools (underrun feeder, end-of-
data protocol bench, PWM register check, cross-build scripts).