mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
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.
208 lines
7.3 KiB
Python
208 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Fan floor measurement: the numbers the airflow gates ship with.
|
|
|
|
Two modes, both sampling the four tachometers (/status fans, rpm) and the
|
|
purge-air current (sysfs head/purge_air_current, raw) once a second:
|
|
|
|
spinup From the engine's idle posture, M8 opens a run session (the cut
|
|
fan profile), the tool samples for --seconds, then M9 ends it and
|
|
the tool waits for the engine to leave phase "run". Per fan it
|
|
reports the steady speed (the mean of the last --steady seconds),
|
|
the time to 90 % of it, and the spread (min, max, standard
|
|
deviation) over the steady window; for the purge fan the current
|
|
at idle and at run duty (the engine holds purge air on at all
|
|
times, so both are the "on" reading; a dead pump reads about 1).
|
|
Needs the controller in GRBL mode, idle, fans quiet.
|
|
|
|
cut No commands: samples for --seconds while the operator runs a
|
|
real cut, and reports the spread the same way. This is the
|
|
number the debounce has to ride over, measured under the only
|
|
load that matters.
|
|
|
|
Results print as a table and land as JSON in the bench data directory
|
|
(FORGETEST_BENCH_DATA, else beside this file): fan_floor_<mode>_<stamp>.json.
|
|
Runs on the board (the bench page) or from a LAN host (gfbench: GF_HOST).
|
|
"""
|
|
import json
|
|
import math
|
|
import socket
|
|
import sys
|
|
import time
|
|
|
|
from gfbench import HOST, LOCAL, board, data_path, forgectrl_request
|
|
|
|
FANS = ("exhaust", "intake_1", "intake_2", "air_assist")
|
|
PURGE_ATTR = "/sys/glowforge/head/purge_air_current"
|
|
|
|
|
|
def usage():
|
|
raise SystemExit("usage: fan_floor_measure.py spinup|cut [--seconds N] [--steady N]")
|
|
|
|
|
|
def purge_current():
|
|
try:
|
|
if LOCAL:
|
|
with open(PURGE_ATTR) as f:
|
|
return int(f.read().strip())
|
|
return int(board("cat " + PURGE_ATTR).strip())
|
|
except (OSError, ValueError):
|
|
return None
|
|
|
|
|
|
def fans_now():
|
|
st, body = forgectrl_request("GET", "/status")
|
|
if st != 200 or not isinstance(body, dict):
|
|
return {}
|
|
return dict(body.get("fans") or {})
|
|
|
|
|
|
def phase():
|
|
st, body = forgectrl_request("GET", "/cool/status")
|
|
if st != 200 or not isinstance(body, dict):
|
|
raise SystemExit("GET /cool/status: HTTP %s with a body that is not a JSON object: %r"
|
|
% (st, body if isinstance(body, str) else type(body).__name__))
|
|
return body.get("phase")
|
|
|
|
|
|
def sample(seconds, label):
|
|
rows = []
|
|
t0 = time.time()
|
|
while time.time() - t0 < seconds:
|
|
t = round(time.time() - t0, 1)
|
|
f = fans_now()
|
|
row = {"t": t, "purge": purge_current()}
|
|
row.update({k: f.get(k, 0) for k in FANS})
|
|
rows.append(row)
|
|
if len(rows) % 10 == 1:
|
|
print(" %s +%5.1f s exhaust %5d intake %5d/%5d air %5d purge %s"
|
|
% (label, t, row["exhaust"], row["intake_1"], row["intake_2"],
|
|
row["air_assist"], row["purge"]))
|
|
time.sleep(max(0.0, 1.0 - ((time.time() - t0) % 1.0)))
|
|
return rows
|
|
|
|
|
|
def stats(vals):
|
|
vals = [v for v in vals if v is not None]
|
|
if not vals:
|
|
return {"n": 0}
|
|
mean = sum(vals) / len(vals)
|
|
sd = math.sqrt(sum((v - mean) ** 2 for v in vals) / len(vals)) if len(vals) > 1 else 0.0
|
|
return {"n": len(vals), "mean": round(mean, 1), "min": min(vals), "max": max(vals),
|
|
"sd": round(sd, 1)}
|
|
|
|
|
|
def spinup_report(rows, steady_s):
|
|
steady = [r for r in rows if r["t"] >= rows[-1]["t"] - steady_s]
|
|
out = {}
|
|
for k in FANS:
|
|
st = stats([r[k] for r in steady])
|
|
target = 0.9 * st.get("mean", 0)
|
|
t90 = next((r["t"] for r in rows if r[k] >= target), None) if target > 0 else None
|
|
st["t90_s"] = t90
|
|
out[k] = st
|
|
out["purge_run"] = stats([r["purge"] for r in steady])
|
|
return out
|
|
|
|
|
|
class Grbl:
|
|
def __init__(self):
|
|
self.s = socket.create_connection((HOST, 23), timeout=5)
|
|
self.s.settimeout(0.15)
|
|
time.sleep(0.5)
|
|
self.drain()
|
|
|
|
def drain(self):
|
|
out = b""
|
|
try:
|
|
while True:
|
|
d = self.s.recv(4096)
|
|
if not d:
|
|
break
|
|
out += d
|
|
except socket.timeout:
|
|
pass
|
|
return out.decode("ascii", "replace")
|
|
|
|
def cmd(self, line, wait=0.4):
|
|
self.s.sendall(line.encode() + b"\n")
|
|
time.sleep(wait)
|
|
return self.drain().strip()
|
|
|
|
def close(self):
|
|
self.s.close()
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) < 2 or argv[1] not in ("spinup", "cut"):
|
|
usage()
|
|
mode = argv[1]
|
|
seconds = 120
|
|
steady_s = 60
|
|
args = argv[2:]
|
|
while args:
|
|
a = args.pop(0)
|
|
if a == "--seconds":
|
|
seconds = int(args.pop(0))
|
|
elif a == "--steady":
|
|
steady_s = int(args.pop(0))
|
|
else:
|
|
usage()
|
|
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|
result = {"mode": mode, "seconds": seconds, "steady_s": steady_s, "started": stamp}
|
|
|
|
if mode == "spinup":
|
|
ph = phase()
|
|
if ph != "idle":
|
|
raise SystemExit("the engine is in phase %r; run this from idle" % ph)
|
|
idle = fans_now()
|
|
result["idle_fans"] = idle
|
|
result["purge_idle"] = purge_current()
|
|
print("idle: fans %s purge %s" % (idle, result["purge_idle"]))
|
|
g = Grbl()
|
|
try:
|
|
print("M8:", g.cmd("M8"))
|
|
rows = sample(seconds, "run")
|
|
finally:
|
|
print("M9:", g.cmd("M9"))
|
|
t0 = time.time()
|
|
while time.time() - t0 < 20 and phase() == "run":
|
|
time.sleep(1)
|
|
g.close()
|
|
result["rows"] = rows
|
|
result["report"] = spinup_report(rows, steady_s)
|
|
rep = result["report"]
|
|
print()
|
|
print("%-11s %8s %8s %8s %6s %7s" % ("fan", "steady", "min", "max", "sd", "t90"))
|
|
for k in FANS:
|
|
r = rep[k]
|
|
print("%-11s %8.0f %8d %8d %6.1f %7s" % (k, r.get("mean", 0), r.get("min", 0),
|
|
r.get("max", 0), r.get("sd", 0),
|
|
"%.0f s" % r["t90_s"] if r.get("t90_s") is not None else "-"))
|
|
po = rep["purge_run"]
|
|
print("purge current: idle %s, run %.0f (min %s max %s); the pump is always on, a dead one reads ~1"
|
|
% (result["purge_idle"], po.get("mean", 0), po.get("min"), po.get("max")))
|
|
print()
|
|
print("candidate floors at 55 %% of steady: " + " ".join(
|
|
"%s %.0f rpm" % (k, 0.55 * rep[k].get("mean", 0)) for k in FANS))
|
|
else:
|
|
print("sampling %d s while the cut runs (no commands sent)" % seconds)
|
|
rows = sample(seconds, "cut")
|
|
result["rows"] = rows
|
|
result["report"] = {k: stats([r[k] for r in rows]) for k in FANS}
|
|
result["report"]["purge"] = stats([r["purge"] for r in rows])
|
|
rep = result["report"]
|
|
print()
|
|
print("%-11s %8s %8s %8s %6s" % ("fan", "mean", "min", "max", "sd"))
|
|
for k in FANS + ("purge",):
|
|
r = rep[k]
|
|
print("%-11s %8.0f %8s %8s %6.1f" % (k, r.get("mean", 0), r.get("min"), r.get("max"), r.get("sd", 0)))
|
|
|
|
path = data_path("fan_floor_%s_%s.json" % (mode, stamp))
|
|
with open(path, "w") as f:
|
|
json.dump(result, f, indent=1)
|
|
print("wrote", path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv)
|