mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
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.
This commit is contained in:
+14
-12
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <sar> <seconds>`), 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 <path>/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. |
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user