Fan floors from the measurement; a hunt is measured, not judged

forgectrl pin 47e4256: the airflow floors set from the bench measurement
(exhaust 6400, intake 2290, air assist 6000 rpm, purge current 300, grace
15 s) and the operating-point rule: a fan is judged while the laser is
armed, when a job's profile may raise it but never lower it below the run
duty, or whenever it is commanded at the run duty; the service's hunts,
sent with the extraction fans off, are measured and published unjudged.

Catalog: cloud.mode-switch now waits out the connect-time hunt sampling
/cool/status and requires no AIRFLOW, the exhaust row unjudged and the
exhaust actually off; its covers gain forgectrl src/cool.* and
src/airflow.*. cooling.fan-gate-trips uses an 8 s test grace (the
intakes take 7 s to 90 percent and tripped under the old 2 s at the new
floor) and its off leg waits for the row state as well as gates_off.
fan_floor_measure.py names a reply that is not JSON and calls the purge
readings idle and run (the pump is always on).

Docs: COOLING 3a and the settings table, BRINGUP item 19 and a facts-bank
entry with the measured fan speeds, the bench README, and a CAMPAIGN-LOG
entry for the measurement, the two status-document finds, the hunt find
and the hot-deployed bench runs of both tests.
This commit is contained in:
ScottW514
2026-08-22 09:56:53 -04:00
parent 71a79b6675
commit adcd1adb9a
9 changed files with 165 additions and 32 deletions
+6 -4
View File
@@ -342,7 +342,7 @@ def gate_off(ctx):
FAN_KEYS = ("cool_tach_exhaust_min_rpm", "cool_purge_min_current", "cool_fan_grace_s")
FAN_GRACE_S = "2" # the shortest grace the test can wait out with margin
FAN_GRACE_S = "8" # past the intakes' 7 s to 90 percent, so only the leg's floor trips
FAN_TRIP_WAIT_S = 20 # grace + three ticks, with slack for the 1 Hz pipeline
@@ -352,10 +352,11 @@ def _fan_gate(c, name):
@test("cooling.fan-gate-trips", title="A fan under its floor past the grace is a fault; a floor of zero is off",
subsystem="cooling", kind="auto", mode="grbl", est_min=4,
covers=_COOL_COVERS, requires=["cooling.gate-off"],
covers=_COOL_COVERS + [("forgectrl", "src/main.c")], requires=["cooling.gate-off"],
steps=["Machine idle, fans quiet. The test writes the exhaust floor, the purge current floor and "
"the spin-up grace and restores them; four short M8/M9 cycles spin the fans."],
description="The airflow gates run while the cut fan profile is applied. An exhaust floor no fan "
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 "
@@ -410,7 +411,8 @@ def fan_gate_trips(ctx):
# gates_off says so, and the other fans are judged on their own.
_set_gates(ctx, fc, {"cool_tach_exhaust_min_rpm": "0",
"cool_purge_min_current": orig["cool_purge_min_current"]})
c = _run_session(ctx, grbl, fc, lambda c: c.get("verdict") == "OK" and "exhaust" in (c.get("gates_off") or []),
c = _run_session(ctx, grbl, fc, lambda c: c.get("verdict") == "OK" and "exhaust" in (c.get("gates_off") or [])
and _fan_gate(c, "exhaust").get("state") == "off",
"exhaust off leg")
ev["exhaust_off"] = {"cool": c, "gate": _fan_gate(c, "exhaust")}
ctx.check(c.get("verdict") == "OK", "the exhaust floor at zero did not clear the gate: %s", c)