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.

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)