mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 01:01:12 -07:00
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:
+43
-63
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user