From e32c86fb108b074b7349de2ac897a54d96fd7c13 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sat, 29 Aug 2026 18:02:46 -0400 Subject: [PATCH] Pin forgectrl 42cdb71; add the coolant offset check; record the compensation on the bench The pin carries the calibrate tool's sampling fix (3 s at 8 Hz a side). aa_offset_check.py proves the correction on a machine: M8 brings the fans to the run profile, dark, while the raw coolant counts, /status and the engine's readings are averaged before, during and after; it turns the flow check off for its session and restores it. On the bench page as aa-offset-check. CAMPAIGN-LOG records the tool's first run, the applied value (16 counts) and the proof: the readings held within 0.1 C under the run profile where they dropped 1.0 C uncorrected. BRINGUP item 21 names what remains. No catalog consequence: a bench tool and records. --- docs/BRINGUP.md | 7 +- docs/CAMPAIGN-LOG.md | 32 +++++ forgetest/forgetest/bench.py | 7 ++ .../forgectrl/forgectrl-pin.inc | 2 +- scripts/bench/README.md | 1 + scripts/bench/aa_offset_check.py | 110 ++++++++++++++++++ 6 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 scripts/bench/aa_offset_check.py diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 94b166d..2af08da 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -1476,9 +1476,10 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`. gates read the coolant about 1.2 C cooler than it is while the air assist runs unless `cool_aa_offset_counts` carries the machine's value (the `aa-offset-calibrate` diagnostic measures it, the panel's Apply - writes it; zero is the factory's uncorrected reading). Owed: the - calibrate run on this machine and its value applied, a second - machine's value when one is on the bench; and the mid-run + writes it; zero is the factory's uncorrected reading; the bench + machine carries 16). Owed: the calibrate tool's recommendation + accepted on an image (its first sampling refused its own result), a + second machine's value when one is on the bench; and the mid-run toggling between two levels (0.6 to 1.1 C, both sensors together), which comes only with the tube lit: not with the fans alone, not under motion, not in an armed dark window. The HV supply's input current on diff --git a/docs/CAMPAIGN-LOG.md b/docs/CAMPAIGN-LOG.md index 4e0621d..e1aa0b0 100644 --- a/docs/CAMPAIGN-LOG.md +++ b/docs/CAMPAIGN-LOG.md @@ -4492,6 +4492,38 @@ two sensor lines during a cut is the next instrument. Its size is 0.6 to 1.1 C either way, inside the over-temperature ceiling's 2 C hysteresis, and the flow check reads means. +## 2026-08-29: the air-assist offset taken off the coolant readings, on the bench + +forgectrl `cool_aa_offset_counts` (f9b4893, the status link fix 25cf969), +image 20260829214735 flashed by the operator. The correction is in ADC +counts, keyed to the air-assist duty the engine commands, taken off both +raw readings in the engine and in `/status` (more counts read colder, so +the fan's ground lift reads as a drop and the correction subtracts; the +host test caught the first cut adding it). + +**The calibrate tool** (`aa-offset-calibrate`, the panel's "Calibrate +coolant offset") ran end to end on the bench: three idle-to-run cycles, +six edges reading 12.7/18.3, -15.8/-18.5, 16.3/16.8, -12.5/-10.3, +15.7/17.8, -15.7/-21.0 counts (down/up), mean 16.0. It refused its own +result on the spread (10.7 counts against its 8-count limit): 1.5 s at +4 Hz is six samples a side against about 5 counts of single-sample noise. +The tool now reads 3 s at 8 Hz a side (forgectrl 42cdb71, the next +image); the value was applied directly, `cool_aa_offset_counts = 16`, this +machine's number. + +**The proof** (`scripts/bench/aa_offset_check.py`, dark, no press: M8 +brings the fans to the run profile, the raw counts, `/status` and the +engine's readings averaged before, during and after). Uncorrected, the +upstream reading dropped 1.02 C under the run profile (raw +15.2 counts). +Corrected, with the flow check off for the session so its heater stayed +out of the downstream sensor: the raw counts stepped +15.4 / +13.3 and +`/status` read 23.96 / 23.94 against 23.87 / 23.96 before, +0.09 and +-0.02 C; the engine's own readings +0.31 / +0.25 inside the same window. +The readings hold still while the fan runs. A first run of the check had +left `cool_flow_check_s` at 0 (the script's restore posted an empty value +and got a 400; fixed); the setting was put back to 50 and the engine +re-read it at the next session. + ## Superseded status notes ### Shared machine services — remaining polish, as listed 2026-08-13 diff --git a/forgetest/forgetest/bench.py b/forgetest/forgetest/bench.py index 3f3dffd..8cda8d8 100644 --- a/forgetest/forgetest/bench.py +++ b/forgetest/forgetest/bench.py @@ -138,6 +138,13 @@ TOOLS = [ "with both sensors at 25 Hz, and scores the common-mode step at every edge and the level " "toggling inside every dwell; every value is restored on exit. The pump stops once for " "8 s with the tube dark and the heater off. About five minutes."}, + {"id": "aa-offset-check", "title": "Coolant offset correction under the run airflow", "script": "aa_offset_check.py", + "safety": "dry", "where": "board", "ported": True, + "args": [_arg("dwell", "float", 20.0, "seconds to read under the run profile")], + "desc": "M8 brings the fans to the run profile (no laser-on, no press) while the raw coolant " + "counts, /status and the engine's readings are averaged before, during and after: with " + "cool_aa_offset_counts at the machine's value the readings hold still while the raw counts " + "step; at zero they drop by about a degree. About a minute."}, {"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")], diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc index 4686009..685ca88 100644 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc +++ b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc @@ -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 = "25cf969cf149bc539ff90f875fcc7edaa6c52ae2" +SRCREV = "42cdb717b9fe30ed0e051636fee6fe5ea4d68299" PV = "0.1.0" diff --git a/scripts/bench/README.md b/scripts/bench/README.md index 56ccb3d..301fb8e 100644 --- a/scripts/bench/README.md +++ b/scripts/bench/README.md @@ -42,6 +42,7 @@ page's takeover does that; from a host, stop them first. | `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 `, `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. | +| `aa_offset_check.py` | Coolant offset correction under the run airflow (board; controller running; dark, no press): M8 brings the fans to the run profile while the raw coolant counts, `/status` and the engine's readings are averaged before, during and after; with `cool_aa_offset_counts` at the machine's value the readings hold still while the raw counts step, at zero they drop by about a degree. `aa_offset_check.py [dwell_s]`. | | `offset_probe.py` | Coolant-sensor offset probe (board; forgectrl idle; dark, no press): switches one actuator at a time (exhaust at 100/50/25 %, intakes, air assist, purge, heater, pump, TEC, lid lamp, then all run fans) with both thermistors sampled at 25 Hz and scores the common-mode step at every edge and the level toggling inside every dwell; `offset_probe.py ladder` runs the air-assist duty ladder alone. Every value is restored on exit. JSON record 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. | diff --git a/scripts/bench/aa_offset_check.py b/scripts/bench/aa_offset_check.py new file mode 100644 index 0000000..e69afd3 --- /dev/null +++ b/scripts/bench/aa_offset_check.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Does the coolant reading hold still when the run airflow comes on? + +With cool_aa_offset_counts at the machine's value the engine takes the +air-assist fan's ground shift off both coolant readings, so /status and +/cool/status should read the same coolant before and after M8 brings the +fans to the run profile, while the raw counts step by the offset. With +the setting at zero the readings drop by about a degree instead. Dark, +no press: M8 opens a run session without a laser-on. Runs on the board +(the bench directory on PYTHONPATH for the Grbl client). + +Usage: aa_offset_check.py [dwell_s] +""" +import json +import sys +import time +import urllib.request + +from live_fire_drills import Grbl, HOST, PORT + + +def raw(): + out = [] + for a in ('pic/water_temp_1', 'pic/water_temp_2'): + with open('/sys/glowforge/' + a) as f: + out.append(int(f.read().strip())) + return out + + +def status(): + with urllib.request.urlopen('http://127.0.0.1:8080/status', timeout=2) as r: + s = json.load(r) + with urllib.request.urlopen('http://127.0.0.1:8080/cool/status', timeout=2) as r: + c = json.load(r) + with urllib.request.urlopen('http://127.0.0.1:8080/settings', timeout=2) as r: + st = json.load(r) + return (s['coolant']['down_c'], s['coolant']['up_c'], c['down_c'], c['up_c'], c['phase'], + st.get('cool_aa_offset_counts')) + + +def mean_over(secs): + rs, ss = [], [] + end = time.time() + secs + while time.time() < end: + rs.append(raw()) + ss.append(status()[:4]) + time.sleep(0.5) + n = len(rs) + return ([sum(x[i] for x in rs) / n for i in range(2)], + [sum(x[i] for x in ss) / n for i in range(4)]) + + +def post_settings(**kv): + """POST /settings with the panel token; returns the reply dict.""" + import urllib.parse + try: + with open('/data/forgefirm/panel.token') as f: + tok = f.read().strip() + except OSError: + tok = '' + req = urllib.request.Request('http://127.0.0.1:8080/settings', + data=urllib.parse.urlencode(kv).encode(), + headers={'X-ForgeFIRM-Token': tok}) + with urllib.request.urlopen(req, timeout=4) as r: + return json.load(r) + + +def main(): + dwell = float(sys.argv[1]) if len(sys.argv) > 1 else 20.0 + g = Grbl(HOST, PORT) + if 'Idle' not in g.status(): + print('REFUSED: controller not Idle') + return 2 + setting = status()[5] + print('cool_aa_offset_counts = %s' % setting) + # A run session starts the flow check, whose heater warms the + # downstream sensor inside this check's dwell: off for this session, + # back afterward. + with urllib.request.urlopen('http://127.0.0.1:8080/settings', timeout=2) as r: + was = json.load(r).get('cool_flow_check_s') + if was in (None, ''): + was = '50' # unset reads as empty: the shipped default + print('flow check off for this session (cool_flow_check_s was %s): %s' + % (was, post_settings(cool_flow_check_s='0').get('cool_flow_check_s'))) + print('before M8: settling 8 s') + r0, s0 = mean_over(8.0) + print(' raw %.1f/%.1f /status %.2f/%.2f engine %.2f/%.2f' % (r0[0], r0[1], *s0)) + print('M8: %s' % g.cmd('M8')) + time.sleep(6.0) # the fans to run duty, the offset in + r1, s1 = mean_over(dwell) + print(' raw %.1f/%.1f /status %.2f/%.2f engine %.2f/%.2f (phase %s)' + % (r1[0], r1[1], *s1, status()[4])) + print('M9: %s' % g.cmd('M9')) + if was is not None: + post_settings(cool_flow_check_s=str(was)) + else: + post_settings(cool_flow_check_s='50') + with urllib.request.urlopen('http://127.0.0.1:8080/settings', timeout=2) as r: + print('flow check restored: cool_flow_check_s = %s' % json.load(r).get('cool_flow_check_s')) + time.sleep(25.0) # the session closes, the fans idle + r2, s2 = mean_over(8.0) + print(' raw %.1f/%.1f /status %.2f/%.2f engine %.2f/%.2f' % (r2[0], r2[1], *s2)) + print('\nraw counts stepped %+.1f / %+.1f under the run profile; /status moved %+.2f / %+.2f C; ' + 'the engine moved %+.2f / %+.2f C' + % (r1[0] - r0[0], r1[1] - r0[1], s1[0] - s0[0], s1[1] - s0[1], s1[2] - s0[2], s1[3] - s0[3])) + return 0 + + +if __name__ == '__main__': + sys.exit(main())