Prove the corner rolloff; document the one-press recorder

The stream harness gains rule 20: under M4 with the curve in force, the
accelerate-in head of a line renders less density at the default gamma
of 2 than at gamma 1 (0.317 against 0.537 on the host grid) while the
cruise middle renders identically - the exponent shapes only the
velocity-scaled rolloff, never the programmed level. The panel-serves
covers widen to the whole recorder (src/curverec.*). BRINGUP describes
the rolloff and the one-press recorder; the MOTION table gains
laser_corner_gamma; the panel help explains the knob.
This commit is contained in:
ScottW514
2026-08-30 21:48:32 -04:00
parent 5787260a3d
commit 58cad9a8d8
4 changed files with 54 additions and 6 deletions
+12 -5
View File
@@ -250,11 +250,18 @@ pairs, ships with the bench-measured default compiled in, accepts
`off` for the identity, falls back loudly on a bad value, and is
reloaded at every precompute with the arm naming it (`laser armed
(density, floor 10 %, curve bench-default)`). `$35`/`$36` still floor
and ceil the result. An owner measures their own curve from the panel:
the dose-curve recorder hands them a ladder G-code to run from their
sender - every arm gate stands, forgectrl records the tube current and
the head thermopile passively, fits the rungs, and Apply writes the
result (`forgectrl/docs/SERVICES.md`). Rasters hold their tonality down
and ceil the result. Under M4 the curve makes delivered light exactly
proportional to velocity - and heat still accumulates where the head
slows, so corners over-burned on the bench; `laser_corner_gamma`
(default 2, range 0.25 to 4) bends the rolloff to light proportional to
(v/v_programmed)^gamma, starving the slow spots the way the raw convex
mapping used to by accident, while the programmed level at speed stays
exactly the curve's. An owner measures their own curve from the panel:
the dose-curve recorder streams the ladder job itself from one Record
press (absolute from X0 Y0, refused while a sender is connected; the
operator's button press starts the fire with every arm gate standing),
records the tube current and the head thermopile, fits the rungs, and
Apply writes the result (`forgectrl/docs/SERVICES.md`). 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.
+1
View File
@@ -428,6 +428,7 @@ only be changed while the machine is idle.
| `laser_disarm_s` | 60 | Spindle-off grace before the armed window closes. |
| `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_dose_curve` | (bench default) | The measured dose curve as density:light percent pairs; S commands a light fraction and the driver maps it onto the density that delivers it. `off` = identity; a bad value falls back to the default. The panel's recorder measures and applies a machine's own. |
| `laser_corner_gamma` | 2 | The corner rolloff under M4: delivered light follows (v/v_programmed)^gamma, so 1 is plain proportionality and higher values starve the slow spots where heat accumulates. Rides the curve. |
| `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. |
+1 -1
View File
@@ -223,7 +223,7 @@ def settings_bounds(ctx):
covers=[("forgectrl", "src/ui.*"), ("forgectrl", "src/ui/**"), ("forgectrl", "src/status.*"),
("forgectrl", "src/cam.c"), ("forgectrl", "src/main.c"), ("forgectrl", "src/super.c"),
("grblhal-glowforge", "src/glowforge_status.c"), ("grblhal-glowforge", "src/serial.c"),
("forgectrl", "src/curverec.c")],
("forgectrl", "src/curverec.*")],
description="The panel page is served, /status carries the machine telemetry the panel and "
"the acceptance tool read (including the sys block: CPU busy percent over the "
"interval since the previous read, memory used percent), and /cam/status "
+40
View File
@@ -68,6 +68,11 @@ over TCP, then checks the dumps against the kernel feeder contract:
near 80 percent density), monotonic, floored and ceiled by
$35/$36; every other session runs with laser_dose_curve = off so
its S-to-level arithmetic stays exact
20. the corner rolloff starves the slow spots: under M4 with the curve
in force, the accelerate-in head of a line renders less density at
the default gamma of 2 than at gamma 1, and the cruise middle
renders the same - the exponent shapes only the velocity-scaled
rolloff, never the programmed level
The analog sessions select the reference mode through the config; on
hardware the controller ignores it (density is the only product model -
@@ -861,6 +866,41 @@ def main():
"ladder renders through the %g %% floor key, levels %s"
% (PWM_MIN_PCT, list(expect_levels)))
# --- rule 20: the corner rolloff starves the accel head -------------
# One long M4 line from rest under the curve, at gamma 1 and the
# default 2. The accelerate-in head runs velocity-scaled; its
# rendered density must drop with the exponent while the cruise
# middle stays put.
# F6000 = 100 mm/s: the accel from rest lasts ~143 ms (~4000 ticks),
# so the first 2000 ticks are genuinely velocity-scaled.
JOB_M4_LONG = ["G91", "G21", "M4 S1000", "G1 X60 F6000", "M5"]
head_ticks = 2000
dens_head = {}
dens_mid = {}
for gname, gconf in (("g1", "laser_corner_gamma = 1\n"), ("g2", "")):
data = run_session("rolloff-" + gname, JOB_M4_LONG,
conf=DENSITY_CONF_CURVED + gconf)
ticks = tick_bytes(data)
spans = fire_spans(ticks)
if len(spans) != 1:
fail("[rolloff-%s] %d fire spans, expected 1" % (gname, len(spans)))
a, b = spans[0]
seg = ticks[a:b]
head = seg[:head_ticks]
mid_a = len(seg) // 2 - 2000
mid = seg[mid_a:mid_a + 4000]
dens_head[gname] = sum(1 for t in head if t & 0x10) / float(len(head))
dens_mid[gname] = sum(1 for t in mid if t & 0x10) / float(len(mid))
if not dens_head["g2"] < dens_head["g1"] - 0.02:
fail("[rolloff] gamma 2 does not starve the accel head (g1 %.3f, g2 %.3f)"
% (dens_head["g1"], dens_head["g2"]))
if abs(dens_mid["g2"] - dens_mid["g1"]) > 0.02:
fail("[rolloff] gamma changed the cruise density (g1 %.3f, g2 %.3f): it "
"must shape only the rolloff" % (dens_mid["g1"], dens_mid["g2"]))
print("PASS [rolloff]: accel-head density %.3f at gamma 1 -> %.3f at the "
"default 2; cruise %.3f alike" % (dens_head["g1"], dens_head["g2"],
dens_mid["g1"]))
# --- rule 19: the dose curve bends S onto delivered light -----------
cur = run_session("curve", JOB_CURVE, conf=DENSITY_CONF_CURVED)
if "curve bench-default" not in run_session.text: