mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
The critical tier proven on a rising loop; the board temperatures watched
forgectrl pin 76115fd: the chassis LM75 and the supply sensor ride /status as temps (degrees and a raw count), the engine ranges them over every run session into one run-end line, and a critical fault that clears with its session yields the reason to the standing hold. Bench: critical_tier_drill.py (a bench tool now, registered as critical-tier) sets the ceiling, the resume gate and the critical line a few tenths above the live upstream reading and lets the engine's own flow-check heater warm the loop through them inside one M8 session; temp_calibrate.py gains supply-watch, supply-point and supply-fit for the supply sensor against a thermometer on its heatsink, the fit shown beside UAPI.md's unverified guess. Catalog: cooling.gate-off checks the /status temps fields and the run-end board-temperature line (the unit fake mirrors both, one new failure case); cooling.critical-tier checks the reason after a faulted session. Docs: CAMPAIGN-LOG entries for cooling.critical-tier on dev image 20260822154257 and the warm-loop drill (OVERTEMP at 10 s, CRITICAL at 14 s, the fault ending with the session); BRINGUP item 19, the facts bank (board temperatures at idle), COOLING section 9, the bench README.
This commit is contained in:
+18
-3
@@ -623,6 +623,11 @@ until `releases/v<version>/acceptance.json` is committed.
|
||||
|
||||
## Hardware facts bank (measured)
|
||||
|
||||
- **Board temperatures at idle** (room ~22 C, machine on for hours): the
|
||||
chassis LM75 reads **29.0 C**, `pic/pwr_temp` reads **589 raw** (the
|
||||
unverified guess `raw * 0.08715 - 21` would make that 30.3 C; a thermometer
|
||||
on the supply heatsink decides). Ranged per job by the engine from here
|
||||
on.
|
||||
- **Fan speeds at the cut profile** (exhaust duty 65535, intake 43278, air
|
||||
assist 1023; sampled at 1 Hz over 120 s from idle, the exhaust duct's
|
||||
inline booster fan off): exhaust **11640 rpm** steady (spread 11444 to
|
||||
@@ -1375,9 +1380,19 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
cross-check) is the fail tier above the ceiling's pause: at or over it
|
||||
in a run session the verdict is `CRITICAL` (fire blocked, hold, no
|
||||
resume this job), the fault ends with the session, no header touches
|
||||
it; `cooling.critical-tier` in the catalog (45), bench run owed on the
|
||||
next image, plus the heater-driven warm-loop drill to the critical
|
||||
line. The watch-only board temperatures follow.
|
||||
it; `cooling.critical-tier` in the catalog (45), PASS on dev image
|
||||
`20260822154257`, and the warm-loop drill (`critical_tier_drill.py`)
|
||||
PASS the same day on a genuinely rising loop: `OVERTEMP` at the
|
||||
ceiling, `CRITICAL` four seconds later at the line, the fault ending
|
||||
with the session (CAMPAIGN-LOG). **The board temperatures are watched:**
|
||||
the chassis LM75 (degrees) and the supply sensor (raw count, its
|
||||
conversion still unverified) ride `/status` as `temps`, show on the
|
||||
Status tab, and are ranged over every run session into one run-end
|
||||
log line; no gate behind either until the record says where one
|
||||
belongs, and the supply's conversion waits on thermometer points at
|
||||
the heatsink during a long cut (`temp_calibrate.py supply-point`,
|
||||
three points, then `supply-fit`). Bench run of the status fields and
|
||||
the log line owed on the next image.
|
||||
|
||||
Nothing here can put energy where it was not commanded: the hardware chain
|
||||
is the emission boundary and no header field touches it, and forgectrl runs
|
||||
|
||||
@@ -3368,6 +3368,40 @@ and flashed the same day (dev image `20260822145201`, forgectrl d51dbdb):
|
||||
`cooling.fan-gate-trips` PASS in 60 s, the engine reading `OK`, no hold,
|
||||
fire allowed in the smoke-clear phase right after the tripped session.
|
||||
|
||||
## 2026-08-22: the coolant critical tier, on an image and on a rising loop
|
||||
|
||||
Dev image `20260822154257` (forgectrl a1875a8 pinned by forgefirm f51140e):
|
||||
`cooling.critical-tier` PASS in 23 s. As recorded: the settings API refused
|
||||
a critical line equal to the ceiling (`400 cool_temp_critical_c must be
|
||||
above cool_temp_max`) and changed nothing; with the ceiling at 6 C, the
|
||||
resume gate at 5 C and the critical line at 7 C under 24.3 C coolant the
|
||||
session read `CRITICAL` (`fire_ok false`, `hold true`, no `resume_ok`,
|
||||
reason `CRITICAL: coolant 24.3 C at or over the 7 C critical line - hold,
|
||||
no resume this job`); after the session the ceiling alone held
|
||||
(`OVERTEMP`); with the critical line at its top of 70 the gate was off
|
||||
(`gates_off` naming `coolant_critical`, the run-start log line) and the
|
||||
ceiling alone paused; restored, `OK` with nothing off.
|
||||
|
||||
The physical drill, `critical_tier_drill.py`, the same day. The loop heater
|
||||
reaches the high twenties at most, so the lines were set a few tenths
|
||||
above the live upstream reading (24.57 C: ceiling 25.0, resume 24.8,
|
||||
critical 25.3) and the engine's own flow-check heater (100 percent, 300 s
|
||||
windows, rechecks every 30 s, the suspect threshold at its top) warmed the
|
||||
loop through them inside one `M8` session. Transitions, as sampled once a
|
||||
second: `OK` at 24.1 C; `OVERTEMP` at 10 s with the upstream at 25.05 C
|
||||
(`coolant 25.0 C over 25 C limit - hold until 25 C`); `CRITICAL` at 14 s
|
||||
at 26.24 C (`fire_ok false`, `hold true`, relayed on the Grbl port as a
|
||||
`[MSG:Warning: CRITICAL: ...]`). `M9` ended the fault and the ceiling's
|
||||
`OVERTEMP` stood in the smoke-clear phase (upstream 27.7 C, downstream
|
||||
49.7 C from the heater, which the session end switched off). Settings
|
||||
restored and re-read in a short session: `OK`, limits back to 33 / 31 / 38.
|
||||
|
||||
One find, cosmetic: after the session the reason text still read the
|
||||
critical line's message under the ceiling's verdict, because the ceiling
|
||||
names itself only on its rising edge and the critical fault had overwritten
|
||||
it. The engine now re-publishes the standing hold's reason when a critical
|
||||
fault clears (no new log line), and `cooling.critical-tier` checks it.
|
||||
|
||||
## Superseded status notes
|
||||
|
||||
### Shared machine services — remaining polish, as listed 2026-08-13
|
||||
|
||||
@@ -435,6 +435,12 @@ Stated plainly so nobody counts on them:
|
||||
cannot be detected (the output has no readback), so this will become a user
|
||||
setting plus a simple hysteresis around the factory's setpoints.
|
||||
- **A fire watch that acts** (§7).
|
||||
- **Chassis and supply ceilings.** Both temperatures are measured and not
|
||||
gated: the chassis LM75 in degrees and the supply sensor as a raw count,
|
||||
in `/status` as `temps`, and ranged over every job in one log line
|
||||
(`temps this job: ...`). A ceiling for each comes from that record once
|
||||
there is enough of it, and the supply's conversion from a thermometer on
|
||||
its heatsink (`temp_calibrate.py supply-point`).
|
||||
- **Fan floors measured on more than one machine.** The shipped floors are
|
||||
a fraction of one bench machine's run-duty speeds; a machine whose fans
|
||||
read differently sets its own (§8), and a floor of zero turns that gate
|
||||
|
||||
@@ -126,6 +126,13 @@ TOOLS = [
|
||||
"args": [_arg("cycles", "int", 3, "cycles per case (flow / no-flow)")],
|
||||
"desc": "Runs the real check (40 percent / 50 s, cut-profile fans) from a heater-warmed baseline, alternating "
|
||||
"flow and no-flow; results to the bench data directory. Slow: about 15 minutes per cycle."},
|
||||
{"id": "critical-tier", "title": "Coolant critical-tier warm-loop drill", "script": "critical_tier_drill.py",
|
||||
"safety": "dry", "where": "board", "ported": True,
|
||||
"args": [_arg("max_seconds", "int", 1200, "give up after this long without CRITICAL", flag="--max-seconds")],
|
||||
"desc": "Sets the ceiling, the resume gate and the critical line a few tenths above the live upstream "
|
||||
"reading and lets the engine's own flow-check heater warm the loop through them inside one M8 "
|
||||
"session: OVERTEMP at the ceiling, then CRITICAL (fire blocked, hold, no resume), the fault ending "
|
||||
"with the session; settings restored and re-read. Results to the bench data directory."},
|
||||
{"id": "flow-recheck", "title": "Coolant re-check characterization", "script": "flow_recheck_char.py",
|
||||
"safety": "takeover", "where": "board", "ported": True,
|
||||
"args": [_arg("heater_pct", "int", 50, "heater duty percent"), _arg("window_s", "int", 30, "re-check window")],
|
||||
@@ -144,10 +151,13 @@ TOOLS = [
|
||||
"desc": "Prints elapsed,raw_down,raw_up at the interval; the sampler behind the flow tools."},
|
||||
{"id": "temp-calibrate", "title": "Coolant temperature spot-check", "script": "temp_calibrate.py",
|
||||
"safety": "dry", "where": "board", "ported": True,
|
||||
"args": [_arg("mode", "choice", "watch", "watch / point / fit", ["watch", "point", "fit"]),
|
||||
"args": [_arg("mode", "choice", "watch", "coolant: watch / point / fit; supply (pic/pwr_temp): "
|
||||
"supply-watch / supply-point / supply-fit",
|
||||
["watch", "point", "fit", "supply-watch", "supply-point", "supply-fit"]),
|
||||
_arg("value", "str", None, "point: the thermometer reading in C; watch: seconds (default 60)")],
|
||||
"desc": "Pairs a measured temperature with averaged raw readings; fits a per-machine line. Points "
|
||||
"accumulate in the bench data directory."},
|
||||
"desc": "Pairs a measured temperature with averaged raw readings; fits a per-machine line. The coolant "
|
||||
"sensors, or the power supply's (thermometer on its heatsink, against the unverified guess in "
|
||||
"UAPI.md). Points accumulate in the bench data directory."},
|
||||
{"id": "fan-test", "title": "Fan/coolant bench", "script": "fan_test.py",
|
||||
"safety": "dry", "where": "board", "ported": True, "args": [],
|
||||
"desc": "Snapshots fan PWMs/tachs/temps, drives M8 -> cut fans, M9 -> cooldown -> idle; the tach "
|
||||
|
||||
@@ -270,7 +270,9 @@ def _tail_has(fc, needle):
|
||||
"(OVERTEMP, hold, fire blocked) at the next run start; set to its top the "
|
||||
"engine must skip the gate (verdict OK), report it in gates_off on /status "
|
||||
"and /cool/status, say so in the settings reply, and log the run-start line; "
|
||||
"restored, everything reads as before.")
|
||||
"restored, everything reads as before. Alongside: /status carries the watched "
|
||||
"board temperatures (chassis in degrees, supply as a raw count) and every run "
|
||||
"session ends with them ranged into one log line.")
|
||||
def gate_off(ctx):
|
||||
fc = ctx.forgectrl
|
||||
ev = ctx.evidence
|
||||
@@ -278,8 +280,17 @@ def gate_off(ctx):
|
||||
orig = {k: before.get(k, "") for k in GATE_KEYS}
|
||||
ev["orig"] = orig
|
||||
ctx.log("original: %s", orig)
|
||||
up = (fc.status().get("coolant") or {}).get("up_c")
|
||||
st0 = fc.status()
|
||||
up = (st0.get("coolant") or {}).get("up_c")
|
||||
ctx.check(up is not None and up > 8.0, "coolant too cold for the trip leg (up_c %s)", up)
|
||||
# The board temperatures ride /status, watched and not gated: the
|
||||
# chassis in degrees, the supply as its raw count.
|
||||
temps = st0.get("temps") or {}
|
||||
ev["temps"] = temps
|
||||
ctx.check(isinstance(temps.get("chassis_c"), (int, float)) and 5.0 <= temps["chassis_c"] <= 80.0,
|
||||
"/status temps.chassis_c is not a plausible chassis temperature: %s", temps)
|
||||
ctx.check(isinstance(temps.get("supply_raw"), int) and 0 <= temps["supply_raw"] <= 1023,
|
||||
"/status temps.supply_raw is not a 10-bit count: %s", temps)
|
||||
c0 = _cool(fc)
|
||||
ctx.check(c0.get("verdict") == "OK", "engine is not at OK before the test: %s", c0)
|
||||
ctx.check(c0.get("gates_off") == [], "a gate is already off: %s", c0.get("gates_off"))
|
||||
@@ -336,6 +347,10 @@ def gate_off(ctx):
|
||||
ctx.check(c.get("verdict") == "OK" and c.get("gates_off") == [],
|
||||
"engine did not return to OK with no gate off after the restore: %s", c)
|
||||
ctx.log("restored ceiling %s reports state %s", val, state)
|
||||
# Every run session ends with the board temperatures ranged
|
||||
# into one line.
|
||||
ctx.check(_tail_has(fc, "temps this job: chassis "),
|
||||
"the run-end board-temperature line is missing from the forgectrl log")
|
||||
finally:
|
||||
if not restored:
|
||||
# The engine reads settings at run start only: restoring
|
||||
@@ -436,6 +451,9 @@ def critical_tier(ctx):
|
||||
ctx.check(c.get("verdict") == "OVERTEMP",
|
||||
"after the faulted session the engine reads %s, expected the ceiling's OVERTEMP: %s",
|
||||
c.get("verdict"), c)
|
||||
ctx.check("CRITICAL" not in (c.get("reason") or ""),
|
||||
"the reason still names the ended critical fault under the ceiling's hold: %r",
|
||||
c.get("reason"))
|
||||
|
||||
# Leg 2: the critical line at its top is the gate off; the
|
||||
# ceiling alone pauses, and gates_off says so.
|
||||
|
||||
@@ -229,11 +229,13 @@ class GateOffTests(unittest.TestCase):
|
||||
cooling.SESSION_END_WAIT_S = 3
|
||||
self.fc.state["status"] = dict(self.fc.state["status"],
|
||||
coolant={"down_c": 22.4, "up_c": 22.3, "pump": True, "tec": False},
|
||||
temps={"chassis_c": 29.0, "supply_raw": 589},
|
||||
gates_off=[])
|
||||
self.fc.state["cool"] = {"phase": "idle", "verdict": "OK", "fire_ok": False, "hold": False,
|
||||
"gates_off": []}
|
||||
self.fc.state["settings"].update({"cool_temp_max": "", "cool_temp_resume": ""})
|
||||
self.log_line = True # the engine writes its run-start line
|
||||
self.temps_line = True # the engine writes its run-end temperature line
|
||||
self.report_off = True # the engine reports the off gate
|
||||
self.trips = True # the engine trips a low ceiling
|
||||
self.sessions = 0 # run sessions the engine saw (M8 with the phase not run)
|
||||
@@ -279,6 +281,10 @@ class GateOffTests(unittest.TestCase):
|
||||
def end():
|
||||
time.sleep(0.3)
|
||||
cool["phase"] = "smoke"
|
||||
if self.temps_line:
|
||||
self.fc.state["logs_tail"]["text"] += (
|
||||
"Aug 22 12:00:30 forgectrl: cool: temps this job: chassis 29.0..29.4 C, "
|
||||
"supply raw 587..592\n")
|
||||
time.sleep(0.2)
|
||||
cool["phase"] = "idle"
|
||||
threading.Thread(target=end, daemon=True).start()
|
||||
@@ -367,6 +373,12 @@ class GateOffTests(unittest.TestCase):
|
||||
self.assertIn("run-start log line", str(cm.exception))
|
||||
self.assertEqual(self.fc.state["settings"]["cool_temp_max"], "")
|
||||
|
||||
def test_a_missing_run_end_temperature_line_fails(self):
|
||||
self.temps_line = False
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test()
|
||||
self.assertIn("board-temperature line", str(cm.exception))
|
||||
|
||||
def test_a_custom_ceiling_is_restored_verbatim(self):
|
||||
self.fc.state["settings"].update({"cool_temp_max": "30", "cool_temp_resume": "28"})
|
||||
self._describe()
|
||||
|
||||
@@ -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 = "a1875a8113b45c42e252caa3e4b409c1ba4b39fe"
|
||||
SRCREV = "76115fd87d0b085cd087a59ae0d58213007c42bf"
|
||||
PV = "0.1.0"
|
||||
|
||||
@@ -40,6 +40,8 @@ page's takeover does that; from a host, stop them first.
|
||||
| `flow_characterize.py` | Coolant flow characterization using the factory temperature curve (board or host; forgectrl and controller stopped): baseline → flow → no-flow → recovery, printing the ΔT bands and their separation. Takes the heater duty as an argument (`flow_characterize.py 30`); aborts if downstream passes 45 °C. |
|
||||
| `flow_matrix.py` | **The flow-detection design matrix** (board or host; forgectrl and controller stopped; with `flow_sampler.py` from `/usr/share/forgetest/bench/`): duty × duration × flow/no-flow, every run from a common cooled baseline, interleaved repeats. One heating trace yields the metric at every candidate duration, so cost and precision come from the same 60 runs. Prints a cost table, a precision table (mean±sd, worst-case margin, d′) and a ranked shortlist. `flow_matrix.py [duties] [repeats]` (or env `FM_DUTIES`, `FM_REPEATS`, `FM_RESULTS`); results/log in the bench data directory, resumable. |
|
||||
| `flow_sustained.py` | Long-run test of the real re-check cadence via M8 (board or host; controller running): counts verdicts/false faults against the configured `cool_flow_rise` and tracks whether the loop accumulates heat. `flow_sustained.py [minutes]`. |
|
||||
| `temp_calibrate.py` (`supply-*` modes) | The power supply's sensor (`pic/pwr_temp`, raw) against a thermometer on its heatsink: `supply-watch`, `supply-point <C>`, `supply-fit`; the fit is printed beside `UAPI.md`'s unverified guess. Three points during a long cut settle it. |
|
||||
| `critical_tier_drill.py` | The coolant critical tier on a rising temperature (board or host): sets the ceiling, the resume gate and the critical line a few tenths above the live upstream reading and lets the engine's own flow-check heater warm the loop through them inside one `M8` session, expecting `OVERTEMP` at the ceiling and then `CRITICAL` (fire blocked, hold, no resume) with the fault ending at `M9`; restores the settings and cycles a session so the engine re-reads them. Results as JSON in the bench data directory. |
|
||||
| `flow_warm_validate.py` | Runs the real check from a heater-warmed baseline (board or host; forgectrl and controller stopped; `flow_warm_validate.py [cycles_per_case]`; results/log in the bench data directory; exit 1 if any run is misclassified). Note the ceiling: 100 % duty pushes the downstream sensor past 50 °C in 30 s while the bulk barely moves, so warm-loop validation above ~23 °C needs the laser, not the heater. |
|
||||
| `flow_recheck_char.py` | Characterizes short in-run re-checks and the differential metric (board or host; forgectrl and controller stopped; `flow_recheck_char.py [heater_pct] [window_s]`); shows why over-temp cannot see a stopped pump and why passive warming trends are ambiguous. |
|
||||
| `flow_confirm_drill.py` | Coolant flow suspicion/confirmation drill (runs on the board): one continuous M8 session walks the verdict state machine through real pump-off transients — verified → SUSPECT (+ immediate re-check) → cleared → SUSPECT → FAULT (consecutive) → recovered — printing PASS/FAIL per transition. Leaves the machine idle (M9, pump on, heater off). |
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Coolant critical-tier drill: the loop warmed through the lines by the
|
||||
engine itself, inside one run session.
|
||||
|
||||
The loop heater reaches the high twenties at most, so the drill sets the
|
||||
coolant ceiling, the resume gate and the critical line a few tenths above
|
||||
the live upstream reading and lets the engine's own flow-check heater
|
||||
(duty 100 percent, 300 s windows, rechecks every 30 s, the suspect
|
||||
threshold parked at its top so the warm-up is not read as a flow fault)
|
||||
carry the coolant through them during a bare M8 session. Expected, in
|
||||
order: OVERTEMP at the ceiling (the pause tier), CRITICAL at the critical
|
||||
line (the fail tier: fire blocked, hold, no resume), the fault ending with
|
||||
the session and the ceiling's hold standing after it. The settings are
|
||||
restored at the end and a short session makes the engine re-read them.
|
||||
|
||||
No laser, no motion, nothing armed. Runs on the board (the bench page) or
|
||||
from a LAN host (gfbench: GF_HOST). Results as JSON in the bench data
|
||||
directory.
|
||||
|
||||
Usage: critical_tier_drill.py [--max-seconds N] (default 1200)
|
||||
"""
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
from gfbench import HOST, data_path, forgectrl_get, forgectrl_post
|
||||
|
||||
KEYS = ("cool_temp_max", "cool_temp_resume", "cool_temp_critical_c",
|
||||
"cool_flow_heater_pct", "cool_flow_check_s", "cool_recheck_s", "cool_flow_rise")
|
||||
ABOVE = {"cool_temp_max": 0.4, "cool_temp_resume": 0.2, "cool_temp_critical_c": 0.7}
|
||||
HEAT = {"cool_flow_heater_pct": "100", "cool_flow_check_s": "300", "cool_recheck_s": "30",
|
||||
"cool_flow_rise": "40"}
|
||||
|
||||
|
||||
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 cool():
|
||||
st, c = forgectrl_get("/cool/status")
|
||||
if st != 200 or not isinstance(c, dict):
|
||||
raise RuntimeError("/cool/status -> %s %r" % (st, c))
|
||||
return c
|
||||
|
||||
|
||||
def row(c, t):
|
||||
return "%5.0f s %-6s %-8s fire_ok=%-5s hold=%-5s up %.2f down %.2f | %s" % (
|
||||
t, c["phase"], c["verdict"], c["fire_ok"], c["hold"], c["up_c"], c["down_c"], c.get("reason", ""))
|
||||
|
||||
|
||||
def session_ended(wait=60):
|
||||
for _ in range(wait):
|
||||
time.sleep(1)
|
||||
c = cool()
|
||||
if c["phase"] != "run":
|
||||
return c
|
||||
return cool()
|
||||
|
||||
|
||||
def main(argv):
|
||||
max_s = 1200
|
||||
args = argv[1:]
|
||||
while args:
|
||||
a = args.pop(0)
|
||||
if a == "--max-seconds":
|
||||
max_s = int(args.pop(0))
|
||||
else:
|
||||
print(__doc__)
|
||||
return 2
|
||||
st, before = forgectrl_get("/settings")
|
||||
if st != 200:
|
||||
print("GET /settings -> %s" % st)
|
||||
return 2
|
||||
orig = {k: before.get(k, "") for k in KEYS}
|
||||
print("original settings: %s" % orig)
|
||||
c = cool()
|
||||
if c["phase"] == "run":
|
||||
print("a run session is already open; nothing done")
|
||||
return 2
|
||||
t0_up = c["up_c"]
|
||||
lines = {k: "%.1f" % (t0_up + d) for k, d in ABOVE.items()}
|
||||
lines.update(HEAT)
|
||||
print("upstream %.2f C; drill settings: %s" % (t0_up, lines))
|
||||
st, body = forgectrl_post("/settings", params=lines)
|
||||
if st != 200:
|
||||
print("POST /settings -> %s %r" % (st, body))
|
||||
return 2
|
||||
result = {"started": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "upstream_start_c": t0_up,
|
||||
"settings": lines, "transitions": [], "critical_s": None}
|
||||
g = None
|
||||
rc = 1
|
||||
try:
|
||||
g = Grbl()
|
||||
print("M8 -> %s" % g.cmd("M8"))
|
||||
t0 = time.time()
|
||||
last = None
|
||||
crit_at = None
|
||||
while time.time() - t0 < max_s:
|
||||
time.sleep(1)
|
||||
c = cool()
|
||||
t = time.time() - t0
|
||||
key = (c["verdict"], c["fire_ok"], c["hold"])
|
||||
if key != last or int(t) % 15 == 0:
|
||||
print(row(c, t))
|
||||
if key != last:
|
||||
result["transitions"].append({"t_s": round(t), "verdict": c["verdict"], "up_c": c["up_c"],
|
||||
"down_c": c["down_c"], "reason": c.get("reason", "")})
|
||||
last = key
|
||||
if c["verdict"] == "CRITICAL" and crit_at is None:
|
||||
crit_at = t
|
||||
result["critical_s"] = round(t)
|
||||
if crit_at is not None and t - crit_at >= 10:
|
||||
break
|
||||
if crit_at is None:
|
||||
print("no CRITICAL within %d s (upstream %.2f C)" % (max_s, c["up_c"]))
|
||||
print("M9 -> %s" % g.cmd("M9"))
|
||||
c = session_ended()
|
||||
print("after the session: %s" % row(c, time.time() - t0))
|
||||
result["after_session"] = {k: c.get(k) for k in ("phase", "verdict", "fire_ok", "hold", "reason", "up_c")}
|
||||
rc = 0 if crit_at is not None else 1
|
||||
finally:
|
||||
if g:
|
||||
try:
|
||||
g.cmd("M9")
|
||||
except OSError:
|
||||
pass
|
||||
g.close()
|
||||
st, body = forgectrl_post("/settings", params=orig)
|
||||
print("restore POST /settings -> %s" % st)
|
||||
result["restored"] = st == 200
|
||||
try:
|
||||
session_ended()
|
||||
g2 = Grbl()
|
||||
g2.cmd("M8")
|
||||
time.sleep(3)
|
||||
c = cool()
|
||||
print("re-read session: %s %s limits %s" % (c["phase"], c["verdict"], c.get("limits")))
|
||||
g2.cmd("M9")
|
||||
g2.close()
|
||||
c = session_ended()
|
||||
print("final: %s" % row(c, 0))
|
||||
result["final"] = {k: c.get(k) for k in ("phase", "verdict", "fire_ok", "hold", "up_c")}
|
||||
except OSError as e:
|
||||
print("re-read session failed: %s" % e)
|
||||
out = data_path("critical_tier_drill_%s.json" % time.strftime("%Y%m%d-%H%M%S"))
|
||||
with open(out, "w") as f:
|
||||
json.dump(result, f, indent=1)
|
||||
print("wrote %s" % out)
|
||||
print("RESULT: %s" % ("PASS" if rc == 0 else "FAIL"))
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -14,6 +14,14 @@ Usage:
|
||||
temp_calibrate.py point <measured_C> [note] record a calibration point
|
||||
temp_calibrate.py fit fit and print the calibration
|
||||
|
||||
temp_calibrate.py supply-watch [seconds] the same for the power supply
|
||||
temp_calibrate.py supply-point <C> [note] sensor (pic/pwr_temp, raw):
|
||||
temp_calibrate.py supply-fit thermometer on the heatsink vs
|
||||
the raw count; UAPI.md's guess
|
||||
raw * 0.08715 - 21 is shown
|
||||
beside it. Points go to
|
||||
supply_calibration.json.
|
||||
|
||||
Points accumulate in temp_calibration.json in the bench data directory
|
||||
(gfbench.data_path: next to this script, or FORGETEST_BENCH_DATA). Take
|
||||
at least two points as far apart in temperature as practical (e.g. cold
|
||||
@@ -28,6 +36,22 @@ import time
|
||||
from gfbench import board, degc, data_path
|
||||
|
||||
STORE = data_path('temp_calibration.json')
|
||||
SUPPLY_STORE = data_path('supply_calibration.json')
|
||||
|
||||
|
||||
def supply_guess_c(raw):
|
||||
"""UAPI.md's unverified guess for pic/pwr_temp."""
|
||||
return raw * 0.08715 - 21
|
||||
|
||||
|
||||
def supply_raw(samples=5, delay=1.0):
|
||||
acc, n = 0, 0
|
||||
for _ in range(samples):
|
||||
out = board('cat /sys/glowforge/pic/pwr_temp').strip()
|
||||
if out.isdigit():
|
||||
acc += int(out); n += 1
|
||||
time.sleep(delay)
|
||||
return acc / n if n else None
|
||||
|
||||
|
||||
def uapi_c(raw):
|
||||
@@ -72,8 +96,71 @@ def fit(points, key):
|
||||
return slope, my - slope * mx
|
||||
|
||||
|
||||
def supply_main(mode):
|
||||
if mode == 'supply-watch':
|
||||
seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 60.0
|
||||
print('pwr_temp raw guess-C (%.0f s)' % seconds)
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < seconds:
|
||||
r = supply_raw(1, 0)
|
||||
if r is None:
|
||||
print(' (no reading)')
|
||||
else:
|
||||
print(' %6.1f %.2f' % (r, supply_guess_c(r)), flush=True)
|
||||
time.sleep(2)
|
||||
return 0
|
||||
if mode == 'supply-point':
|
||||
try:
|
||||
measured = float(sys.argv[2])
|
||||
except (IndexError, ValueError):
|
||||
print('supply-point needs the thermometer reading in C (value)')
|
||||
return 2
|
||||
note = sys.argv[3] if len(sys.argv) > 3 else ''
|
||||
print('sampling pwr_temp (5 s)...', flush=True)
|
||||
r = supply_raw()
|
||||
if r is None:
|
||||
print('no reading from the machine')
|
||||
return 1
|
||||
data = {'points': []}
|
||||
if os.path.exists(SUPPLY_STORE):
|
||||
with open(SUPPLY_STORE) as f:
|
||||
data = json.load(f)
|
||||
data['points'].append({'measured_c': measured, 'raw': r, 'note': note,
|
||||
'when': time.strftime('%Y-%m-%d %H:%M:%S')})
|
||||
with open(SUPPLY_STORE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
print('recorded: measured %.2f C raw=%.1f (guess %.1f C) (%d points total in %s)'
|
||||
% (measured, r, supply_guess_c(r), len(data['points']), SUPPLY_STORE))
|
||||
return 0
|
||||
if mode == 'supply-fit':
|
||||
if not os.path.exists(SUPPLY_STORE):
|
||||
print('no points yet')
|
||||
return 1
|
||||
with open(SUPPLY_STORE) as f:
|
||||
pts = json.load(f)['points']
|
||||
if len(pts) < 2:
|
||||
print('need at least 2 points (have %d)' % len(pts))
|
||||
return 1
|
||||
print('points:')
|
||||
for p in pts:
|
||||
print(' %6.2f C raw=%.1f guess %.1f C %s %s'
|
||||
% (p['measured_c'], p['raw'], supply_guess_c(p['raw']), p['when'], p['note']))
|
||||
slope, offset = fit(pts, 'raw')
|
||||
if slope is None:
|
||||
print('points share one raw value; no fit')
|
||||
return 1
|
||||
print('fit: degC = %.5f * raw + %.2f (UAPI.md guess: 0.08715 * raw - 21)' % (slope, offset))
|
||||
worst = max(abs(p['measured_c'] - supply_guess_c(p['raw'])) for p in pts)
|
||||
print('the guess is off by at most %.1f C at these points' % worst)
|
||||
return 0
|
||||
print(__doc__)
|
||||
return 2
|
||||
|
||||
|
||||
def main():
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else 'watch'
|
||||
if mode.startswith('supply-'):
|
||||
return supply_main(mode)
|
||||
|
||||
if mode == 'watch':
|
||||
seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 60.0
|
||||
|
||||
Reference in New Issue
Block a user