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.

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 · pos→Buck Conv · vin
- Buck Conv · vin→120mm Fan · v12
- Buck Conv · vout→ESP32-C3 · vin
- ESP32-C3 · vin→SEN55 · vdd
- ESP32-C3 · 3v3→OLED 128×64 · vcc
- ESP32-C3 · 3v3→Button · b
- DC Jack · neg→Buck Conv · gnd
- Buck Conv · gnd→Buck Conv · gnd2
- Buck Conv · gnd2→ESP32-C3 · gnd
- ESP32-C3 · gnd→120mm Fan · gnd
- 120mm Fan · gnd→SEN55 · gnd
- SEN55 · gnd→SEN55 · sel
- SEN55 · gnd→OLED 128×64 · gnd
- OLED 128×64 · gnd→Resistor · b
- ESP32-C3 · g8→SEN55 · sda
- SEN55 · sda→OLED 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.



