From 884d014d2c6467f2f5fb9465c6a43b6740202245 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sat, 12 Sep 2026 14:08:34 -0400 Subject: [PATCH] Pin forgectrl 0.1.24 (verified fan writes) and cover the lost-duty put-back forgectrl 0.1.24 = 2380e07: every fan duty write is read back and retried, a duty a device lost is put back by the tick, and the airflow fault names the duty commanded and the duty in force. The corner card of the sheet on the bench reference was held as a slow air-assist fan after a run-duty write the head never took; nothing checked. Acceptance: cooling.fan-duty-readback (auto, grbl) opens an M8 session, reads every fan's run duty back, writes the head's air-assist register back to the idle duty and the exhaust PWM to zero behind the engine's back, and holds both to their run duties again within a few ticks, each loss named in the log, the session OK to its end, the idle duties after M9. Covers the head driver too. On the bench reference with forgectrl 0.1.24 hot-deployed it PASSED: both duties back after 0.5 s. --- forgetest/forgetest/suite/cooling.py | 61 +++++++++++++++++++ .../forgectrl/forgectrl-pin.inc | 4 +- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/forgetest/forgetest/suite/cooling.py b/forgetest/forgetest/suite/cooling.py index 0e27aab..86d434e 100644 --- a/forgetest/forgetest/suite/cooling.py +++ b/forgetest/forgetest/suite/cooling.py @@ -1329,3 +1329,64 @@ def flow_under_load(ctx): ctx.check(dt is not None, "the window did not close after M2") ctx.log("PASS: lit through the window, raw rise %.1f C, laser %.1f C off, judged %.1f C against %.1f " "(dT %.1f), window closed %.1f s after M2", raw, share, rise, float(limit), float(dtc), dt) + + +@test("cooling.fan-duty-readback", title="A fan duty a device lost is read back and put back inside a tick", + subsystem="cooling", kind="auto", mode="grbl", est_min=2, + covers=_COOL_COVERS + [("kernel-module-glowforge", "src/head*")], + requires=["kernel.latch-locked-idle"], + description="M8 opens a run session and every fan reads its run duty back from its device. " + "The head's air-assist register is then written back to the idle duty behind the " + "engine's back (what a head reset or a dropped transaction leaves) and the exhaust " + "PWM to zero: within a few ticks each reads its run duty again, the log names each " + "loss and its put-back, and the session stays OK with no airflow fault (a fan put " + "back gets its spin-up grace). Then M9 and the idle duties.") +def fan_duty_readback(ctx): + fc = ctx.forgectrl + ev = ctx.evidence + run = {"head/air_assist_pwm": 1023, "thermal/exhaust_pwm": 65535, "thermal/intake_pwm": 43278} + with ctx.grbl() as g: + c = _hold_session(ctx, g, fc, lambda c: c.get("phase") == "run", "readback", 20) + try: + ctx.check(c.get("phase") == "run", "no run session opened: %s", c) + ctx.sleep(2) + before = {k: hw.sysfs_int(k) for k in run} + ctx.log("duties at run: %s", before) + ev["run_duties"] = before + ctx.check(all(before.get(k) == v for k, v in run.items()), + "not every fan reads its run duty: %s", before) + hw.sysfs_write("head/air_assist_pwm", 204) + hw.sysfs_write("thermal/exhaust_pwm", 0) + lost = {k: hw.sysfs_int(k) for k in ("head/air_assist_pwm", "thermal/exhaust_pwm")} + ctx.log("written behind the engine's back: %s", lost) + back = {} + t0 = time.time() + while time.time() - t0 < 6: + ctx.sleep(0.5) + back = {k: hw.sysfs_int(k) for k in ("head/air_assist_pwm", "thermal/exhaust_pwm")} + if back.get("head/air_assist_pwm") == 1023 and back.get("thermal/exhaust_pwm") == 65535: + break + took = round(time.time() - t0, 1) + ev["put_back"] = {"duties": back, "after_s": took} + ctx.log("put back after %s s: %s", took, back) + ctx.check(back.get("head/air_assist_pwm") == 1023, "the air-assist duty was not put back: %s", back) + ctx.check(back.get("thermal/exhaust_pwm") == 65535, "the exhaust duty was not put back: %s", back) + ctx.sleep(2) + ctx.check(_tail_has(fc, "head/air_assist_pwm read 204 with 1023 commanded: put back"), + "the air-assist loss is not named in the log") + ctx.check(_tail_has(fc, "thermal/exhaust_pwm read 0 with 65535 commanded: put back"), + "the exhaust loss is not named in the log") + for _ in range(8): + ctx.sleep(1) + c = _cool(fc) + if c.get("verdict") != "OK": + break + ev["verdict_after"] = {"verdict": c.get("verdict"), "hold": c.get("hold"), "reason": c.get("reason")} + ctx.check(c.get("verdict") == "OK" and not c.get("hold"), + "the session did not stay OK after the put-back: %s %s", c.get("verdict"), c.get("reason")) + finally: + g.command("M9") + _session_ended(ctx, fc, "readback") + idle = {k: hw.sysfs_int(k) for k in run} + ev["idle_duties"] = idle + ctx.log("duties at idle: %s", idle) diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc index 9ca1a00..7f9d923 100644 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc +++ b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc @@ -2,5 +2,5 @@ # only SRCREV and PV here - the image manifest leaves *-pin.inc out of the # layer content hash because the component entry already identifies the # pinned source (forgefirm-image-manifest.bbclass). -SRCREV = "cb8c140d488c2bf5e2a45302cbfad928d4323c47" -PV = "0.1.23" +SRCREV = "2380e07cce024e40f6097bdbe8fdb2034b5bd624" +PV = "0.1.24"