mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
Cover both dose models explicitly now that density is the default
grblHAL-glowforge 03d70e0, pinned here, makes density the shipped model and $35 the density floor at 10. Every analog session in the stream harness now selects its model rather than inheriting it: without that the flip would have quietly turned them into density runs and taken the analog fallback's coverage with them. laser.power-floor carries the new floor and its PWMSAR minimum, and its description no longer describes $35 as a duty floor. All ten stream sessions pass on the new defaults, with the analog duties shifting exactly as the floor predicts - min_value 12, gradient 0.115 - along with both C harnesses and the lifecycle harness. Recorded in BRINGUP and the campaign log, including what the defaults rest on and what they do not: the seven ladders ran at F300 and F100 at constant power, so production feeds, M4 into corners and the raster path remain unproven on hardware.
This commit is contained in:
+21
-12
@@ -203,9 +203,9 @@ 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: `analog` (the
|
||||
default) ships it as a power byte, `density` pins the duty at full and
|
||||
modulates the FIRE bit instead - a base period of `laser_pulse_ticks`
|
||||
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
|
||||
@@ -221,7 +221,10 @@ 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. Structurally the model is a
|
||||
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.
|
||||
|
||||
@@ -1138,14 +1141,20 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
onto that band, which is what the factory does and what `$35` is under
|
||||
this model.
|
||||
|
||||
Owed: the shipping defaults. The model, the minimum pulse and the
|
||||
scale are all proven on hardware, but `laser_power_model` still
|
||||
defaults to `analog`, so nothing of this reaches a machine until the
|
||||
key is set. Flipping the default means `$35` must move with it - 16 is
|
||||
the analog duty floor, 10 is the density floor, and the wrong pairing
|
||||
is a dead band either way - and it wants one real job at a production
|
||||
feed first: every ladder here ran at F300 or F100, where dose per
|
||||
millimeter is generous, and no raster has run at all.
|
||||
**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.
|
||||
|
||||
Owed: validation at production feeds. Every ladder behind these
|
||||
defaults ran at F300 or F100, where dose per millimeter is generous and
|
||||
the power is constant - none of them exercised M4's velocity scaling
|
||||
into corners, a real sender's level changes, or the raster path, which
|
||||
has not run at all. The arithmetic says dotting will not be the
|
||||
problem (at 10 % density the pulse interval is 1.07 ms, which at
|
||||
2000 mm/min is 35 um against a ~200 um spot), but that is reasoning,
|
||||
not a cut.
|
||||
|
||||
For reference, the factory maps its whole 1-100 power
|
||||
scale onto density 18.9-79.5 % (fit from the three captures; Full Power
|
||||
|
||||
+29
-6
@@ -3042,12 +3042,35 @@ level real pulses, the minimum pulse to keep them strikeable, and the floor to
|
||||
put the user's range on the band that works — the same three pieces the
|
||||
factory uses, arrived at from this bench's own measurements.
|
||||
|
||||
Owed: the shipping defaults. `laser_power_model` still defaults to `analog`,
|
||||
so none of this reaches a machine until the key is set, and `$35` must move
|
||||
with the model (16 is the analog duty floor, 10 the density floor; the wrong
|
||||
pairing is a dead band either way). Both want one real job at a production
|
||||
feed first — every ladder here ran at F300 or F100, where dose per millimeter
|
||||
is generous, and no raster has run at all.
|
||||
### The defaults flipped
|
||||
|
||||
`laser_power_model` now defaults to `density` and `$35` to 10, so a stock
|
||||
machine runs the model and a commanded 1 % marks. The analog path stays as an
|
||||
explicit `laser_power_model = analog`.
|
||||
|
||||
The two settings are coupled and the pairing matters: `$35` is a **density**
|
||||
floor under the shipped model and a **duty** floor under the fallback, wanting
|
||||
~10 and ~16 respectively, and the wrong pairing is a dead band in either
|
||||
direction. The arm warns on both mismatches — a zero floor under density,
|
||||
where the bottom of the S range asks for pulses too far apart to re-strike,
|
||||
and a sub-lasing floor under analog.
|
||||
|
||||
Test-side consequences worth noting, since the default reaches into the
|
||||
harness: every analog session in `laser_stream_test.py` now selects its model
|
||||
explicitly rather than inheriting it, or the flip would have silently turned
|
||||
them into density runs and taken the analog fallback's coverage with them.
|
||||
`laser_arm_test` asserts the inverse of what it used to — no config key now
|
||||
means density — and `laser.power-floor` carries the new floor and its
|
||||
PWMSAR minimum. All ten stream sessions, both C harnesses and the lifecycle
|
||||
harness pass on the new defaults; the analog duties shift exactly as the new
|
||||
floor predicts (min_value 12, gradient 0.115).
|
||||
|
||||
Owed: validation at a production feed. Every ladder behind these defaults ran
|
||||
at F300 or F100 at constant power, so none of them exercised M4's velocity
|
||||
scaling into corners, a real sender's mid-run level changes, or the raster
|
||||
path, which has not run at all. The arithmetic says dotting will not be the
|
||||
problem — at 10 % density the pulse interval is 1.07 ms, 35 µm at
|
||||
2000 mm/min against a ~200 µm spot — but that is reasoning, not a cut.
|
||||
|
||||
## Superseded status notes
|
||||
|
||||
|
||||
@@ -26,10 +26,13 @@ _LASER_COVERS = [("grblhal-glowforge", "src/**"), ("kernel-module-glowforge", "*
|
||||
("forgectrl", "src/status.c"), ("forgectrl", "src/main.c")]
|
||||
|
||||
# boards/glowforge.h: DEFAULT_SPINDLE_PWM_MIN_VALUE against the hardware's
|
||||
# 127-count PWM period. The tube's measured lasing threshold is PWMSAR 20.
|
||||
POWER_FLOOR_PCT = 16.0
|
||||
# 127-count PWM period. Under the shipped FIRE-density dose model this is
|
||||
# a density floor: the bottom of the S range maps onto it, measured as
|
||||
# the lowest level that still marks. (Under the analog fallback the same
|
||||
# setting is a duty floor and wants ~16, the duty this tube lases at.)
|
||||
POWER_FLOOR_PCT = 10.0
|
||||
PWM_PERIOD = 127
|
||||
PWMSAR_FLOOR_MIN = 20
|
||||
PWMSAR_FLOOR_MIN = 12
|
||||
|
||||
ARM_CUE = ("LIVE FIRE. Eye protection on, exhaust running, fire watch and extinguisher in reach, "
|
||||
"scrap under the head with room to move (%s), lid closed. When the job starts the "
|
||||
@@ -228,11 +231,11 @@ def wait_disarm(ctx, timeout):
|
||||
@test("laser.power-floor", title="The shipped duty floor holds commanded power above the lasing threshold",
|
||||
subsystem="laser", kind="auto", est_min=1,
|
||||
covers=[("grblhal-glowforge", "src/**")],
|
||||
description="The tube lases only above ~16 % duty, so $35 must floor every nonzero S there: "
|
||||
"unfloored, M4's velocity-scaled power falls into the dead band at every corner "
|
||||
"and reversal and marks nothing. Reads $$ and checks the floor is the "
|
||||
description="$35 floors the bottom of the laser's output range, and unfloored the low end "
|
||||
"of S asks for pulses too far apart for the discharge to re-strike - a "
|
||||
"commanded 1 % would emit nothing. Reads $$ and checks the floor is the "
|
||||
"commissioned percent, that $31 is 0 (the floor, not $31, sets the bottom of "
|
||||
"the range), and that the floor lands at or above PWMSAR 20. Reports the "
|
||||
"the range), and that it lands at or above the measured minimum. Reports the "
|
||||
"stored setting, which is the one in force after any controller start.")
|
||||
def power_floor(ctx):
|
||||
ev = ctx.evidence
|
||||
@@ -263,7 +266,7 @@ def power_floor(ctx):
|
||||
"controller restart, since the spindle mapping is precomputed at start",
|
||||
floor_pct, POWER_FLOOR_PCT)
|
||||
ctx.check(counts >= PWMSAR_FLOOR_MIN,
|
||||
"the floor lands at PWMSAR %d, below the %d the tube needs to lase",
|
||||
"the floor lands at PWMSAR %d, below the %d the tube needs",
|
||||
counts, PWMSAR_FLOOR_MIN)
|
||||
ctx.check(rpm_min == 0, "$31 is %s, not 0: the bottom of the S range is no longer the floor",
|
||||
rpm_min)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# changes; keep only SRCREV and PV here - the image manifest leaves
|
||||
# *-pin.inc out of the layer content hash because the component entry
|
||||
# already identifies the pinned source (forgefirm-image-manifest.bbclass).
|
||||
SRCREV = "f7e8c17bb3065a6b27cef4bb1099c9564359980e"
|
||||
SRCREV = "03d70e01f6abc66faeeed6304ab2de02e4cadedc"
|
||||
PV = "0.1.0"
|
||||
|
||||
@@ -64,13 +64,15 @@ BIN = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else "build-native/grblHA
|
||||
PORT = 2399
|
||||
STEPS_PER_MM = 53.333
|
||||
|
||||
# The S -> duty mapping the board defaults produce: $30 = 1000, $31 = 0,
|
||||
# The S -> level mapping the board defaults produce: $30 = 1000, $31 = 0,
|
||||
# and a $35 floor (boards/glowforge.h DEFAULT_SPINDLE_PWM_MIN_VALUE)
|
||||
# against the hardware's 127-count period. Changing the board's floor
|
||||
# against the hardware's 127-count period. The shipped floor is the
|
||||
# density one; the analog sessions below select their model explicitly
|
||||
# rather than inheriting the default, so both paths stay covered. Changing the board's floor
|
||||
# changes every expectation below, which is why it is mirrored here
|
||||
# rather than inferred from the stream.
|
||||
PWM_PERIOD = 127
|
||||
PWM_MIN_PCT = 16.0
|
||||
PWM_MIN_PCT = 10.0
|
||||
PWM_MIN = int(PWM_PERIOD * PWM_MIN_PCT / 100.0)
|
||||
RPM_MAX = 1000.0
|
||||
|
||||
@@ -139,6 +141,7 @@ JOB_LADDER.append("M5")
|
||||
# the floor exists only to keep an analog duty out of the tube's dead
|
||||
# band - under density every pulse is full-power, and a floor would just
|
||||
# clamp the light end of the range.
|
||||
ANALOG_CONF = "laser_power_model = analog\n"
|
||||
DENSITY_PERIOD = 20
|
||||
DENSITY_MIN_TICKS = 3
|
||||
DENSITY_CONF = ("laser_power_model = density\n"
|
||||
@@ -550,7 +553,7 @@ def count_fire(data):
|
||||
|
||||
def main():
|
||||
# --- session A: M4 dynamic power, rules 1-6 + 7-8 -------------------
|
||||
data = run_session("m4", JOB_M4)
|
||||
data = run_session("m4", JOB_M4, conf=ANALOG_CONF)
|
||||
fire_ticks, powers, x_max, tail_steps = check_m4_job(data)
|
||||
check_termination("m4", data)
|
||||
gap_a = check_fire_gaps("m4", data)
|
||||
@@ -560,7 +563,7 @@ def main():
|
||||
powers, x_max, tail_steps, gap_a))
|
||||
|
||||
# --- session B: M3 constant power to stream end, rule 7 -------------
|
||||
data = run_session("m3-term", JOB_M3_TERM)
|
||||
data = run_session("m3-term", JOB_M3_TERM, conf=ANALOG_CONF)
|
||||
if not count_fire(data):
|
||||
fail("[m3-term] no FIRE bits in the stream")
|
||||
check_termination("m3-term", data)
|
||||
@@ -569,7 +572,7 @@ def main():
|
||||
% (len(data), count_fire(data), gap_b))
|
||||
|
||||
# --- session C: cycle churn, rules 8-9 ------------------------------
|
||||
data = run_session("churn", JOB_CHURN)
|
||||
data = run_session("churn", JOB_CHURN, conf=ANALOG_CONF)
|
||||
if not count_fire(data):
|
||||
fail("[churn] no FIRE bits in the stream")
|
||||
check_termination("churn", data)
|
||||
@@ -578,7 +581,7 @@ def main():
|
||||
% (len(data), count_fire(data), gap_c))
|
||||
|
||||
# --- session D: power ladder, rule 10 -------------------------------
|
||||
data = run_session("ladder", JOB_LADDER)
|
||||
data = run_session("ladder", JOB_LADDER, conf=ANALOG_CONF)
|
||||
counts = check_power_ladder("ladder", data, LADDER_DUTY)
|
||||
check_termination("ladder", data)
|
||||
gap_d = check_fire_gaps("ladder", data)
|
||||
@@ -632,7 +635,7 @@ def main():
|
||||
% (len(ch), count_fire(ch), gap_e))
|
||||
|
||||
# --- session H: a level change inside a run costs no byte -----------
|
||||
lv_a = run_session("levels-analog", JOB_LEVELS)
|
||||
lv_a = run_session("levels-analog", JOB_LEVELS, conf=ANALOG_CONF)
|
||||
lv_d = run_session("levels-density", JOB_LEVELS, conf=DENSITY_CONF)
|
||||
pa = [b & 0x7F for b in lv_a if b & 0x80]
|
||||
pd = [b & 0x7F for b in lv_d if b & 0x80]
|
||||
@@ -649,7 +652,7 @@ def main():
|
||||
% (len(pa), sorted(set(pa)), len(pd)))
|
||||
|
||||
# --- session I: a level set while idle still cuts (rule 14) ---------
|
||||
idle_s = run_session("idle-s", JOB_IDLE_S)
|
||||
idle_s = run_session("idle-s", JOB_IDLE_S, conf=ANALOG_CONF)
|
||||
fire_by_duty = {}
|
||||
cur = None
|
||||
for b in idle_s:
|
||||
|
||||
Reference in New Issue
Block a user