XY microstep modes: the baseline, the catalog test and the bench tools

The baseline derives x/y_mode, step_freq, ramp_rate and the configured
markers from the xy_microsteps setting; ramp_rate joins the GRBL
controller's set (the driver writes it; the cloud client runs at the
module's). motion.microstep-modes cycles 8, 16 and 32: the save restarts
the idle controller, the kernel reads the mode with its tick and ramp,
$100/$101 are the mode's and a typed $100 is overwritten, a 40 mm jog at
top speed returns to Idle with the kernel counters over the mode agreeing
with the commanded travel and the accelerometer seeing the head move; the
setting is put back as found.

Bench tools: xy_mode_test.py (the null-sink harness the grblHAL CI runs),
raster_dry.py (a top-speed raster per mode), xy_pattern_accel.py (the
operator's pattern from home with the machine silent and the head
accelerometer listening), arc_tolerance_sweep.py (a $12 ladder on the 9
in circle: the chord rate the protocol loop feeds, about 300 a second, is
the ceiling, not the core), and the xymode and xycircle live drills.
BRINGUP carries the present state and the facts; CAMPAIGN-LOG the dated
record, including the planner-blocks spin (a $398 of 255 or more loops
forever at start, a core bug) and its recovery.
This commit is contained in:
ScottW514
2026-09-07 18:05:28 -04:00
parent bfb5cb27d8
commit 8fc5250d6d
13 changed files with 1974 additions and 26 deletions
+82 -21
View File
@@ -46,10 +46,11 @@ from .log import now_ts
# motor_lock/x_mode/y_mode/x_decay/y_decay and the hold currents are the
# GRBL controller's init writes (glowforge_io.c): motor_lock 0, every axis
# in the pulse path (a job's Z moves the lens; the driver's Z soft limit
# guards it), step_freq its default machine tick, ramp_rate the module
# default; 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.
# 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
# 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 = [
("cnc/motor_lock", "0"),
("cnc/x_mode", "8"),
@@ -68,11 +69,12 @@ FIXED_SYSFS = [
# The subset the GRBL controller writes at its start: checked and restored
# in GRBL mode only. The cloud client sets its own values for these from
# every pulse header (step_freq 10 kHz, the run currents) and hands the
# hold currents back at idle; forcing the GRBL values under it would be
# the baseline configuring another controller's machine.
# every pulse header (step_freq 10 kHz, the run currents), runs at the
# service's own x8 with the module's ramp whatever xy_microsteps says, and
# hands the hold currents back at idle; forcing the GRBL values under it
# would be the baseline configuring another controller's machine.
GRBL_CONTROLLER_SYSFS = ("cnc/motor_lock", "cnc/x_mode", "cnc/y_mode", "cnc/x_decay", "cnc/y_decay",
"cnc/step_freq", "pic/x_step_current", "pic/y_step_current")
"cnc/step_freq", "cnc/ramp_rate", "pic/x_step_current", "pic/y_step_current")
# Settings the baseline never hands back as bare settings: controller_mode
# is the persisted mirror of the live mode (the mode item restores it
@@ -117,6 +119,41 @@ GRBL_PORT_S = 30 # the Grbl port after the supervisor reports
XY_STEPS_PER_MM = 53.333 # boards/glowforge.h (x8 microstepping)
RETURN_MAX_MM = 100.0 # a displaced head is jogged back at most this far
# 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,
# 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_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
def xy_mode_of(settings):
"""The mode a /settings body names (unset, unreadable, or not a mode
reads as the default)."""
try:
mode = int((settings or {}).get("xy_microsteps") or XY_MODE_DEFAULT)
except (TypeError, ValueError, AttributeError):
return XY_MODE_DEFAULT
return mode if mode in XY_MODES else XY_MODE_DEFAULT
def xy_steps_per_mm(mode):
return XY_STEPS_PER_MM_OF[mode]
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
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)}
return [(attr, own.get(attr, val)) for attr, val in FIXED_SYSFS]
def leds_root():
r = os.environ.get("GF_LEDS_ROOT") or "/sys/class/leds/"
@@ -228,6 +265,12 @@ class Baseline:
except hw.HwError as e:
return None, str(e)
def xy_mode(self):
"""The XY microstep mode the settings name now (the default when
forgectrl does not answer)."""
st, body = self.fc_get("/settings")
return xy_mode_of(body if st == 200 and isinstance(body, dict) else None)
def wait_settled(self, timeout=SETTLE_S, unreachable_s=10):
"""Block until forgectrl reports a settled supervisor: motion
verified (the probe passed), motion-fault (the ladder exhausted),
@@ -510,7 +553,7 @@ class Baseline:
if residue:
left.append(Leftover("pulse ring", "%d unplayed bytes" % residue, "0 (nothing queued)",
"unrestorable: the next run would replay them first"))
for attr, want in FIXED_SYSFS:
for attr, want in fixed_sysfs(self.xy_mode()):
if self.cloud_mode() and attr in GRBL_CONTROLLER_SYSFS:
continue
got = hw.sysfs_read(attr)
@@ -703,7 +746,7 @@ def check_fixed_against(ref, log):
machine worth a look, not a leftover."""
diffs = []
sysfs = ref.get("sysfs") or {}
for attr, want in FIXED_SYSFS + IDLE_READBACKS:
for attr, want in fixed_sysfs(ref_xy_mode(ref)) + IDLE_READBACKS:
got = sysfs.get(attr)
if got is not None and got != want:
diffs.append("%s: boot=%s constant=%s" % (attr, got, want))
@@ -719,34 +762,52 @@ def check_fixed_against(ref, log):
# the state /mode already calls "running", because "running" is the spawn,
# not the config. motor_lock is no marker: the probe and the controller
# both leave it 0.
CONFIGURED_MARKERS = [(a, dict(FIXED_SYSFS)[a]) for a in ("cnc/step_freq", "cnc/y_mode")]
CONFIGURED_MARKER_ATTRS = ("cnc/step_freq", "cnc/y_mode")
CONFIGURED_MARKERS = [(a, dict(FIXED_SYSFS)[a]) for a in CONFIGURED_MARKER_ATTRS]
CONFIGURED_TIMEOUT_S = 20
CONFIGURED_SETTLE_S = 1.0
def configured_markers(mode=XY_MODE_DEFAULT):
"""The markers with the mode's own values (the tick and the mode both
follow the setting)."""
return [(a, dict(fixed_sysfs(mode))[a]) for a in CONFIGURED_MARKER_ATTRS]
def ref_xy_mode(ref):
"""The XY microstep mode a saved reference was taken under: its own
/settings dump, else the default."""
fc = (ref or {}).get("forgectrl") or {}
return xy_mode_of(fc.get("/settings") if isinstance(fc, dict) else None)
def reference_preconfig(ref):
"""True when a saved reference shows the pre-controller state: every
marker present differs from its fixed value (the probe's step_freq and
the module's y_mode together), i.e. it was dumped before the
controller's init writes landed."""
sysfs = (ref or {}).get("sysfs") or {}
seen = [(sysfs.get(a), want) for a, want in CONFIGURED_MARKERS if sysfs.get(a) is not None]
markers = configured_markers(ref_xy_mode(ref))
seen = [(sysfs.get(a), want) for a, want in markers if sysfs.get(a) is not None]
return bool(seen) and all(got != want for got, want in seen)
def wait_controller_configured(log, mode_body, timeout=CONFIGURED_TIMEOUT_S, sleep=time.sleep):
def wait_controller_configured(log, mode_body, timeout=CONFIGURED_TIMEOUT_S, sleep=time.sleep,
xy_mode=XY_MODE_DEFAULT):
"""After the supervisor reports the controller running: block until the
GRBL controller's init writes have landed (the CONFIGURED_MARKERS read
their fixed values), then a short settle. Only the GRBL controller
writes those; in any other mode return at once. Bounded: on timeout
the caller proceeds and the reference will say so."""
GRBL controller's init writes have landed (the configured markers read
the fixed values of the XY microstep mode in force), then a short
settle. Only the GRBL controller writes those; in any other mode
return at once. Bounded: on timeout the caller proceeds and the
reference will say so."""
if not (isinstance(mode_body, dict) and mode_body.get("controller") == "running"
and (mode_body.get("mode") or "grbl") == "grbl"):
return True
markers = configured_markers(xy_mode)
t0 = time.time()
while time.time() - t0 < timeout:
got = [(a, hw.sysfs_read(a)) for a, _ in CONFIGURED_MARKERS]
if all(g == want for (a, g), (_, want) in zip(got, CONFIGURED_MARKERS)):
got = [(a, hw.sysfs_read(a)) for a, _ in markers]
if all(g == want for (a, g), (_, want) in zip(got, markers)):
sleep(CONFIGURED_SETTLE_S)
log("baseline: controller configured %.1f s after running" % (time.time() - t0))
return True
@@ -755,7 +816,7 @@ def wait_controller_configured(log, mode_body, timeout=CONFIGURED_TIMEOUT_S, sle
sleep(0.25)
log("baseline: WARNING - controller did not apply its config within %d s (%s); "
"the reference may show the supervisor's probe values"
% (timeout, ", ".join("%s=%s" % (a, hw.sysfs_read(a)) for a, _ in CONFIGURED_MARKERS)))
% (timeout, ", ".join("%s=%s" % (a, hw.sysfs_read(a)) for a, _ in markers)))
return False
@@ -794,7 +855,7 @@ def boot_reference(log, data_dir):
return None
bl = Baseline(log)
mode = bl.wait_settled()
wait_controller_configured(log, mode)
wait_controller_configured(log, mode, xy_mode=bl.xy_mode())
ref = dump_all(bl)
ref.update({"ts": now_ts(), "boot_id": bid, "uptime_s": uptime_s()})
try:
+32
View File
@@ -249,6 +249,38 @@ TOOLS = [
"desc": "The Z soft limit belongs to the driver, not to $20: an unreferenced Z is collapsed to where the "
"lens stands, and neither a $20 nor a $132 write frees it. A CI harness (the grblHAL repo): needs "
"the host-built null-sink controller, not the machine, so it is not a bench-page tool."},
{"id": "raster-dry", "title": "Dry top-speed raster per XY microstep mode", "script": "raster_dry.py",
"safety": "dry", "where": "board", "ported": True,
"args": [_arg("modes", "choice", "8 16 32", "the modes to run, in order", ["8", "16", "32", "8 16 32"])],
"desc": "For each mode given: stores xy_microsteps, waits for the restarted controller, streams a 60-pass "
"raster of 150 mm at F12000 with the laser off, and reports the peak feed, the controller CPU, the "
"kernel counters against the start, underruns and clamped events. Needs the head with 150 mm of "
"free +X and 12 mm of free +Y travel, no other Grbl client. Ends at x8."},
{"id": "xy-pattern-accel", "title": "XY microstep modes by the head accelerometer, the machine silent",
"script": "xy_pattern_accel.py", "safety": "dry", "where": "board", "ported": True,
"args": [_arg("modes", "choice", "8 16 32", "the modes to run, in order", ["8", "16", "32", "8 16 32"])],
"desc": "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. Per mode: every fan, the coolant pump and the TEC commanded off through "
"the engine's quiet hold (the machine silent, the modes audible), the fixed 10 s wait, the head "
"accelerometer sampled over the bus at 800 Hz through the pattern; "
"cruise-window RMS and peak-to-peak per leg and overall, the kernel counters against home, "
"underruns; JSON with the trace to the bench data directory. Needs the machine homed and at home, "
"the lid closed, no other Grbl client, the bed clear. Ends at x8."},
{"id": "arc-tolerance-sweep", "title": "How fine an arc the controller can plan ($12 ladder)",
"script": "arc_tolerance_sweep.py", "safety": "dry", "where": "board", "ported": True,
"args": [_arg("mode", "choice", "16", "the XY microstep mode", ["8", "16", "32"], flag="--mode"),
_arg("ladder", "str", "0.002 0.001 0.0005 0.00025 0.0001", "the $12 values to try, in order")],
"desc": "From home, the machine silent: to (9, 9) in, then the 9 in circle at F12000 once per $12 in the "
"ladder. Per rung: the chords and boundaries a second, the circle time against the ideal, the lowest "
"feed mid-circle, the fewest free planner blocks, the controller CPU, clamped events, underruns, the "
"accelerometer's cruise RMS. $12 goes back to what it was; the head returns home. Needs the machine "
"homed and at home, the lid closed, no other Grbl client."},
{"id": "xy-mode-test", "title": "XY microstep mode harness", "script": "xy_mode_test.py",
"safety": "dry", "where": "host", "ported": False, "args": [],
"desc": "The XY scale is the microstep mode's, never typed: xy_microsteps sets $100/$101 and the "
"machine tick, a typed $100 is overwritten, and $110/$111 are held under a lowered tick. A CI "
"harness (the grblHAL repo): needs the host-built null-sink controller, not the machine, so it "
"is not a bench-page tool."},
{"id": "puls-profile", "title": "Factory .puls profile decoder", "script": "puls_profile.py",
"safety": "dry", "where": "host", "ported": False, "args": [],
"desc": "Decodes factory pulse streams into velocity/accel profiles. Runs anywhere; needs a .puls file "
+1 -1
View File
@@ -23,7 +23,7 @@ job's length for its progress report.
import gzip
import struct
STEPS_PER_MM = 53.333 # boards/glowforge.h, x8 microstepping
STEPS_PER_MM = 53.333 # x8 microstepping: the service's scale, which cloud mode runs at
# The header of a factory print for this machine type, as captured.
PRINT_HEADER = {
+140
View File
@@ -9,6 +9,7 @@ import os
import time
from ..catalog import test
from .. import baseline as _baseline
from .. import hw
from ..runner import Failed
@@ -289,6 +290,145 @@ def jog_roundtrip(ctx):
"motion on %d of %d legs", len(moves), maxrate, drift, max(p2px, p2py), len(moving), len(moves))
# ---------------------------------------------------------- microstep modes
def grbl_setting(g, key):
"""One $-setting's value from the controller, or None."""
for line in g.command(key):
if line.startswith(key + "="):
try:
return float(line[len(key) + 1:].split()[0])
except ValueError:
return None
return None
def set_xy_mode(ctx, fc, value):
"""Store xy_microsteps (empty = clear it) and wait for the GRBL
controller forgectrl restarts for it to come back configured, with
the Grbl port answering. Returns the /mode body the machine settled
on."""
if value:
st, body = fc.post("/settings", data={"xy_microsteps": value})
else:
st, body = fc.post("/settings", params={"xy_microsteps": ""})
ctx.check(st == 200, "could not set xy_microsteps=%r (%s: %s)", value or "(clear)", st, body)
mode = int(value or _baseline.XY_MODE_DEFAULT)
ctx.sleep(2.0) # the controller's stop is under way
bl = _baseline.Baseline(ctx.log, abort=ctx.run.aborted.is_set)
body = bl.wait_settled()
ctx.check(isinstance(body, dict) and body.get("controller") == "running",
"the controller did not come back after the xy_microsteps write (/mode: %s)", body)
ctx.check(_baseline.wait_controller_configured(ctx.log, body, xy_mode=mode),
"the controller came back but did not apply its x%d config", mode)
end = time.time() + _baseline.GRBL_PORT_S
while True:
ctx.checkpoint()
try:
with hw.Grbl():
return body
except OSError:
if time.time() > end:
raise Failed("the Grbl port did not answer within %d s of the restart"
% _baseline.GRBL_PORT_S)
time.sleep(1.0)
@test("motion.microstep-modes",
title="XY microstep modes: the setting drives the drivers, the scale and the tick",
subsystem="motion", kind="auto", mode="grbl", est_min=5,
covers=_MOTION_COVERS + [("forgectrl", "src/main.c"), ("forgectrl", "src/settings.*")],
requires=["motion.jog-roundtrip"],
steps=["Bed clear, lid closed; the head needs 40 mm of free +X travel."],
description="One setting, xy_microsteps, is the XY scale. For each mode it admits (8, 16, "
"32): the save restarts the idle controller; the kernel reads the mode on both "
"axes with the mode's machine tick and stop ramp; $100/$101 are the mode's and a "
"typed $100 is overwritten on the spot; a 40 mm jog out and back at the top speed "
"returns to Idle with the kernel counters, over the mode, agreeing with the "
"commanded travel, no drift, and the head accelerometer seeing the head move. "
"The setting is put back as found, with the controller restarted for it.")
def microstep_modes(ctx):
ev = ctx.evidence
fc = ctx.forgectrl
ctx.check(hw.AccelSampler().available,
"no head accelerometer found (i2c %s): the motion witness is missing", hw.HEAD_ACCEL_I2C)
was = (fc.settings() or {}).get("xy_microsteps") or ""
ev["xy_microsteps_before"] = was or "(unset)"
results = []
try:
for mode in _baseline.XY_MODES:
ctx.checkpoint()
ctx.log("--- x%d ---", mode)
set_xy_mode(ctx, fc, str(mode))
r = {"mode": mode, "sysfs": {}}
results.append(r) # in the evidence even when a check fails
for attr, want in _baseline.fixed_sysfs(mode):
if attr not in ("cnc/x_mode", "cnc/y_mode", "cnc/step_freq", "cnc/ramp_rate"):
continue
got = hw.sysfs_read(attr)
r["sysfs"][attr] = got
ctx.check(got == want, "x%d: %s reads %s, want %s", mode, attr, got, want)
spm = _baseline.xy_steps_per_mm(mode)
accel = hw.AccelSampler()
with ctx.grbl() as g, accel:
clean_slate(ctx, g)
for key in ("$100", "$101"):
v = grbl_setting(g, key)
r[key] = v
ctx.check(v is not None and abs(v - spm) < 0.001,
"x%d: %s=%s, want %.3f", mode, key, v, spm)
g.command("$100=53.333")
v = grbl_setting(g, "$100")
r["typed_100"] = v
ctx.check(v is not None and abs(v - spm) < 0.001,
"x%d: a typed $100 stuck at %s (want %.3f: the scale is the mode's)", mode, v, spm)
start = g.status_report()["MPos"]
k0 = kernel_xy_mm(ctx)
t0 = time.time()
rr = g.command("$J=G91X40F12000")
ctx.check(not any(x.startswith("error") for x in rr), "x%d: jog refused: %s", mode, rr)
peak, states, _ = wait_idle(ctx, g, 30)
ctx.check("TIMEOUT" not in states, "x%d: the jog out did not return to Idle: %s", mode, states)
# grblHAL's Idle leads the kernel by the stream depth and
# the decel tail: the counters are read once the machine
# itself is idle.
machine_idle(ctx)
out_mm = kernel_xy_mm(ctx)[0] - k0[0]
r["kernel_out_mm"] = round(out_mm, 3)
r["peak"] = peak
ctx.check(abs(out_mm - 40.0) <= 0.2,
"x%d: the kernel counters say %.3f mm for a 40 mm jog: the scale and the "
"drivers' mode disagree", mode, out_mm)
rr = g.command("$J=G91X-40F12000")
ctx.check(not any(x.startswith("error") for x in rr), "x%d: jog back refused: %s", mode, rr)
peak2, states, _ = wait_idle(ctx, g, 30)
ctx.check("TIMEOUT" not in states, "x%d: the jog back did not return to Idle: %s", mode, states)
machine_idle(ctx)
t1 = time.time()
final = g.status_report()["MPos"]
drift = abs(final[0] - start[0])
p2px, p2py, n = accel.p2p(t0, t1)
r.update({"drift_mm": round(drift, 3), "accel_p2p": [p2px, p2py], "accel_samples": n})
ctx.log("x%d: out %.3f mm by the kernel, peak %.0f/%.0f mm/min, drift %.3f mm, "
"accel p2p x=%d y=%d over %d samples",
mode, out_mm, peak, peak2, drift, p2px, p2py, n)
ctx.check(drift <= 0.05, "x%d: position drift %.3f mm", mode, drift)
ctx.check(max(peak, peak2) >= 6000, "x%d: the jog peaked at only %.0f mm/min", mode,
max(peak, peak2))
ctx.check(max(p2px, p2py) >= hw.ACCEL_P2P_MOVING,
"x%d: the head did not move during the jog (accel p2p x=%d y=%d, below %d): "
"the counters ran without the gantry", mode, p2px, p2py, hw.ACCEL_P2P_MOVING)
machine_idle(ctx)
finally:
ev["modes"] = results
set_xy_mode(ctx, fc, was)
ev["xy_microsteps_after"] = (fc.settings() or {}).get("xy_microsteps") or "(unset)"
ctx.log("PASS: %s", "; ".join("x%d: %.3f mm, drift %.3f, accel %d"
% (r["mode"], r["kernel_out_mm"], r["drift_mm"], max(r["accel_p2p"]))
for r in results))
# ---------------------------------------------------------------- liveness
@test("motion.liveness-probe", title="Supervisor motion-liveness verdict", subsystem="motion",
+59
View File
@@ -271,6 +271,65 @@ class BaselineTests(unittest.TestCase):
{"sysfs": {"cnc/motor_lock": "8", "cnc/step_freq": "10000", "cnc/y_mode": "8"}}))
self.assertFalse(baseline.reference_preconfig({"sysfs": {}}))
# -- the XY microstep mode drives the fixed values ------------------------
def test_the_fixed_values_follow_the_microstep_mode(self):
x8 = dict(baseline.fixed_sysfs(8))
self.assertEqual(x8, dict(baseline.FIXED_SYSFS))
x16 = dict(baseline.fixed_sysfs(16))
self.assertEqual((x16["cnc/x_mode"], x16["cnc/y_mode"], x16["cnc/step_freq"], x16["cnc/ramp_rate"]),
("16", "16", "56320", "250000"))
x32 = dict(baseline.fixed_sysfs(32))
self.assertEqual((x32["cnc/x_mode"], x32["cnc/step_freq"], x32["cnc/ramp_rate"]),
("32", "112640", "500000"))
# everything else is the same list, in the same order
self.assertEqual([a for a, _ in baseline.fixed_sysfs(32)], [a for a, _ in baseline.FIXED_SYSFS])
self.assertEqual(x32["cnc/x_decay"], x8["cnc/x_decay"])
def test_the_mode_is_read_from_the_settings_with_a_default(self):
self.assertEqual(baseline.xy_mode_of({"xy_microsteps": "16"}), 16)
self.assertEqual(baseline.xy_mode_of({"xy_microsteps": "32"}), 32)
self.assertEqual(baseline.xy_mode_of({"xy_microsteps": ""}), 8)
self.assertEqual(baseline.xy_mode_of({}), 8)
self.assertEqual(baseline.xy_mode_of(None), 8)
self.assertEqual(baseline.xy_mode_of({"xy_microsteps": "24"}), 8)
self.assertEqual(baseline.xy_mode_of({"xy_microsteps": "abc"}), 8)
self.assertEqual(baseline.ref_xy_mode({"forgectrl": {"/settings": {"xy_microsteps": "16"}}}), 16)
self.assertEqual(baseline.ref_xy_mode({"forgectrl": {"/settings": None}}), 8)
self.assertEqual(baseline.ref_xy_mode({}), 8)
def test_fixed_constants_are_checked_at_the_reference_mode(self):
ref = {"sysfs": {"cnc/x_mode": "16", "cnc/step_freq": "56320", "cnc/ramp_rate": "250000"},
"forgectrl": {"/settings": {"xy_microsteps": "16"}}}
self.assertEqual(baseline.check_fixed_against(ref, self.lines.append), [])
ref["forgectrl"]["/settings"]["xy_microsteps"] = "8"
diffs = baseline.check_fixed_against(ref, self.lines.append)
self.assertEqual(diffs, ["cnc/x_mode: boot=16 constant=8",
"cnc/step_freq: boot=56320 constant=28160",
"cnc/ramp_rate: boot=250000 constant=125000"])
def test_wait_configured_watches_the_mode_in_force(self):
self._probe_state()
calls = {"n": 0}
def sleep(_s):
calls["n"] += 1
if calls["n"] == 2: # an x16 controller's init writes land
self._attr("cnc/step_freq", "56320")
self._attr("cnc/y_mode", "16")
ok = baseline.wait_controller_configured(
self.lines.append, {"controller": "running", "mode": "grbl", "motion": "verified"},
timeout=5, sleep=sleep, xy_mode=16)
self.assertTrue(ok)
self.assertTrue(any("controller configured" in l for l in self.lines))
# a reference taken under x16 is not pre-config
self.assertFalse(baseline.reference_preconfig(
{"sysfs": {"cnc/step_freq": "56320", "cnc/y_mode": "16"},
"forgectrl": {"/settings": {"xy_microsteps": "16"}}}))
self.assertTrue(baseline.reference_preconfig(
{"sysfs": {"cnc/step_freq": "10000", "cnc/y_mode": "1"},
"forgectrl": {"/settings": {"xy_microsteps": "16"}}}))
def test_stale_preconfig_reference_is_retaken_on_a_fresh_boot(self):
os.environ["FORGETEST_BOOT_ID"] = "test-boot-2"
try: