From 6f53e628f36d9c85a9021e9faad2131616f1391a Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sun, 2 Aug 2026 17:31:53 -0400 Subject: [PATCH] Fan/thermal control done and bench-verified; docs updated BRINGUP records the gate as complete with tach-verified results; LIGHTBURN.md documents the per-layer Air Assist toggle driving the cut-profile ventilation; fan_test.py joins the bench kit. --- docs/BRINGUP.md | 26 +++++++++-------- docs/LIGHTBURN.md | 9 ++++++ scripts/bench/README.md | 2 ++ scripts/bench/fan_test.py | 59 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 scripts/bench/fan_test.py diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 8a794bb..941a956 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -220,18 +220,20 @@ hardware I/O — host testing). mapping) plus a chain-armed first-light procedure; the hardware verification prerequisites are complete. Interlock-trip recovery behavior remains to be exercised (non-scope check). - - **Additional laser-on prerequisite (operator-mandated - 2026-08-02): fan/thermal control must be implemented first** — - air assist + purge (head), exhaust + intake (thermal), and the - coolant loop (water pump, TEC, water-temp monitoring). Factory - run/idle/cooldown values are in the captured pulse headers - (AAid/AArd/AAwd, EFid/EFrd/EFwd, IFid/IFrd/IFwd + CM/CT water - thresholds — see `_RESOURCES/*.info` and the gfutilities - settings map); kernel interfaces exist under - /sys/glowforge/head and /sys/glowforge/thermal. Map into the - driver as job-state-driven config (run at cycle start, cooldown - after, idle thereafter) like the factory's - `_config_from_pulse` scheme. + - **Fan/thermal control (operator-mandated laser-on prerequisite): + DONE 2026-08-02, bench-verified** (`glowforge_cooling.c` in the + driver; test `scripts/bench/fan_test.py`). Factory pulse-header + values throughout: init = pump on / TEC off / purge on / idle + fans (air assist 204); **M8** (coolant flood — LightBurn's + per-layer Air Assist) = cut profile (air 1023, exhaust 65535, + intake 43278); **M9** = 15 s cooldown (`GFCOOL_COOLDOWN_S`) then + idle. Water temp polled at 1 Hz vs the ~31 °C factory run + ceiling → one-shot controller warning (laser milestone upgrades + it to a hard fire gate). Verified via tach readbacks: air tach + period 4439→699 under M8, exhaust stopped→full, intakes ~3×, + cooldown hold, clean return to idle; coolant temp visibly + dropped during the blast. TEC/heater control remain for the + thermal part of the laser milestone. - **Interlock readback semantics cross-check: OPEN** (see factory-laser-safety-readbacks notes). 3. **Homing (design decided 2026-08-02)**: the factory machine has NO diff --git a/docs/LIGHTBURN.md b/docs/LIGHTBURN.md index 6c5459c..d223efe 100644 --- a/docs/LIGHTBURN.md +++ b/docs/LIGHTBURN.md @@ -65,6 +65,15 @@ re-parked.) - **Console tab**: raw grbl — `?` status, `$$` settings, `$X` unlock, `$J=G91X10F1200` jog. +## Air assist / fans + +Each cut/engrave layer has an **Air Assist** toggle (in the layer's cut +settings). Turning it on makes LightBurn emit M8/M9 around that layer, +which drives the machine's full cut-profile ventilation: air assist to +full, exhaust and intake fans to factory run speeds, then a ~15 s +cooldown after the layer before returning to idle. Leave it ON for +anything that will eventually involve the beam; expect real fan noise. + ## A good first job 1. Draw a rectangle (~100 × 60 mm) with a circle inside. diff --git a/scripts/bench/README.md b/scripts/bench/README.md index f3840d0..58df8e5 100644 --- a/scripts/bench/README.md +++ b/scripts/bench/README.md @@ -10,6 +10,8 @@ target board (dev image, python3 present) unless noted. | `check_pwm.py` | Laser PWM register check (audit M8): reads PWM2 PWMCR/PWMPR via /dev/mem, expects divider 13 × ~127 counts ≈ 40 kHz. | | `pwm_sweep.py` | LASER_PWM scope test (runs on the board): `check` = read-only safety readbacks + PWM2 dump; `sweep` = steps PWMSAR through 50/25/75/6/100 % duty with 4 s holds, then restores. Run only in the locked state (controller stopped, cnc disabled, latch locked). Waveform gate PASSED with it 2026-08-02: 40 kHz stable, duty tracks PWMSAR (6.4 % measured vs 6.3 % commanded at the low end). | | `pwm_hold.py` | Holds one PWMSAR value for a scope-measurement window (`pwm_hold.py `), then restores. Same locked-state rule. | +| `fire_test.py` | FIRE drop-timing scope test (runs on the board): A = latch locked (expects nothing on FIRE/LASER_ON), B = latch unlocked / normal end-of-data, U = true underrun. Duty 0 throughout; refuses to unlock if HV reports good. All gates PASSED with it 2026-08-02 (2.0000 s pulses exact, both paths). | +| `fan_test.py` | Fan/coolant bench (Windows-side): snapshots fan PWMs/tachs/temps, drives M8 → cut fans, M9 → cooldown → idle, verifying via tach readbacks. All-green 2026-08-02. | | `build-glowforge.sh` | Cross-compiles **grblHAL-glowforge** (the canonical driver repo, `../../../grblHAL-glowforge`) in the forge-yocto WSL distro. Run: `wsl -d forge-yocto -- bash /build-glowforge.sh` (from PowerShell; Git Bash mangles /mnt/c paths). This is the production controller build. | | `build-feeder.sh` | Cross-compiles `feeder.c` the same way. | | `puls_profile.py` | Decodes factory `.puls` streams (raw or GF1-headered) into velocity/accel profiles: peak speeds, ramp-slope fits, per-move segments, Z cadence. Runs anywhere (stdlib only). Source of the factory-true grblHAL defaults (milestone 2): 700/590 mm/s² accel, 200 mm/s max rate, 28160 Hz travel tick. | diff --git a/scripts/bench/fan_test.py b/scripts/bench/fan_test.py new file mode 100644 index 0000000..3cee90c --- /dev/null +++ b/scripts/bench/fan_test.py @@ -0,0 +1,59 @@ +import socket, subprocess, time + +HOST = '172.16.1.97' + +def board(cmd): + r = subprocess.run(['wsl', '-d', 'forge-yocto', '--', 'ssh', + '-o', 'PreferredAuthentications=none', + 'root@' + HOST, cmd], + capture_output=True, text=True, timeout=30) + return r.stdout.strip().replace('\n', ' ') + +ATTRS = ('head/air_assist_pwm head/air_assist_tach head/purge_air ' + 'thermal/exhaust_pwm thermal/tach_exhaust ' + 'thermal/intake_pwm thermal/tach_intake_1 thermal/tach_intake_2 ' + 'thermal/water_pump_on thermal/tec_on pic/water_temp_1 pic/water_temp_2') + +def snap(tag): + vals = board('cd /sys/glowforge && cat ' + ATTRS).split() + keys = [a.split('/')[-1] for a in ATTRS.split()] + print(tag) + for k, v in zip(keys, vals): + print(' %-18s %s' % (k, v)) + +s = socket.create_connection((HOST, 23), timeout=5) +s.settimeout(0.15) +def drain(): + out = b'' + try: + while True: + d = s.recv(4096) + if not d: break + out += d + except socket.timeout: pass + return out.decode('ascii', 'replace') +def cmd(l, w=0.4): + s.sendall(l.encode() + b'\n'); time.sleep(w); return drain().strip() + +time.sleep(0.5); drain() + +snap('--- baseline (driver idle profile)') +w2 = board('cat /sys/glowforge/pic/water_temp_2') +print('water_temp_2 = %s -> %.1f C' % (w2, int(w2) * -0.09653 + 94)) + +print() +print('=== M8: cut-profile fans ON (loud) ===') +print('M8:', cmd('M8')) +time.sleep(4) # spin-up +snap('--- during M8') + +print() +print('=== M9: cooldown (fans stay on ~15 s) ===') +print('M9:', cmd('M9')) +time.sleep(3) +snap('--- during cooldown (should still be at run values)') +time.sleep(14) # past the 15 s cooldown +snap('--- after cooldown (should be idle: AA 204, EF 0, IF 0)') +s.sendall(b'?'); time.sleep(0.3) +t = drain() +print('grbl:', t[t.rfind('<'):t.rfind('>') + 1] if '<' in t else t)