Track the x32 xy_microsteps default in forgetest and the bench tools

Acceptance baseline: XY_MODE_DEFAULT 8 -> 32, split out XY_MODE_BASE (8) for
the tick/ramp scaling, matching the driver. The mode-aware comparisons
(fixed_sysfs of the resolved mode) judge an unset machine at x32.

Host CI: xy_mode_test.py default and invalid cases expect x32/213.333/112640.

Bench tools: raster_dry.py and xy_pattern_accel.py set_mode() cleared the key
for mode 8, which relied on clear == the x8 default; clearing now yields x32,
so they set the mode explicitly and restore to x32. live_fire_drills
STREAM_RATE_HZ is the 28160 Hz laser reference tick, the same at every mode.
This commit is contained in:
ScottW514
2026-09-16 15:39:20 -04:00
parent 147393322f
commit 9a288b175b
6 changed files with 24 additions and 26 deletions
+5 -4
View File
@@ -48,7 +48,7 @@ from .log import now_ts
# in the pulse path (a job's Z moves the lens; the driver's Z soft limit
# guards it), step_freq its machine tick and ramp_rate the kernel stop
# ramp, both derived from the XY microstep mode (the values here are the
# x8 default's; fixed_sysfs() gives the mode's); streaming is only ever 1
# x8 base; fixed_sysfs() gives the mode's); streaming is only ever 1
# inside a live job; the head white LED is a camera lamp, off at idle; the
# loop heater and TEC are the diagnostics' tools, off at idle.
FIXED_SYSFS = [
@@ -147,11 +147,12 @@ RETURN_MAX_MM = 100.0 # a displaced head is jogged back at most th
POSITION_DEADBAND_MM = 0.1
# The XY microstep mode (the xy_microsteps setting: 8, 16 or 32; unset =
# 8). The GRBL controller reads it at its start and derives its scale,
# 32). The GRBL controller reads it at its start and derives its scale,
# its machine tick and the kernel stop ramp from it (boards/glowforge.h):
# the fixed values of x/y_mode, step_freq and ramp_rate are the mode's.
XY_MODES = (8, 16, 32)
XY_MODE_DEFAULT = 8
XY_MODE_BASE = 8 # the factory x8 reference the tick and ramp scale from
XY_MODE_DEFAULT = 32 # the mode an unset or invalid setting reads as
XY_STEPS_PER_MM_OF = {8: 53.333, 16: 106.667, 32: 213.333} # $100/$101 per mode
XY_TICK_X8_HZ = 28160 # the x8 machine tick
XY_RAMP_X8_HZ_PER_S = 125000 # the kernel stop ramp at it
@@ -175,7 +176,7 @@ def fixed_sysfs(mode=XY_MODE_DEFAULT):
"""FIXED_SYSFS with the mode's own x/y_mode, step_freq and ramp_rate:
the tick and the ramp scale with the mode (x16 doubles both, x32
quadruples them)."""
k = mode // XY_MODE_DEFAULT
k = mode // XY_MODE_BASE
own = {"cnc/x_mode": str(mode), "cnc/y_mode": str(mode),
"cnc/step_freq": str(XY_TICK_X8_HZ * k),
"cnc/ramp_rate": str(XY_RAMP_X8_HZ_PER_S * k)}
+3 -3
View File
@@ -36,9 +36,9 @@ page's takeover does that; from a host, stop them first.
| `live_fire_drills.py` | **LIVE LASER** drills, on the board (the bench page) or from a LAN host (`GF_HOST`): `live_fire_drills.py <drill> [S] [F]` - `witness` (emission witness, lid-IR peaks vs the ambient baseline, HV current, job-based disarm on M2), `hold` (disarm grace in Hold), `faultpos` (armed job refuses a stale origin after an underrun), `ircut` (lid-IR characterization cut at S/F), `pthresh` (laser power-threshold ladder: 13 constant-power rungs from 2 % to 30 % of full on scrap; the lowest rung that marks is the tube's striking threshold and reads directly as the `$35` value - requires `$35` = 0 for the run), `dladder` (density ladder at a chosen base period), `pcurve` (laser performance-curve ladder: one 100 mm line per level at 10 mm/s under M3, the laser off between rungs and a mid-ladder rung repeated at the end; reads `pic/hv_current` and the head thermopile `head/beam_detect_analog` (a scatter detector in the beam path upstream of the final mirror, so it sees the beam, not the material) from sysfs at ~25 Hz on the board, brackets each rung on the controller's Run/Idle states, and reports per rung the current with a clipped-at-1023 flag, the thermopile delta over its laser-off baseline and in-line drift, then the normalized curve, monotonicity, a line fit with its threshold intercept and the repeat-rung drift; JSON record with the raw trace in the bench data directory; rungs follow `laser_power_model`, a comma list overrides; a curve measurement wants `$35` = 0), `dpatch [F] [pitch] [length]` (depth witness for the density dose curve: two rows of small serpentine-filled patches, row A CW at feeds giving relative doses 1.0 to 0.25 of the reference feed, row B at the reference feed at 100/80/60/45/30 % density; the operator matches each row-B patch to the row-A patch of equal depth, which reads the density's light fraction off the material beside the thermopile's prediction; JSON record), `m4feeds [S] [F1] [F2]` (the density time base across feeds: one out-and-back line pair per feed at the same S under M4 density, one armed run; the operator reads within-line evenness and reversal darkness at both feeds - M4's velocity scaling is what holds dose per mm through the accel), `m4corner [S] [F]` (M4 velocity-scaled power into corners: a corner-heavy vector pattern at 30 % under M4 density, one armed run; the operator confirms every commanded segment marks - the floor makes a dropout unreachable - and the drill asserts the arm report, one discharge window and dark after), `m5dark` (the rapids after an M5 ship dark: one 20 mm line at M3 S400, M5, dwell, rapid back, dwell, rapid forward; PASS when the 25 Hz current trace shows one discharge segment and reads dark after the M5 and `laser_on_sampled` never re-lights; the catalog's `laser.m5-rapid-dark` is its port), `flowload` (cooling under laser load, one armed run per invocation, the conf keys it writes put back at the end, the pump never commanded off: `t1` reproduces the flow-check trip with the check on at its defaults and two 30 x 4 mm CW fills at F1500 starting on the press with no dark dwell, and reports the engine's rise/dT verdict beside the 25 Hz trace of both coolant sensors, the current, the digital witness and the heater output in 5 s bins across the window, with the shape at fire start; `t2 <secs> [pct]` runs with the check off and one fill of about `secs` lit seconds at CW or at `pct` density, and reports the lag to each sensor, the rise per raw-second of `hv_current` and what a full 50 s window would add against the 1.6 C margin; `fit` fits rise against dose over every t2 record; JSON records), `expstop` (armed kill on the expected-stop path; needs the panel token - `GF_TOKEN`, or the board's token file) and `ctrlstart` (the separate controller restart after it). Every drill waits for the operator's physical arm press; eye protection, fire watch, extinguisher, and exhaust are mandatory. |
| `pacing_test.py` | Protocol-loop pacing check (runs on the board, dry motion): idle and parked-in-Hold states are coarse-paced, active motion is tight-paced, and a feed-hold/resume mid-move preserves position with no feeder starve. |
| `planner_blocks_test.py` | Host-side planner buffer depth harness (null-sink controller): `$398` written to 400 and to 1000, the controller restarted on the same settings store each time, and it must answer on the port, report the depth in its status report and run a move. A byte-wide ring index once made 255 blocks or more spin the start. Runs in the grblHAL repo's CI. |
| `xy_mode_test.py` | Host-side XY microstep mode harness (null-sink controller): `xy_microsteps` in the shared config sets `$100`/`$101` and the machine tick, a typed `$100` is overwritten on the spot, a value that is not a mode falls back to x8 with a warning, and `$110`/`$111` are held under a tick lowered with `GFSINK_RATE`. Runs in the grblHAL repo's CI. |
| `raster_dry.py` | A dry raster at top speed at each XY microstep mode (runs on the board; the controller in GRBL mode, idle, no other Grbl client): per mode given (default 8, 16, 32) it stores `xy_microsteps` through forgectrl, waits for the restarted controller, streams 60 passes of 150 mm at F12000 with the laser off, and reports the peak feed, the controller CPU, the kernel counters against the start, `cnc/underruns` and any clamped-event line. Needs 150 mm of free +X and 12 mm of free +Y travel. Ends at x8. |
| `xy_pattern_accel.py` | The XY microstep modes by the head accelerometer with the machine silent (runs on the board; the machine homed and at home, the lid closed, no other Grbl client): per mode it takes the cooling engine's quiet hold with the pump (`POST /cool/quiet?on=1&pump=1`: every fan, the pump and the TEC off), waits the fixed 10 s, samples the LIS2HH12 over `/dev/i2c-3` in one SMBus block transaction per reading at about 600 Hz, and runs the pattern from home at F12000: to (18, 9) in, to (9, 9) in, a 9 in circle from its mid-bottom back to (9, 9), to (9, 0), home. Reports the cruise-window RMS and peak-to-peak per leg and overall, the leg times, the kernel counters against home and `cnc/underruns`; JSON with the trace in the bench data directory. Ends at x8. |
| `xy_mode_test.py` | Host-side XY microstep mode harness (null-sink controller): `xy_microsteps` in the shared config sets `$100`/`$101` and the machine tick, a typed `$100` is overwritten on the spot, a value that is not a mode falls back to x32 with a warning, and `$110`/`$111` are held under a tick lowered with `GFSINK_RATE`. Runs in the grblHAL repo's CI. |
| `raster_dry.py` | A dry raster at top speed at each XY microstep mode (runs on the board; the controller in GRBL mode, idle, no other Grbl client): per mode given (default 8, 16, 32) it stores `xy_microsteps` through forgectrl, waits for the restarted controller, streams 60 passes of 150 mm at F12000 with the laser off, and reports the peak feed, the controller CPU, the kernel counters against the start, `cnc/underruns` and any clamped-event line. Needs 150 mm of free +X and 12 mm of free +Y travel. Ends at x32. |
| `xy_pattern_accel.py` | The XY microstep modes by the head accelerometer with the machine silent (runs on the board; the machine homed and at home, the lid closed, no other Grbl client): per mode it takes the cooling engine's quiet hold with the pump (`POST /cool/quiet?on=1&pump=1`: every fan, the pump and the TEC off), waits the fixed 10 s, samples the LIS2HH12 over `/dev/i2c-3` in one SMBus block transaction per reading at about 600 Hz, and runs the pattern from home at F12000: to (18, 9) in, to (9, 9) in, a 9 in circle from its mid-bottom back to (9, 9), to (9, 0), home. Reports the cruise-window RMS and peak-to-peak per leg and overall, the leg times, the kernel counters against home and `cnc/underruns`; JSON with the trace in the bench data directory. Ends at x32. |
| `arc_tolerance_sweep.py` | How fine an arc the controller can plan (runs on the board; the machine homed and at home, the lid closed, no other Grbl client, the machine silent through the quiet hold): a `$12` ladder on the 9 in circle at F12000 at the mode given (`--mode`, default 16). Per rung: the chords and the chord boundaries a second, the circle time against the ideal, the lowest feed mid-circle and the fewest free planner blocks, the controller CPU, clamped events, underruns and the accelerometer's cruise RMS. `$12` goes back to what it was on every exit path; the head returns home. |
| `gfbench.py` | Not a tool: the helper the board/host tools share - `HOST`/`LOCAL` from `GF_HOST`, `board(cmd)` (local `sh -c` or ssh), the factory coolant conversion `degc()`, `data_path()` (`FORGETEST_BENCH_DATA` or next to the tool), forgectrl's HTTP API with the panel token, `setting(key)` (from forgectrl, or from `/data/forgefirm/forgefirm.conf` on the board while forgectrl is stopped). |
| `lens_travel.py` | Lens travel drill (runs on the board; stops the controller through forgectrl and restarts it; lens motion only): steps the lens over sysfs the way the focus card's lens home does and counts, per condition and round, the hall's hysteresis band (down off the rising edge until the hall leaves home, back up until it reads home), the rising edge's height above the bottom stop after a drive onto it, and the top stop's height above the edge (a drive up, down until the hall leaves home, back up; the difference). Conditions: the card's half-step drive onto the stop, a short drive, a long one, the factory's full-step home mode; `--ladder` drives a list of half-step descents below the leave-home point and counts each back (exact until the stop, short by an even number after a stall), `--current` picks the run current, the hold current, or hold down / run up, `--settle` and `--cadence` vary the timing; `--park bottom|top|edge-N|edge+N` holds the lens there for a depth-gauge reading with the controller in standby, `--park edge` returns it to the rising edge and restarts the controller. Stall drills on the bench reference machine only. The posture found is put back. |
+1 -1
View File
@@ -655,7 +655,7 @@ def drill_pthresh(g):
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)
STREAM_RATE_HZ = 28160 # the 28160 Hz laser reference tick (the same at every microstep mode)
PWM_PERIOD = 127 # 7-bit power byte against PWMSAR
CONF = os.environ.get('GFHOME_CONF') or '/data/forgefirm/forgefirm.conf'
PULSE_MIN_KEY = 'laser_pulse_min_ticks'
+4 -4
View File
@@ -68,7 +68,7 @@ def cpu_ticks(pid):
def set_mode(mode):
r = fc("/settings?xy_microsteps=", {}) if mode == 8 else fc("/settings", {"xy_microsteps": str(mode)})
r = fc("/settings", {"xy_microsteps": str(mode)})
if "http" in r:
raise SystemExit("settings write refused: %s" % r)
time.sleep(3)
@@ -214,9 +214,9 @@ def main():
print("=== x%d ===" % mode)
set_mode(mode)
verdicts[mode] = raster(mode)
if MODES[-1] != 8:
print("=== restore x8 ===")
set_mode(8)
if MODES[-1] != 32:
print("=== restore x32 ===")
set_mode(32)
print("settings xy_microsteps:", repr(fc("/settings").get("xy_microsteps")))
print("RESULT:", " ".join("x%d=%s" % (m, "PASS" if v else "FAIL") for m, v in verdicts.items()))
return 0 if all(verdicts.values()) else 1
+6 -6
View File
@@ -145,13 +145,13 @@ def expect_log(log, needle, what):
def case_default():
s = Session()
try:
expect_scale(s, 8, 53.333)
expect_scale(s, 32, 213.333)
rate = s.setting("$110")
check(near(rate, 12000.0), "$110=%.0f stands at x8" % rate,
"$110 is %.3f at x8, want 12000" % rate)
check(near(rate, 12000.0), "$110=%.0f stands at x32" % rate,
"$110 is %.3f at x32, want 12000" % rate)
finally:
log = s.close()
expect_log(log, "x8 microsteps, 28160 Hz machine tick", "no key: x8, the 28160 Hz tick")
expect_log(log, "x32 microsteps, 112640 Hz machine tick", "no key: x32, the 112640 Hz tick")
def case_x16():
@@ -184,11 +184,11 @@ def case_x32():
def case_invalid():
s = Session("xy_microsteps = 24\n")
try:
expect_scale(s, 8, 53.333)
expect_scale(s, 32, 213.333)
finally:
log = s.close()
expect_log(log, "xy_microsteps '24' is not 8, 16 or 32", "a value that is not a mode is refused with a warning")
expect_log(log, "x8 microsteps, 28160 Hz machine tick", "and the machine runs at x8")
expect_log(log, "x32 microsteps, 112640 Hz machine tick", "and the machine runs at x32")
def case_low_tick():
+5 -8
View File
@@ -23,7 +23,7 @@ through the pattern. Reports, per leg and overall, the cruise-window
RMS and peak-to-peak of X and Y with the mean removed, the leg times,
the kernel counters against home, and cnc/underruns; JSON with the raw
trace to the bench data directory. Releases the hold and puts the
accelerometer back on every exit path. Ends at x8.
accelerometer back on every exit path. Ends at x32.
Needs the controller in GRBL mode, homed and standing at home, the lid
closed, no other Grbl client, and the bed clear across the pattern.
@@ -156,10 +156,7 @@ def fail(msg):
def set_mode(mode):
if mode == 8:
st, body = forgectrl_post("/settings", params={"xy_microsteps": ""})
else:
st, body = forgectrl_post("/settings", data={"xy_microsteps": str(mode)})
st, body = forgectrl_post("/settings", data={"xy_microsteps": str(mode)})
if st != 200:
fail("settings write refused: %s %s" % (st, body))
time.sleep(3)
@@ -397,9 +394,9 @@ def main():
json.dump(rec, f)
print(" record: %s" % path)
results.append(rec)
if modes[-1] != 8:
print("=== restore x8 ===")
set_mode(8)
if modes[-1] != 32:
print("=== restore x32 ===")
set_mode(32)
print("\n=== summary (cruise RMS with the mean removed, raw counts; CTRL4 0x%02x) ==="
% results[0].get("ctrl4", 0))
print("%-6s %10s %10s %10s %10s %8s" % ("mode", "x rms", "x p2p", "y rms", "y p2p", "result"))