mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
Prove the M101 dose-model switch; record the judged dose curves
The stream harness gains rules 18 to 21: the floor is derived from the selected model's config key at the arm and a typed $35 is overwritten; M101 switches the rendering exactly at the boundary in both directions with no continuous FIRE at full duty across it; a refused switch (the spindle on) leaves the stream unchanged, and the harness resyncs with an empty line because the core skips G-code after an error until the sender resyncs; M2 reverts a program-scoped switch and Q1 holds. The analog sessions pin laser_floor_analog at the density floor so the existing duty expectations stand, and the density ladder's unfloored run moves from a chained $35 write to the laser_floor_density key. The catalog's laser.power-floor becomes model-aware: it reads the configured model and the floor keys from forgectrl, switches to the configured model with M101 so the derivation runs without a fire, and expects $35 to be that model's floor. The new laser.power-model-switch switches to each model with the spindle off, checks the reported message and $35 after each switch, and checks the M2 revert. The new mswitch bench drill runs the switch on the machine in one armed run. Docs follow: BRINGUP's Laser control section describes the switch, the derived floors and the measured dose response of both models; the MOTION settings table gains the five keys; LIGHTBURN gains a Power models section and drops the stale 30 percent floor advice; SAFETY names the switch's refusal rule; the CAMPAIGN-LOG records the judged depth-witness runs of 2026-08-30 and the switch's host and bench proof.
This commit is contained in:
+69
-29
@@ -206,31 +206,69 @@ 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.** `laser_power_model` in the shared machine config selects how
|
||||
the shipper renders 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 - 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. The model is selected per arm
|
||||
and reported (`laser armed (density)`). 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. Under this model `$35` stops being a duty floor
|
||||
and becomes a density floor - the control that maps S onto the band that
|
||||
does useful work, which is what the factory does with its own scale. It
|
||||
ships at 10, putting a commanded 1 % at 10.2 % density; **selecting `analog`
|
||||
means raising it to ~16**, the duty this tube lases at, and the arm warns on
|
||||
either mismatch (a zero floor under density, a sub-lasing one under analog). 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.** 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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
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
|
||||
@@ -1306,10 +1344,12 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
this model.
|
||||
|
||||
**The defaults are flipped:** `laser_power_model` defaults to `density`
|
||||
and `$35` to 10, the density floor, so a stock machine runs the model
|
||||
and a commanded 1 % marks. The analog path remains as `laser_power_model
|
||||
= analog`, and a machine switched to it must raise `$35` to ~16 or low
|
||||
S lands in the duty dead band; the arm warns on either mismatch.
|
||||
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).
|
||||
|
||||
Owed: validation at production feeds. Every ladder behind these
|
||||
defaults ran at F300 or F100, where dose per millimeter is generous and
|
||||
|
||||
@@ -5986,6 +5986,20 @@ Closed: the arm is decided by the window alone (grblHAL-glowforge a7dcdca), an R
|
||||
`serial.c`.
|
||||
|
||||
|
||||
### The dose curves judged on material, and the model switch built, 2026-08-30
|
||||
|
||||
Bench, dev image 20260829220329, three `dpatch` depth-witness runs, each two rows of 30 x 4 mm serpentine fills (row A CW at six feeds for relative doses 1.0 to 0.25, row B at the reference feed at 100 / 80 / 60 / 45 / 30 % of the model's range), the operator matching each row-B patch to the row-A patch of equal depth by eye:
|
||||
|
||||
- Density on Thick Draftboard at F1500 (`bench-data/dpatch_20260830-172227.json`) and on acrylic at F1125 (`dpatch_20260830-173933.json`): the operator's verdict on both, "sensor predictions are accurate", B8 (80 % density) = A4 (CW at half the dose), B9 = A5, B10 = A6, B7 = A1. Thermopile 0.47 / 0.35 / 0.19 / 0.07 of CW at 80 / 60 / 45 / 30 % (0.45 / 0.34 / 0.19 / 0.06 on acrylic), tube current 0.56 / 0.45 / 0.39 / 0.35. Row A flat within 17 % and 16 %. The density curve is the tube's, not the sensor's, and the pending question from 2026-08-25 (B8 = A4 or A2) is closed on A4. By eye: every box deeper at both ends (the reversal slow-down under constant power), a slight line pattern from the 0.3 mm serpentine, no dot pattern.
|
||||
- Analog on acrylic at F1125 (`dpatch_20260830-175759.json`), the drill extended to run under `laser_power_model = analog` (forgefirm f88c778): "they match the sensors", B8 (80 % duty) = A2, B9 between A2 and A3, B10 = A4, B11 = A5; light 0.82 / 0.68 / 0.54 / 0.37 of CW at 80 / 60 / 45 / 30 % duty, current 0.80 / 0.61 / 0.46 / 0.31. The 2026-08-25 ladder's prior (0.72 / 0.52 / 0.37 / 0.07) was low, worst at 30 % duty where its F600 lines sat in the unstable discharge band. The thermopile drifted inside this run (row A 1733 down to 1290, baseline 1835 up to 1896), so the ratios carry more uncertainty than the density runs.
|
||||
- Finish, the two acrylic runs side by side: "finish is the same"; the only visible pattern is present at every power, under CW and under 100 % density (continuous fire) alike, so it is mechanical, not the dither. Operator's decision: the analog model stays and is developed as a second, near-linear model.
|
||||
|
||||
Each run: `cool_flow_recheck_s = 600` for the run and removed after, the `dpatch` record copied to the tree, `/tmp` cleaned, `laser_power_model` put back to `density`, the machine idle and disarmed.
|
||||
|
||||
Built the same day, host-proven, no fire: the dose-model switch. `M101 P0` (analog) / `M101 P1` (density) as a driver M-code, refused with the spindle commanded on (error 253, reason reported) or the controller not idle, program-scoped with `Q1` to stick; the per-model floors as config keys (`laser_floor_density` 10, `laser_floor_analog` 16) loaded into `$35` in RAM at every arm and switch with the PWM mapping re-precomputed, the stored setting never written; the stream leading the first run after a switch dark; the cooling report carrying the model in force (`model=` on `POST /cool/state`, the engine preferring it to the config key for the tube-heat share); the five `laser_*` keys in forgectrl's settings whitelist and the panel's GRBL tab with help text; `laser.power-floor` made model-aware and the new catalog test `laser.power-model-switch`. Proof: `tests/laser_arm_test.c` cases J to O (derived floors, validate and execute refusals, switch, revert, Q1, reset), `laser_stream_test.py` rules 18 to 21 (a typed `$35` overwritten at the arm; both switch directions rendering exactly at the boundary with no continuous FIRE at full duty across it; the refusal leaving the stream unchanged; `M2` reverting and `Q1` holding), forgectrl's host tests. Found on the way: the core skips every G-code line after an error until the sender resyncs with an empty line or a `$` command (`protocol.c`), which is how the harness now follows a refused switch. One flake, not a defect: rule 13's mask comparison slipped one tick at the tail while a build ran alongside in the same VM (the shipper is wall-paced); three runs alone were identical.
|
||||
|
||||
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.
|
||||
|
||||
## Reference notes
|
||||
|
||||
### Head-IRQ source validation — the beam-emission hypothesis
|
||||
|
||||
+37
-3
@@ -147,6 +147,39 @@ restart the controller with the head re-parked.)
|
||||
- **Console tab**: raw grbl — `?` status, `$$` settings, `$X` unlock,
|
||||
`$J=G91X10F1200` jog.
|
||||
|
||||
## Power models
|
||||
|
||||
The controller has two ways to turn a layer's power into light, and a job
|
||||
can pick either.
|
||||
|
||||
- **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.
|
||||
|
||||
## Air assist / fans
|
||||
|
||||
Each cut/engrave layer has an **Air Assist** toggle (in the layer's cut
|
||||
@@ -188,6 +221,7 @@ decay mode), and the head finishing per the job's return setting.
|
||||
|
||||
4. For the live pass: put scrap material on the bed (never an empty
|
||||
honeycomb over the fan grill), re-enable the layer's **Output**, set
|
||||
power to **30 %** or more (below ~30 % the tube barely marks), turn
|
||||
the layer's **Air Assist** on, close the lid, **Frame**, **Start**,
|
||||
and press the white button when it lights. Watch the whole job.
|
||||
power to **20 %** (any nonzero power marks under the default power
|
||||
model; 20 % is a light pass on scrap), turn the layer's **Air Assist**
|
||||
on, close the lid, **Frame**, **Start**, and press the white button
|
||||
when it lights. Watch the whole job.
|
||||
|
||||
@@ -426,6 +426,11 @@ 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_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. |
|
||||
| `cloud_pause_backtrack_ticks` | 2000 | Cloud pause: laser-off backtrack after the stop. |
|
||||
| `cloud_resume_lead_ticks` | 1950 | Cloud resume: laser-off lead before firing again. |
|
||||
|
||||
@@ -203,6 +203,14 @@ 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.
|
||||
- **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