mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -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:
+7
-1
@@ -1502,7 +1502,13 @@ forgectrl restarts; the fresh-boot dump of this image (uptime 235 s)
|
|||||||
confirmed the fixed values (`motor_lock 8`, `x/y_mode 8`, `x/y_decay 1`,
|
confirmed the fixed values (`motor_lock 8`, `x/y_mode 8`, `x/y_decay 1`,
|
||||||
`step_freq 28160` - the controller's tick, not the probe's 10000 -
|
`step_freq 28160` - the controller's tick, not the probe's 10000 -
|
||||||
`ramp_rate 125000`, hold currents 33/5, lamps and button LEDs 0, heater
|
`ramp_rate 125000`, hold currents 33/5, lamps and button LEDs 0, heater
|
||||||
and TEC off). Proof: k1-k2 / k3 / fire-abu re-run under the baseline -
|
and TEC off). A reference dumped at uptime 30 s on 2026-08-16 showed the
|
||||||
|
probe values instead (`motor_lock 0`, `step_freq 10000`, `y_mode 1`): the
|
||||||
|
dump raced the controller's init writes - `/mode` reports `running` at the
|
||||||
|
spawn, not at the config - so `boot_reference()` now waits for the
|
||||||
|
controller's markers (`step_freq`/`motor_lock`/`y_mode` at their fixed
|
||||||
|
values, bounded 20 s) before dumping, and retakes a pre-config reference
|
||||||
|
while the boot is still fresh. Proof: k1-k2 / k3 / fire-abu re-run under the baseline -
|
||||||
counters (0,0,0) before and after, the probe verified in 3 s after every
|
counters (0,0,0) before and after, the probe verified in 3 s after every
|
||||||
takeover, no ladder, `post: clean` every time. (2) Two forgectrl items
|
takeover, no ladder, `post: clean` every time. (2) Two forgectrl items
|
||||||
for the operator's decision, not changed: the liveness probe should write
|
for the operator's decision, not changed: the liveness probe should write
|
||||||
|
|||||||
@@ -517,8 +517,10 @@ def dump_all(bl):
|
|||||||
|
|
||||||
|
|
||||||
def check_fixed_against(ref, log):
|
def check_fixed_against(ref, log):
|
||||||
"""Compare the fixed constants with a fresh-boot dump; log the diffs
|
"""Compare the fixed constants with a fresh-boot dump; log the diffs.
|
||||||
(a differing constant is a fact about this machine, not a leftover)."""
|
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 = []
|
diffs = []
|
||||||
sysfs = ref.get("sysfs") or {}
|
sysfs = ref.get("sysfs") or {}
|
||||||
for attr, want in FIXED_SYSFS + IDLE_READBACKS:
|
for attr, want in FIXED_SYSFS + IDLE_READBACKS:
|
||||||
@@ -530,6 +532,52 @@ def check_fixed_against(ref, log):
|
|||||||
return diffs
|
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):
|
def boot_reference(log, data_dir):
|
||||||
"""The fresh-boot idle state of this boot: loaded from
|
"""The fresh-boot idle state of this boot: loaded from
|
||||||
<data_dir>/boot-<boot_id>.json when forgetest already took it, taken
|
<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:
|
if not bid:
|
||||||
return None
|
return None
|
||||||
path = os.path.join(data_dir, "boot-%s.json" % bid)
|
path = os.path.join(data_dir, "boot-%s.json" % bid)
|
||||||
|
up = uptime_s()
|
||||||
try:
|
try:
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
ref = json.load(f)
|
ref = json.load(f)
|
||||||
log("baseline: fresh-boot reference loaded (%s, taken %s)" % (path, ref.get("ts")))
|
# A reference dumped before the controller had applied its config
|
||||||
return ref
|
# (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):
|
except (OSError, ValueError):
|
||||||
pass
|
pass
|
||||||
up = uptime_s()
|
|
||||||
if up is None or up > BOOT_MAX_AGE_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); "
|
log("baseline: no fresh-boot reference for this boot (uptime %s s > %d s); "
|
||||||
"power-cycle to take one" % (up, BOOT_MAX_AGE_S))
|
"power-cycle to take one" % (up, BOOT_MAX_AGE_S))
|
||||||
return None
|
return None
|
||||||
bl = Baseline(log)
|
bl = Baseline(log)
|
||||||
bl.wait_settled()
|
mode = bl.wait_settled()
|
||||||
|
wait_controller_configured(log, mode)
|
||||||
ref = dump_all(bl)
|
ref = dump_all(bl)
|
||||||
ref.update({"ts": now_ts(), "boot_id": bid, "uptime_s": uptime_s()})
|
ref.update({"ts": now_ts(), "boot_id": bid, "uptime_s": uptime_s()})
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""The baseline: fixed resting values are restored, preserved values are
|
"""The baseline: fixed resting values are restored, preserved values are
|
||||||
handed back, every deviation is a recorded leftover. Runs against a fake
|
handed back, every deviation is a recorded leftover. Runs against a fake
|
||||||
sysfs tree; forgectrl is unreachable (service-side checks skip)."""
|
sysfs tree; forgectrl is unreachable (service-side checks skip)."""
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import struct
|
import struct
|
||||||
@@ -161,5 +162,86 @@ class BaselineTests(unittest.TestCase):
|
|||||||
self.assertEqual(diffs, ["cnc/step_freq: boot=10000 constant=28160"])
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user