Prove the density-only model; record the rasters and the decision

The stream harness keeps the analog rendering as the host-test
conservatism reference (rule 13's mask, the duty ladders) and drops the
M101 switch sessions; rule 18 stays as the derived-floor proof, now
satisfied from boot by the precompute. The lifecycle harness's
state-files scenario asserts the derived floor is in $$ before any arm.
The catalog's laser.power-model-switch goes and laser.power-floor reads
the one floor key with no M-code needed. The mswitch drill goes; the
m4corner drill becomes a single density pass; the dpatch drill returns
to density only.

The CAMPAIGN-LOG records the first rasters (254 and 508 DPI grayscale
wedges: tonality held to ~14 pulse slots per pixel, no dither artifact,
one benign stale-verdict suppression under CPU starvation) and the
decision that ends the analog mode - the strike transient fires a spot
at every beam-on, and the finish comparison found no advantage. BRINGUP,
LIGHTBURN, MOTION and SAFETY describe the density-only present.
This commit is contained in:
ScottW514
2026-08-30 19:41:50 -04:00
parent 67da1ab9bc
commit d612c0099a
10 changed files with 355 additions and 615 deletions
+43 -63
View File
@@ -206,69 +206,48 @@ of the first tick byte it covers, FIRE as bit 4 OR'd into tick bytes. The
spindle PWM is precomputed to a period of exactly 127, so computed values ARE
power bytes (`$30` default 1000 → S1000 = 127).
**Dose model.** Two models render the per-segment value the core computes:
`density` (the default) pins the duty at full and modulates the FIRE bit,
`analog` ships the value as a power byte instead. Density uses a base
period of `laser_pulse_ticks` (default 20 = 710 us at 28160 Hz, the
factory's ~1.43 kHz) whose on-count is dithered between adjacent integers
with the remainder carried, so densities finer than one tick per period
average out. Density is what the tube's dead band below its lasing
threshold requires: every pulse it emits is full-power, so no commanded
level lands in the band, and a level change inside a run costs no stream
byte at all. `laser_pulse_min_ticks` (default 3 = 106 us) is the shortest
pulse it will emit: below it a period is skipped and its debt carried, so a
faint level arrives as fewer full-width pulses instead of stubs the supply
cannot strike - measured on the bench, a 36 us stub draws no discharge at
all, and the factory never emits below one of its 100 us ticks. The debt is
conserved, so the average density is unchanged: at level 2 the stream goes
from 444 one-tick bursts to 147 three-tick bursts, same density to four
decimals. Structurally the model is a mask on the core's fire state and
never a source of one, so emission stays exactly where the core commanded
it.
**Dose model: density, the only one.** The shipper renders the
per-segment value the core computes by pinning the duty at full and
modulating the FIRE bit on a base period of `laser_pulse_ticks` (default
20 = 710 us at 28160 Hz, the factory's ~1.43 kHz) whose on-count is
dithered between adjacent integers with the remainder carried, so
densities finer than one tick per period average out. Every pulse is
full-power, so no commanded level can land in the tube's dead band, and
no beam-on ever dwells: the analog alternative (duty as the power byte,
continuous FIRE) fires the strike transient as a visible spot at every
turn-on whatever the power, and was removed as a product mode for it -
the rendering survives only as the host harness's conservatism
reference, selectable solely in the null-sink build.
`laser_pulse_min_ticks` (default 3 = 106 us) is the shortest pulse the
model emits: below it a period is skipped and its debt carried, so a
faint level arrives as fewer full-width pulses instead of stubs the
supply cannot strike - measured on the bench, a 36 us stub draws no
discharge at all, and the factory never emits below one of its 100 us
ticks. The debt is conserved, so the average density is unchanged: at
level 2 the stream goes from 444 one-tick bursts to 147 three-tick
bursts, same density to four decimals. Structurally the model is a mask
on the core's fire state and never a source of one, so emission stays
exactly where the core commanded it.
**Selecting the model.** `laser_power_model` in the shared machine config
(the control panel's GRBL tab) is the boot default. A job selects its own
with the driver M-code `M101 P0` (analog) or `M101 P1` (density), sent with
the spindle off: the switch is refused (`error:253`, reason reported) with
the spindle commanded on or the controller not idle, because a model change
under fire could pair density's pinned full duty with analog's continuous
FIRE; between kernel runs, which end dark and lead with a power byte, there
is no torn state to reach. The planner drains before the switch, the armed
window stays open across it, so `M5` / `M101` / `M3` inside a job switches
models between its sections with no new press. The switch is
program-scoped: it reverts to the boot default at `M2`/`M30` and on a soft
reset, so a job header can declare the model it needs without leaving the
machine in it; `M101 P<n> Q1` sticks until the next switch or a controller
restart. The stream leads the first run after a switch dark (duty 0) until
the commanded power lands, so neither model's number is ever shipped under
the other.
**The floor is derived, never typed.** Each model has an S-range floor as a
config key: `laser_floor_density` (default 10, the lowest density that
still marks, putting a commanded 1 % at 10.2 % density) and
`laser_floor_analog` (default 16, the duty this tube lases at; 3 to 14 % is
a dead band). At every arm and every switch the controller loads the
selected model's floor into `$35` in RAM and re-precomputes the PWM
mapping; the stored `$35` is never written, `$$` reports the floor in
force, and a `$35` typed by a sender is overwritten at the next arm. The
arm and switch reports name both (`laser armed (density, floor 10 %)`,
`laser power model set for this program (analog, floor 16 %)`), and a floor
of 0 is honored with a note (the ladders run that way). The cooling report
carries the model in force (`model=` on `POST /cool/state`) so the engine's
tube-heat share follows an `M101` as well as the default.
**The floor is derived, never typed.** `laser_floor_density` (default 10,
the lowest density that still marks, putting a commanded 1 % at 10.2 %
density) is loaded into `$35` at every spindle precompute - boot
included - in RAM only: the stored `$35` is never written, `$$` reports
the floor in force, and a `$35` typed by a sender is overwritten on the
spot. The arm report names it (`laser armed (density, floor 10 %)`), and
a floor of 0 is honored with a note (the ladders run that way). The
cooling report carries the model with the job state.
**Measured dose response (this bench, 2026-08-30, by the head thermopile,
the tube current and the operator's eye on Thick Draftboard and acrylic).**
Density delivers about half of the CW light at 80 % density, a third at
60 %, a fifth at 45 % and a fourteenth at 30 %: the curve is the tube's
(pulsed against CW), not the sensor's, and it is the same physics behind the
factory's 18.9 to 79.5 % mapping with Full Power kept apart. Analog is
close to linear above 30 % duty (0.82 / 0.68 / 0.54 / 0.37 of CW at 80 /
60 / 45 / 30 % duty, the tube current equal to the duty) with the lasing
knee at 20 to 23 % duty below that. The finish on acrylic is the same under
both models, and the only visible pattern is mechanical (present under CW
too), so the models differ in their S scale, not their mark. A per-model S
correction (E4 in the working file) is the open item that follows.
(pulsed against CW), not the sensor's, and it is the same physics behind
the factory's 18.9 to 79.5 % mapping with Full Power kept apart. An S
correction from that curve (E4 in the working file) is the open item that
follows. Rasters hold their tonality down to ~14 pulse slots per pixel
(508 DPI at 6000 mm/min): the dither accumulator's cross-pixel averaging
recovers the levels, with no visible dither pattern.
An S word takes effect whether or not motion is in progress. Per-segment
updates carry the level inside a laser block, but an S executed between
@@ -319,7 +298,7 @@ on a clean re-check; FAULT leaves the hold and the gate for the operator.
never open the Grbl socket (a connection displaces the sender), so the
controller writes two files under `/run/forgefirm`, atomically, on
edges: `grbl.settings` (the `$$` view, rewritten on every setting
change, the M101 switch included) and `grbl.state` (JSON: machine state
change and whenever the derived floor moves) and `grbl.state` (JSON: machine state
and alarm, the sender session with peer and generation, the laser's
armed window and dose model with its floor, the exact `[GC:...]` modal
report, overrides, driver version, `ts_mono` for age; on change plus a
@@ -1359,11 +1338,12 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
**The defaults are flipped:** `laser_power_model` defaults to `density`
with a 10 % floor (`laser_floor_density`), so a stock machine runs the
model and a commanded 1 % marks. The analog path remains as
`laser_power_model = analog` or `M101 P0` in a job, with its own floor
key (`laser_floor_analog`, 16); the controller derives `$35` from the
selected model's key at every arm and switch, so no floor is typed and
no mismatch exists ("Laser control (GRBL mode)" above).
model and a commanded 1 % marks. The analog rendering is not a
product mode (it fires the strike transient as a spot at every
beam-on; removed 2026-08-30) and survives only as the host harness's
conservatism reference; the controller derives `$35` from
`laser_floor_density` at every precompute, so no floor is typed
("Laser control (GRBL mode)" above).
Owed: validation at production feeds. Every ladder behind these
defaults ran at F300 or F100, where dose per millimeter is generous and
+14
View File
@@ -6002,6 +6002,20 @@ Built the same evening, host-proven: the controller's published state (the C6 ga
Bench-proven the same day, one armed run of the new `mswitch` drill on the hot-installed cross-built binaries (forgectrl md5 4dc4677a, grblHAL_glowforge 17e5502f, operator-run install): the arm reported "laser armed (density, floor 10 %)", `M5` then `M101 P0` answered ok and reported "laser power model set for this program (analog, floor 16 %)", `$$` read `$35=16` while analog was in force and `$35=10` again after `M2` reported the revert, the armed window carried across the switch with no re-prompt, and the 25 Hz trace showed exactly two discharge segments, the density line pulsed (hv mean 390, max-mean 547) and the analog line steady (mean 567, max-mean 28), dark after the second `M5` (hv max 0). Board cleaned; the hot-deployed binaries stay until the next flash, `/tmp/*.prev` is the rollback.
### M4 into corners under both models, no dropouts, 2026-08-30
Image 20260830200842 (fresh flash; the C6 state files verified live on first boot, the sender field exercised end to end). The `m4corner` drill, one armed run: a corner-heavy pattern (10 mm line, 1.2 mm teeth, 2 mm square, 5 mm reversal, 0.5 mm teeth) at S300 F2000 under M4, cut under density and again under analog via `M101 P0`, passes offset 8 mm (`bench-data/m4corner_20260830.log`). Operator: no unmarked commanded segment on either pass - the derived floors close the analog dead-band dropout, and density cannot reach one. Artifacts as the curves predicted: analog scorches the line start and the square corners (floor 16 plus M4 dwell) and reads ~0.35 of CW at the same commanded 30 % where density reads ~0.12-0.15 and runs faint; the operator accepts density's weakness pending E4 (the per-model S correction). The M101 switch, its report, the M2 revert and two clean discharge windows all held on the shipped image. Items A3 and B4 of the working file close.
### The density time base across feeds: even all the way, 2026-08-30
The `m4feeds` drill on image 20260830200842, one armed run: a 60 mm U per feed (out and return legs 0.6 mm apart, the turn at the far end) at S1000 under M4 density, F1000 and F4000, passes 5 mm apart (`bench-data/m4feeds_S1000_20260830.log`; a first 20 mm retraced attempt was unreadable, `m4feeds_20260830.log`). At cruise the beam read identically at both feeds (thermopile 3202 / 3184, current 961 / 895), as M4 commands; the operator read the material "even all the way" - end to middle and through the turns, both feeds. B3 of the working file closes: M4's velocity scaling holds dose per mm through the accel, the per-tick time base stands, no per-step base is needed.
### The first rasters, and the decision that ends the analog mode, 2026-08-30
B5 on image 20260830200842, the generated grayscale wedge (`bench-data/gray-wedge.png`) from LightBurn in Image/Grayscale mode under density. Run 1, 254 DPI at 3000 mm/min (37 min, one kernel run of 23.5M callbacks): every band distinguishable, the ramp graduated, no pattern beyond the mechanical stepper signature, no dropout; the dark third saturates into char at 100 % layer power (dose, not modulation). One mid-job "fire suppressed: coolant" warning traced to a single stale-verdict beat under CPU starvation (the controller logged "1 late events clamped ... step generation was starved of CPU", min pacing margin 0.0 ms); the flow re-checks passed all job with the tube share off, the ceiling gates on the upstream sensor and never tripped, and nothing shows on the material. Run 2, 508 DPI at 6000 mm/min (39 min, 25.1M callbacks, min margin 13.3 ms, zero clamps, no suppression): tonality held at ~14 pulse slots per pixel - the dither accumulator's cross-pixel averaging recovered the levels - no coarsening, no moire. Witness photos `bench-data/raster_run1_254dpi_20260830.jpg` and `raster_run2_508dpi_20260830.jpg`.
The analog pass (A5) was cancelled by the operator's decision that ends the mode: analog fires a spot at every turn-on, low power included - the strike transient, seen at A1 as the start-of-line spots and at the corner drill as the scorch halo - and the finish comparison had already found no advantage (identical finish on acrylic). Density is the only product model from here; the analog rendering survives only as the host harness's conservatism reference (the mask rule), selectable only in the null-sink build.
## Reference notes
### Head-IRQ source validation — the beam-emission hypothesis
+13 -30
View File
@@ -147,38 +147,21 @@ restart the controller with the head re-parked.)
- **Console tab**: raw grbl — `?` status, `$$` settings, `$X` unlock,
`$J=G91X10F1200` jog.
## Power models
## Power
The controller has two ways to turn a layer's power into light, and a job
can pick either.
The controller drives the tube the way the factory does: every pulse
fires at full power, and the power setting decides how many ticks of
each 710 us period fire. Every power level marks, low levels included,
because no pulse is ever too weak to strike. The response is not linear:
on this machine 80 % gives about half the light of 100 %, 60 % about a
third, 30 % about a fourteenth - so pick engrave power by test card, and
prefer setting darkness with speed. Grayscale images fade cleanly into
the shadows (a low level becomes sparse full-power pulses), and 254 to
508 DPI rasters hold their tonal steps.
- **Density** (the default): every pulse is full power, and the power
setting decides how many ticks of each 710 us period fire. This is what
the factory does. Every power level marks, low levels included, because
no pulse is ever too weak to strike; the trade is that the response is
not linear: on this machine 80 % gives about half the light of 100 %, 60 %
about a third, 30 % about a fourteenth.
- **Analog**: the beam runs continuously and the power setting sets the PWM
duty. Close to linear above 30 %, with a floor at the duty the tube lases
at (16 %), below which nothing marks. The finish on acrylic is the same
as density's.
The default model is set on the control panel (GRBL tab, "Laser power
model"). A job selects its own with a line in its G-code, with the laser
off:
M5 ; beam off (the switch is refused while the spindle is on)
M101 P0 ; analog for this program (M101 P1 = density)
M3 ...
Put it in the job's start G-code (Edit -> Device Settings -> GCode -> Start
G-Code in LightBurn) or between sections of a job. It reverts to the panel
default when the program ends (`M2`) or on Stop, so a job never leaves the
machine in a model you did not pick; `M101 P0 Q1` typed in the Console
sticks until the next `M101` or a controller restart. The console reports
every switch and every arm with the model and its floor, and `$35` (the
power floor) is set by the controller from the selected model: do not type
it, it is overwritten at the next job.
`$35`, the power floor, is set by the controller from the machine config
(the control panel's GRBL tab, "Laser dose"): do not type it, it is
overwritten at every job.
## Air assist / fans
+1 -3
View File
@@ -426,9 +426,7 @@ only be changed while the machine is idle.
| `lid_policy` | `cancel` | `cancel` = factory behavior; `hold` = stock Grbl door parking. |
| `laser_button_timeout_s` | 300 | How long the machine waits at the button prompt. |
| `laser_disarm_s` | 60 | Spindle-off grace before the armed window closes. |
| `laser_power_model` | `density` | The boot-default dose model: `density` (full-power pulses, dose by pulse density) or `analog` (continuous beam, dose by PWM duty). A job overrides it with `M101 P0` / `M101 P1`. |
| `laser_floor_density` | 10 | The S-range floor under density, percent of full; loaded into `$35` at every arm and switch. |
| `laser_floor_analog` | 16 | The S-range floor under analog, percent duty (the duty the tube lases at); loaded into `$35` the same way. |
| `laser_floor_density` | 10 | The S-range floor, percent of full: the lowest pulse density that still marks. Loaded into `$35` at every spindle precompute; `$35` is derived, never typed. |
| `laser_pulse_ticks` | 20 | Density base period in machine ticks (35.5 us each). |
| `laser_pulse_min_ticks` | 3 | Shortest density pulse in ticks; below it a period is skipped and its debt carried. |
| `rail_settle_s` | 2.5 | Motor-rail off period when a controller takes the device standalone. |
+6 -8
View File
@@ -203,14 +203,12 @@ would not allow.
under a job left standing, so a long pause ends with the machine disarmed
and the next emission needs a fresh press. The relock waits for the kernel to
finish the queue tail so a controlled stop can never leave FIRE driven.
- **Dose-model switch.** `M101` changes how a commanded power is rendered
(full-power pulses at a density, or a continuous beam at a duty). It is
refused with the spindle commanded on or the controller not idle, because
a change under fire could pair the density model's pinned full duty with
the analog model's continuous FIRE; between kernel runs, which end dark
and lead with a power byte, no torn state exists. The first run after a
switch leads dark (duty 0) until the commanded power lands. A switch
reverts at program end and on reset unless the operator made it stick.
- **Dose model.** Density is the only model: every pulse fires at full
power and the commanded level only masks FIRE ticks the core asked for,
never adds one, so emission stays exactly where the core commanded it.
The analog rendering (continuous FIRE at a duty) is not selectable on a
machine - it fires the tube's strike transient as a spot at every
beam-on - and exists only as the host harness's conservatism reference.
- **Coolant fire gates.** The armed window requires a fresh `fire_ok` verdict
from the cooling engine (flow verification, over-temperature, the airflow
floors on every fan, lid-IR emission witness); a stale or failed verdict