Breadboard
docs
Example project · ESP32

3D-printer fume extractor (ESP32, full build)

The complete appliance on one bench: a 12 V barrel jack → buck converter powers an ESP32-C3 and a 120 mm PWM fan, while a Sensirion SEN55 measures the air and an MQ-2 sniffs for smoke. Real MicroPython ramps the fan from the live PM2.5, and the MQ-2 alarm (drag its Gas slider past the threshold) slams the fan to 100 % in SMOKE! mode, after the sensor’s genuine 5 s heater warm-up. Hold the boost button for manual 100 %; everything editable in the Code tab.

The 3D-printer fume extractor (ESP32, full build) circuit as rendered by the simulator

What's on the bench

  • 120mm Fan
  • Buck Conv
  • Button
  • DC Jack
  • ESP32-C3
  • MQ-2 Gas Sensor
  • OLED 128×64
  • Resistor
  • SEN55

The code

This MicroPython script runs on the emulated board every boot; edit it in the Code tab.

from machine import I2C, Pin
import framebuf, time
i = I2C(0, scl=Pin(9), sda=Pin(8))
buf = bytearray(1024)
fb = framebuf.FrameBuffer(buf, 128, 64, framebuf.MONO_VLSB)
for c in b'\xae\xd5\x80\xa8\x3f\xd3\x00\x40\x8d\x14\x20\x00\xa1\xc8\xda\x12\x81\xcf\xd9\xf1\xdb\x40\xa4\xa6\xaf':
    i.writeto(0x3c, bytes([0, c]))
def show():
    i.writeto(0x3c, b'\x00\x21\x00\x7f\x22\x00\x07')
    i.writeto(0x3c, b'\x40' + buf)

from machine import PWM, ADC

# 120 mm fan on LEDC PWM (G4); boost button on G5 (external pull-down).
fan = PWM(Pin(4), freq=1000)
boost = Pin(5, Pin.IN)
# MQ-2 smoke sensor: analog level on G1 (ADC), active-low alarm on G7.
smoke_adc = ADC(Pin(1), atten=ADC.ATTN_11DB)
smoke_alarm = Pin(7, Pin.IN)
FAN_MAX = 1500

# SEN55: start measurement (Sensirion I2C command set).
i.writeto(0x69, b'\x00\x21')
time.sleep_ms(200)

def duty_for(pm, boosted, alarm):
    if alarm or boosted:
        return 100
    if pm < 5:
        return 20            # idle purge
    if pm > 50:
        return 100
    return int(20 + (pm - 5) * 80 / 45)

while True:
    i.writeto(0x69, b'\x03\xc4')
    d = i.readfrom(0x69, 24)
    w = [(d[k*3] << 8) | d[k*3+1] for k in range(8)]
    pm = w[1] / 10
    sv = smoke_adc.read_u16() * 3.3 / 65535
    gas = max(0, min(100, int((sv - 0.4) * 100 / 3.35)))
    alarm = smoke_alarm.value() == 0    # LM393 pulls low on gas
    boosted = boost.value() == 1
    pct = duty_for(pm, boosted, alarm)
    fan.duty_u16(pct * 65535 // 100)
    mode = "SMOKE!" if alarm else ("BOOST" if boosted else "AUTO")
    fb.fill(0)
    fb.text('FUME EXTRACTOR', 0, 0)
    fb.hline(0, 10, 128, 1)
    fb.text('PM2.5 %5.1f ug' % pm, 0, 14)
    fb.text('GAS   %3d %%' % gas, 0, 25)
    fb.text('FAN %3d%%' % pct, 0, 37)
    fb.rect(66, 36, 60, 9, 1)
    fb.fill_rect(66, 36, 60 * pct // 100, 9, 1)
    fb.text('MODE: ' + mode, 0, 50)
    show()
    print('PM2.5', pm, 'ug  gas', gas, '%  fan', pct, '%  ', mode)
    time.sleep_ms(500)