diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index f6a199d..a5547a6 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -737,6 +737,11 @@ not a release. `min_ticks` 3 at that density - and 6 is outside anything the factory does. Below ~5 % no pulse shape reaches the tube: the interval grows as 1/density, so 1 % implies an 11 ms gap, five times what already failed. + **The scale closes that gap instead:** `$35` = 10 maps S onto 9.4-100 % + density, putting a commanded 1 % at 10.2 %, and a ladder weighted to the + bottom (1, 2, 5, 10, 20, 40, 70, 100 % of S) then marked on **all eight + rungs**, with eight current segments and means rising 136 -> 968. So a + user's 1 % is a real, visible mark rather than silence. - **Cooling operating point**: 40 % heater duty, 50 s window, flow-rise threshold 14.4 °C, re-checks every 150 s. Below ~40 % duty the stagnant loop sheds the heater's output by convection well enough to mimic flow (at 30 %, @@ -1133,7 +1138,16 @@ 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 user-facing scale. The factory maps its whole 1-100 power + 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. + + For reference, the factory maps its whole 1-100 power scale onto density 18.9-79.5 % (fit from the three captures; Full Power is off that line at ~99.7 %), so its "1 %" is the bottom of the band that does useful work rather than 1 % of the physical range. Under the diff --git a/docs/CAMPAIGN-LOG.md b/docs/CAMPAIGN-LOG.md index c58f2ab..7060a86 100644 --- a/docs/CAMPAIGN-LOG.md +++ b/docs/CAMPAIGN-LOG.md @@ -3009,11 +3009,45 @@ in the direction that fails. The bench is back at 3. **Measured band for this tube: strikes from ~5 %, marks from ~10 % at F300.** Which closes the pulse-structure route to a usable 1 %. The interval grows as -1/density, so 1 % implies an 11 ms gap, five times what already failed — -no pulse shape reaches down there. The low end is a scaling problem: map the -user's 1–100 onto the band that works, via `$35` as the density floor, which -is what the factory does and why its cut scale starts at 18.9 % while its -engraves reach 6.5 %. Both sit inside the band measured here independently. +1/density, so 1 % implies an 11 ms gap, five times what already failed — no +pulse shape reaches down there. The low end is a scaling problem. + +### The seventh ladder: the scale, and the goal met + +`$35 = 10` — a density floor under this model, not a duty floor — with the +ladder reweighted to the bottom of the user scale (1, 2, 5, 10, 20, 40, 70, +100 % of S), since with a floor in place what matters is whether the lowest +levels a user can dial in still mark. + +The mapping puts S onto 9.4–100 % density, so a commanded 1 % lands at 10.2 %, +just above the ~10 % marking floor the earlier ladders measured. **All eight +rungs marked.** The trace carries eight current segments — boundaries at 14.3, +19.5, ~24.85, 30.2, 35.4, 40.9 and 46.1 s, fire spanning 9.1 → 51.1 s = 42.0 s +against exactly 8 × 5.25 — with means climbing monotonically: + +| rung | commanded | density | mean current | +|---|---|---|---| +| 1 | **1 %** | 10.2 % | 136 | +| 2 | 2 % | 11.0 % | 190 | +| 3 | 5 % | 13.4 % | 214 | +| 4 | 10 % | 18.1 % | 262 | +| 5 | 20 % | 27.6 % | 331 | +| 6 | 40 % | 45.7 % | 340 | +| 7 | 70 % | 72.4 % | 444 | +| 8 | 100 % | 100 % | 968 flat | + +So the original goal is met: a user's 1 % is a real, visible mark rather than +silence, and 100 % is full power. It took the density model to make every +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. ## Superseded status notes diff --git a/scripts/bench/live_fire_drills.py b/scripts/bench/live_fire_drills.py index 0bb58a9..f0877ca 100644 --- a/scripts/bench/live_fire_drills.py +++ b/scripts/bench/live_fire_drills.py @@ -59,8 +59,10 @@ Drills (pass a name): ~1.43 kHz, and 40 and 10 bracket it. Two questions the material answers: does mark depth track density linearly, and how short a burst still marks. Requires - laser_power_model = density and $35 = 0 (a floor lifts every - rung); sets laser_pulse_ticks itself when run on the board. + laser_power_model = density; reads $30/$31/$35/$36 and + reports the mapping, so a run with a density floor set shows + what a shipped machine would actually emit. Sets + laser_pulse_ticks itself when run on the board. dladder [period] [F] e.g. dladder 20 300 expstop Armed kill on the EXPECTED-stop path: start a mark job, then mid-burn POST /controller/stop (the supervisor stops @@ -513,9 +515,10 @@ def drill_pthresh(g): # --- density ladder ------------------------------------------------------- -# Dose levels in percent of full. Even spacing, because the question is -# whether mark depth tracks density linearly rather than where it stops. -DLADDER_PCT = (5, 10, 20, 30, 40, 60, 80, 100) +# Dose levels in percent of full, weighted to the bottom: with a density +# floor set, what matters is whether the lowest levels a user can dial in +# still mark, not how the top half behaves. +DLADDER_PCT = (1, 2, 5, 10, 20, 40, 70, 100) DLADDER_LEN = 25.0 # mm of burn per rung DLADDER_PITCH = 3.0 # mm between rungs STREAM_RATE_HZ = 28160 # machine tick (GFSINK_RATE default) @@ -603,13 +606,27 @@ def drill_dladder(g): print('Set it in %s and re-run. The model is read at each arm, so' % CONF) print('this key needs no controller restart.') return 2 + # The core maps S onto the level this model renders as density, and + # $35/$36 are its floor and ceiling. Read them rather than assuming: + # with a floor set, the ladder is testing the shipping mapping, and + # every rung sits higher than its commanded percent. floor = grbl_setting(g, '$35') - if floor is None or floor > 0.0: - print('PRECONDITION FAILED: $35 is %s, need 0.' % floor) - print('Send $35=0 and restart the controller - the S -> duty mapping') - print('is precomputed once, when the spindle is enabled, so a runtime') - print('write does not reach it.') + ceil = grbl_setting(g, '$36') + rpm_max = grbl_setting(g, '$30') + rpm_min = grbl_setting(g, '$31') + if None in (floor, ceil, rpm_max, rpm_min) or rpm_max <= rpm_min: + print('PRECONDITION FAILED: cannot read $30/$31/$35/$36 (%s/%s/%s/%s)' + % (rpm_max, rpm_min, floor, ceil)) return 2 + min_value = int(PWM_PERIOD * floor / 100.0) + max_value = int(PWM_PERIOD * ceil / 100.0) + gradient = (max_value - min_value) / (rpm_max - rpm_min) + print('mapping: $30=%g $31=%g $35=%g $36=%g -> density %.1f%%..%.1f%%' + % (rpm_max, rpm_min, floor, ceil, + 100.0 * min_value / PWM_PERIOD, 100.0 * max_value / PWM_PERIOD)) + if floor > 0.0: + print('a floor is set, so the rungs below it all land on it - that is') + print('the shipping mapping, not the raw range.') if conf_set('laser_pulse_ticks', str(period)): print('laser_pulse_ticks = %d (written to %s)' % (period, CONF)) else: @@ -636,8 +653,9 @@ def drill_dladder(g): print('rungs (drawn in order, alternating direction, +Y between):') levels = [] for pct in DLADDER_PCT: - sval = int(round(1000 * pct / 100.0)) - level = int(sval * PWM_PERIOD / 1000) # the core's mapping at $35 = 0 + sval = int(round(rpm_max * pct / 100.0)) + level = int((sval - rpm_min) * gradient) + min_value + level = min(level, max_value) dens = level / float(PWM_PERIOD) on = dens * period levels.append((pct, sval, dens)) @@ -710,9 +728,10 @@ def drill_dladder(g): print(' 2. THE SHORT END - the lowest rung that still marks cleanly. Its') print(' burst length in us is the number to keep; a rung that stops') print(' marking sets the floor this base period can reach.') - print('Then run the same ladder at 40 and at 10 on the same material and') - print('compare the low rungs across the three: that is what picks the') - print('base period. Nothing else in the stack can answer it.') + print('The base period does not decide the low end: below the minimum') + print('the pulse interval is min_ticks x tick / density, which the') + print('period cancels out of. What sets the bottom is the density') + print('floor ($35), so a rung that fails is telling you to raise it.') return samples