mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 01:01:12 -07:00
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:
@@ -96,15 +96,18 @@ def wait_mode(ctx, fc, want_mode, want_controller="running", timeout=90):
|
||||
|
||||
|
||||
@test("cloud.mode-switch", title="Controller mode switch grbl -> cloud -> grbl", subsystem="cloud",
|
||||
kind="auto", mode="grbl", est_min=4,
|
||||
covers=_CLOUD_COVERS, requires=["forgectrl.auth", "motion.pacing"],
|
||||
kind="auto", mode="grbl", est_min=5,
|
||||
covers=_CLOUD_COVERS + [("forgectrl", "src/cool.*"), ("forgectrl", "src/airflow.*")],
|
||||
requires=["forgectrl.auth", "motion.pacing"],
|
||||
steps=["Bed clear: the cloud client homes the head to the corner on connect (the factory "
|
||||
"hunt) and the test jogs it back to where it started afterward. Cloud credentials "
|
||||
"configured; the machine on the network."],
|
||||
description="POST /mode switches to the cloud controller: gfcloud comes up under "
|
||||
"supervision, authenticates and establishes its service session (its own log "
|
||||
"lines are the evidence; the connect-time firmware probe is recorded when "
|
||||
"configured); the camera service survives the switch; switching back brings "
|
||||
"configured); its connect-time hunt, which runs with the extraction fans off, "
|
||||
"is measured by the airflow gates but not judged (no AIRFLOW, the exhaust row "
|
||||
"reads unjudged); the camera service survives the switch; switching back brings "
|
||||
"grblHAL up with the Grbl port open and Idle, and the head returns to its start.")
|
||||
def mode_switch(ctx):
|
||||
fc = ctx.forgectrl
|
||||
@@ -157,6 +160,26 @@ def mode_switch(ctx):
|
||||
st, cam1 = fc.get("/cam/status")
|
||||
ev["cam_during_cloud"] = cam1
|
||||
ctx.check(st == 200 and isinstance(cam1, dict), "camera status lost during cloud mode (%s)", st)
|
||||
# The connect-time hunt reports a run to the cooling engine with the
|
||||
# exhaust and the intakes commanded off (the factory's motion profile):
|
||||
# the gates measure it and judge nothing, whatever the floors say.
|
||||
hunt, samples = watch_hunt_gates(ctx, log_offset, HUNT_TIMEOUT_S)
|
||||
ev["hunt"] = hunt
|
||||
ev["hunt_gates"] = samples
|
||||
ctx.check(hunt, "the connect-time hunt did not finish within %d s", HUNT_TIMEOUT_S)
|
||||
run = [c for c in samples if c.get("phase") == "run"]
|
||||
ctx.log("hunt: %d status samples, %d in phase run; verdicts %s", len(samples), len(run),
|
||||
sorted({c.get("verdict") for c in samples}))
|
||||
ctx.check(run, "no /cool/status sample saw the hunt as a run session (the client did not "
|
||||
"report run, or the hunt was shorter than the 1 s poll)")
|
||||
ctx.check(all(c.get("verdict") != "AIRFLOW" for c in samples),
|
||||
"a hunt with its extraction fans off tripped an airflow gate")
|
||||
exh = [((c.get("fan_gates") or {}).get("exhaust") or {}) for c in run]
|
||||
ctx.check(exh and all(g.get("state") == "unjudged" for g in exh),
|
||||
"the exhaust gate judged a hunt: states %s", sorted({g.get("state") for g in exh}))
|
||||
ctx.check(exh and all(g.get("reading", 0) < g.get("floor", 0) for g in exh),
|
||||
"the exhaust was not off during the hunt (readings %s), so the unjudged state "
|
||||
"proves nothing", sorted({g.get("reading") for g in exh}))
|
||||
|
||||
st, body = fc.post("/mode", data={"controller": "grbl"})
|
||||
ctx.log("POST /mode controller=grbl -> %s %s", st, body)
|
||||
@@ -347,6 +370,26 @@ def wait_action_finished(ctx, offset, action, timeout, poll=0.5):
|
||||
return None
|
||||
|
||||
|
||||
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)."""
|
||||
fc = ctx.forgectrl
|
||||
samples = []
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
ctx.checkpoint()
|
||||
st, c = fc.get("/cool/status")
|
||||
if st == 200 and isinstance(c, dict):
|
||||
samples.append(c)
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
i = action_finish_index(lines, "hunt")
|
||||
if i is not None:
|
||||
return lines[i], samples
|
||||
time.sleep(1)
|
||||
return None, samples
|
||||
|
||||
|
||||
def message(line):
|
||||
"""The message part of a log line (after 'ISO-time gfcloud[pid]')."""
|
||||
return line.split(" ", 2)[-1] if line else None
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user