Breadboard
docs
Example project · Builds & projects

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 %. The code is a real multi-file project — main.py imports the display and sensor from drivers/ modules; explore the file tabs in the Code panel.

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

How it works

The complete appliance on one bench: a 12 V barrel jack feeds a buck converter that powers an ESP32-C3 and a 120 mm PWM fan. A Sensirion SEN55 measures the air (PM2.5, VOC) over I²C and an MQ-2 gas sensor sniffs for smoke on the ADC. Real MicroPython reads the live PM2.5 and ramps the fan’s LEDC PWM duty proportionally; if the MQ-2 crosses its threshold it slams the fan to 100 % and raises a SMOKE! alarm. Every reading and the fan state render to the OLED.

What's on the bench

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

How it's wired

  • DC Jack · posBuck Conv · vin
  • Buck Conv · vin120mm Fan · v12
  • Buck Conv · voutESP32-C3 · vin
  • ESP32-C3 · vinSEN55 · vdd
  • ESP32-C3 · 3v3OLED 128×64 · vcc
  • ESP32-C3 · 3v3Button · b
  • DC Jack · negBuck Conv · gnd
  • Buck Conv · gndBuck Conv · gnd2
  • Buck Conv · gnd2ESP32-C3 · gnd
  • ESP32-C3 · gnd120mm Fan · gnd
  • 120mm Fan · gndSEN55 · gnd
  • SEN55 · gndSEN55 · sel
  • SEN55 · gndOLED 128×64 · gnd
  • OLED 128×64 · gndResistor · b
  • ESP32-C3 · g8SEN55 · sda
  • SEN55 · sdaOLED 128×64 · sda

…and 10 more connections — open it in the simulator to see every wire.

The code

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

main.py

from machine import Pin, PWM, ADC
import time
from drivers.oled import fb, show
from drivers import sen55

# 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()

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:
    pm = sen55.read_pm25()
    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)

drivers/oled.py

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)

drivers/sen55.py

# Sensirion SEN55 particulate sensor (I2C command set), sharing the bus
# the display set up in drivers/oled.py.
from drivers.oled import i
import time

def start():
    # 0x0021: start measurement; give the sensor a beat before reading.
    i.writeto(0x69, b'\x00\x21')
    time.sleep_ms(200)

def read_pm25():
    # 0x03C4: read measured values (8 big-endian words + CRC bytes).
    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)]
    return w[1] / 10

Try this

  • Watch the OLED show live PM2.5 and the fan ramping with it.
  • Drag the MQ-2’s Gas slider past the threshold — the fan jumps to 100 % with a SMOKE! alarm.
  • Open the Code tab: this is a real multi-file project. main.py holds the control loop; the display and sensor live in drivers/oled.py and drivers/sen55.py — switch tabs to explore, or add a module of your own with “+ file”.
  • Probe the fan’s PWM pin with the Probe tool and open the scope to watch the LEDC duty change.

Related projects