mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 09:11:11 -07:00
baseline: take the fresh-boot reference after the controller applied its config
/mode reports controller=running at the spawn, so the reference dump raced grblHAL's init writes and captured the supervisor's motion-probe values (motor_lock 0, step_freq 10000, y_mode at the module default) instead of the resting state. boot_reference() now waits for the controller's markers (step_freq/motor_lock/y_mode at their fixed values, bounded 20 s, GRBL mode only) plus a 1 s settle before dumping, retakes a saved reference that shows the pre-config state while the boot is still fresh, and marks it otherwise. Tests cover the wait, its timeout, the non-GRBL no-op, the pre-config recognition and the retake.
This commit is contained in:
@@ -517,8 +517,10 @@ def dump_all(bl):
|
||||
|
||||
|
||||
def check_fixed_against(ref, log):
|
||||
"""Compare the fixed constants with a fresh-boot dump; log the diffs
|
||||
(a differing constant is a fact about this machine, not a leftover)."""
|
||||
"""Compare the fixed constants with a fresh-boot dump; log the diffs.
|
||||
With the dump taken after the controller applied its config (see
|
||||
wait_controller_configured) a differing value is a fact about this
|
||||
machine worth a look, not a leftover."""
|
||||
diffs = []
|
||||
sysfs = ref.get("sysfs") or {}
|
||||
for attr, want in FIXED_SYSFS + IDLE_READBACKS:
|
||||
@@ -530,6 +532,52 @@ def check_fixed_against(ref, log):
|
||||
return diffs
|
||||
|
||||
|
||||
# The attributes the GRBL controller writes at its own start (its analog
|
||||
# config + machine tick): once they read the fixed values the controller
|
||||
# has configured the machine. Before that the kernel shows the supervisor's
|
||||
# motion-probe leftovers (motor_lock 0, step_freq 10000, y_mode at the
|
||||
# module default) - the state /mode already calls "running", because
|
||||
# "running" is the spawn, not the config.
|
||||
CONFIGURED_MARKERS = [(a, dict(FIXED_SYSFS)[a]) for a in ("cnc/step_freq", "cnc/motor_lock", "cnc/y_mode")]
|
||||
CONFIGURED_TIMEOUT_S = 20
|
||||
CONFIGURED_SETTLE_S = 1.0
|
||||
|
||||
|
||||
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 /
|
||||
motor_lock 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]
|
||||
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):
|
||||
"""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."""
|
||||
if not (isinstance(mode_body, dict) and mode_body.get("controller") == "running"
|
||||
and (mode_body.get("mode") or "grbl") == "grbl"):
|
||||
return True
|
||||
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)):
|
||||
sleep(CONFIGURED_SETTLE_S)
|
||||
log("baseline: controller configured %.1f s after running" % (time.time() - t0))
|
||||
return True
|
||||
if any(g is None for _, g in got):
|
||||
return True # no kernel sysfs (host run): nothing to wait for
|
||||
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)))
|
||||
return False
|
||||
|
||||
|
||||
def boot_reference(log, data_dir):
|
||||
"""The fresh-boot idle state of this boot: loaded from
|
||||
<data_dir>/boot-<boot_id>.json when forgetest already took it, taken
|
||||
@@ -539,20 +587,33 @@ def boot_reference(log, data_dir):
|
||||
if not bid:
|
||||
return None
|
||||
path = os.path.join(data_dir, "boot-%s.json" % bid)
|
||||
up = uptime_s()
|
||||
try:
|
||||
with open(path) as f:
|
||||
ref = json.load(f)
|
||||
log("baseline: fresh-boot reference loaded (%s, taken %s)" % (path, ref.get("ts")))
|
||||
return ref
|
||||
# A reference dumped before the controller had applied its config
|
||||
# (the supervisor's probe values still showing) is retaken while
|
||||
# the boot is fresh enough; otherwise it stands, marked.
|
||||
if reference_preconfig(ref):
|
||||
if up is not None and up <= BOOT_MAX_AGE_S:
|
||||
log("baseline: fresh-boot reference %s predates the controller's config; retaking" % path)
|
||||
ref = None
|
||||
else:
|
||||
log("baseline: NOTE fresh-boot reference %s predates the controller's config "
|
||||
"(taken %s); the machine's probe values are in it - power-cycle to retake"
|
||||
% (path, ref.get("ts")))
|
||||
if ref is not None:
|
||||
log("baseline: fresh-boot reference loaded (%s, taken %s)" % (path, ref.get("ts")))
|
||||
return ref
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
up = uptime_s()
|
||||
if up is None or up > BOOT_MAX_AGE_S:
|
||||
log("baseline: no fresh-boot reference for this boot (uptime %s s > %d s); "
|
||||
"power-cycle to take one" % (up, BOOT_MAX_AGE_S))
|
||||
return None
|
||||
bl = Baseline(log)
|
||||
bl.wait_settled()
|
||||
mode = bl.wait_settled()
|
||||
wait_controller_configured(log, mode)
|
||||
ref = dump_all(bl)
|
||||
ref.update({"ts": now_ts(), "boot_id": bid, "uptime_s": uptime_s()})
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The baseline: fixed resting values are restored, preserved values are
|
||||
handed back, every deviation is a recorded leftover. Runs against a fake
|
||||
sysfs tree; forgectrl is unreachable (service-side checks skip)."""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
@@ -161,5 +162,86 @@ class BaselineTests(unittest.TestCase):
|
||||
self.assertEqual(diffs, ["cnc/step_freq: boot=10000 constant=28160"])
|
||||
|
||||
|
||||
# -- the reference is taken after the controller applied its config -----
|
||||
|
||||
def _probe_state(self):
|
||||
# what the kernel shows between the supervisor's motion probe and
|
||||
# the GRBL controller's init writes
|
||||
self._attr("cnc/motor_lock", "0")
|
||||
self._attr("cnc/step_freq", "10000")
|
||||
self._attr("cnc/y_mode", "1")
|
||||
|
||||
def test_wait_configured_returns_once_the_controller_wrote_its_config(self):
|
||||
self._probe_state()
|
||||
calls = {"n": 0}
|
||||
|
||||
def sleep(_s):
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 3: # the controller's init writes land
|
||||
for attr, val in baseline.CONFIGURED_MARKERS:
|
||||
self._attr(attr, val)
|
||||
ok = baseline.wait_controller_configured(
|
||||
self.lines.append, {"controller": "running", "mode": "grbl", "motion": "verified"},
|
||||
timeout=5, sleep=sleep)
|
||||
self.assertTrue(ok)
|
||||
self.assertTrue(any("controller configured" in l for l in self.lines))
|
||||
self.assertGreaterEqual(calls["n"], 4) # 3 polls + the settle
|
||||
|
||||
def test_wait_configured_times_out_and_says_so(self):
|
||||
self._probe_state()
|
||||
t = {"now": 0.0}
|
||||
real_time = baseline.time.time
|
||||
baseline.time.time = lambda: t["now"]
|
||||
try:
|
||||
def sleep(s):
|
||||
t["now"] += s
|
||||
ok = baseline.wait_controller_configured(
|
||||
self.lines.append, {"controller": "running", "mode": "grbl"}, timeout=2, sleep=sleep)
|
||||
finally:
|
||||
baseline.time.time = real_time
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("did not apply its config" in l for l in self.lines))
|
||||
|
||||
def test_wait_configured_is_a_noop_outside_grbl_mode(self):
|
||||
self._probe_state()
|
||||
ok = baseline.wait_controller_configured(
|
||||
self.lines.append, {"controller": "running", "mode": "cloud"}, timeout=1,
|
||||
sleep=lambda s: self.fail("slept in cloud mode"))
|
||||
self.assertTrue(ok)
|
||||
ok = baseline.wait_controller_configured(
|
||||
self.lines.append, {"controller": "stopped", "mode": "grbl"}, timeout=1,
|
||||
sleep=lambda s: self.fail("slept with the controller stopped"))
|
||||
self.assertTrue(ok)
|
||||
|
||||
def test_preconfig_reference_is_recognized(self):
|
||||
self.assertTrue(baseline.reference_preconfig(
|
||||
{"sysfs": {"cnc/motor_lock": "0", "cnc/step_freq": "10000", "cnc/y_mode": "1"}}))
|
||||
self.assertFalse(baseline.reference_preconfig(
|
||||
{"sysfs": {"cnc/motor_lock": "8", "cnc/step_freq": "28160", "cnc/y_mode": "8"}}))
|
||||
# a genuinely different single constant is a machine fact, not pre-config
|
||||
self.assertFalse(baseline.reference_preconfig(
|
||||
{"sysfs": {"cnc/motor_lock": "8", "cnc/step_freq": "10000", "cnc/y_mode": "8"}}))
|
||||
self.assertFalse(baseline.reference_preconfig({"sysfs": {}}))
|
||||
|
||||
def test_stale_preconfig_reference_is_retaken_on_a_fresh_boot(self):
|
||||
os.environ["FORGETEST_BOOT_ID"] = "test-boot-2"
|
||||
try:
|
||||
path = os.path.join(self.tmp, "boot-test-boot-2.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump({"ts": "old", "sysfs": {"cnc/motor_lock": "0", "cnc/step_freq": "10000",
|
||||
"cnc/y_mode": "1"}}, f)
|
||||
ref = baseline.boot_reference(self.lines.append, self.tmp)
|
||||
up = baseline.uptime_s()
|
||||
if up is None or up > baseline.BOOT_MAX_AGE_S:
|
||||
# too old to retake: the stale reference stands, marked
|
||||
self.assertTrue(any("predates the controller's config" in l for l in self.lines))
|
||||
self.assertEqual(ref["ts"], "old")
|
||||
else:
|
||||
self.assertTrue(any("retaking" in l for l in self.lines))
|
||||
self.assertEqual(ref["sysfs"]["cnc/motor_lock"], "8")
|
||||
finally:
|
||||
os.environ.pop("FORGETEST_BOOT_ID", None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user