The fan floors proven on a pinned image; a fan fault ends with its session

The dated record of dev image 20260822135848: the campaign of every
non-operator, non-live test at 18 of 18 PASS with the measured floors and
the operating-point rule (cooling.fan-gate-trips and the hunt leg of
cloud.mode-switch as recorded), and the unplugged-exhaust-fan drill:
AIRFLOW at the grace plus three ticks with the exhaust dead, the other
fans held, the reason relayed on the Grbl port, the replugged fan ok
inside the next session's grace.

The drill showed the fault riding into idle, where the hold canceled
jogs and would have refused the cloud print that re-proves the fan.
forgectrl pin d51dbdb: the fault ends with its run session, and the next
session judges every fan afresh. cooling.fan-gate-trips checks the
verdict is OK with no hold once the tripped session is over (the unit
fake mirrors it); its covers, and the cooling tests' shared covers, gain
src/coolfmt.* (the tree manifest carries the new files at the bumped pin,
and the lint was right to ask). cloud.mode-switch samples the hunt's gate
rows twice a second: a hunt's run phase is a few seconds long.

Docs: COOLING 3a, BRINGUP item 19, CAMPAIGN-LOG.
This commit is contained in:
ScottW514
2026-08-22 10:46:41 -04:00
parent adcd1adb9a
commit c1591e4f69
7 changed files with 107 additions and 15 deletions
+3 -3
View File
@@ -372,8 +372,8 @@ def wait_action_finished(ctx, offset, action, timeout, poll=0.5):
def watch_hunt_gates(ctx, offset, timeout):
"""Wait for the action's terminal line like wait_action_finished,
sampling /cool/status once a second meanwhile. Returns (line or
None, samples)."""
sampling /cool/status twice a second meanwhile (a hunt's run phase
can be a few seconds). Returns (line or None, samples)."""
fc = ctx.forgectrl
samples = []
t0 = time.time()
@@ -386,7 +386,7 @@ def watch_hunt_gates(ctx, offset, timeout):
i = action_finish_index(lines, "hunt")
if i is not None:
return lines[i], samples
time.sleep(1)
time.sleep(0.5)
return None, samples
+24 -4
View File
@@ -10,7 +10,7 @@ import time
from ..catalog import test
from .. import hw
_COOL_COVERS = [("forgectrl", "src/cool.*"), ("forgectrl", "src/diag.*"),
_COOL_COVERS = [("forgectrl", "src/cool.*"), ("forgectrl", "src/coolfmt.*"), ("forgectrl", "src/diag.*"),
("forgectrl", "src/gates.*"), ("forgectrl", "src/airflow.*"),
("forgectrl", "src/settings.*"),
("forgectrl", "src/status.*"), ("forgectrl", "src/ui/**"),
@@ -239,6 +239,18 @@ def _run_session(ctx, g, fc, until, what, wait=None):
_session_ended(ctx, fc, what)
def _after_session(ctx, fc, wait=5):
"""The engine's state a few ticks after a session ended."""
c = {}
t0 = time.time()
while time.time() - t0 < wait:
ctx.sleep(1)
c = _cool(fc)
if c.get("phase") != "run" and c.get("verdict") == "OK":
break
return c
def _tail_has(fc, needle):
st, body = fc.get("/logs/tail", params={"name": "forgectrl", "lines": GATE_LOG_LINES})
text = body.get("text", "") if st == 200 and isinstance(body, dict) else ""
@@ -358,9 +370,10 @@ def _fan_gate(c, name):
description="The airflow gates judge a fan commanded at the cut fan profile, which a bare M8 "
"applies, armed or not. An exhaust floor no fan "
"can meet must trip AIRFLOW after the grace plus three ticks (hold, fire blocked, no "
"resume while the session lasts); a purge current floor at the ADC rail must trip the "
"same way; a floor of zero must read off in gates_off and trip nothing; restored, the "
"next session runs OK with every fan reading at or above its floor.")
"resume while the session lasts) and the fault must end with the session (verdict OK, "
"no hold, once the session is over); a purge current floor at the ADC rail must trip "
"the same way; a floor of zero must read off in gates_off and trip nothing; restored, "
"the next session runs OK with every fan reading at or above its floor.")
def fan_gate_trips(ctx):
fc = ctx.forgectrl
ev = ctx.evidence
@@ -397,6 +410,13 @@ def fan_gate_trips(ctx):
ctx.check(_fan_gate(c, "exhaust").get("state") == "TRIPPED",
"the exhaust gate does not read TRIPPED: %s", c.get("fan_gates"))
ctx.check("exhaust" in (c.get("reason") or ""), "the reason does not name the fan: %r", c.get("reason"))
# The fault is the session's: with the session over, the
# verdict is back to OK and nothing holds (jogs and the next
# job's pre-check must not see a fan fault at idle).
c = _after_session(ctx, fc)
ev["exhaust_trip_after"] = c
ctx.check(c.get("verdict") == "OK" and c.get("hold") is False,
"the fan fault outlived its run session: %s", c)
# Leg 2: the purge fan by current, floor at the ADC rail.
_set_gates(ctx, fc, {"cool_tach_exhaust_min_rpm": orig["cool_tach_exhaust_min_rpm"],