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