mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 09:11:11 -07:00
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.
This commit is contained in:
+4
-3
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 <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. |
|
||||
| `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. |
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user