Air-quality station (SEN55 + OLED)
A desk-ready monitor: the ESP32 polls a Sensirion SEN55 over I2C and renders live PM2.5, humidity, and temperature to the OLED once a second. Press Run, then ▶ Run script in the serial monitor, and the display updates like the real appliance.

How it works
A Sensirion SEN55 read over I²C with its real command set: the script starts measurement by writing 0x0021 to address 0x69, then polls command 0x03C4 and unpacks the 24-byte reply into PM2.5 (scaled ÷10), humidity (÷100), and temperature (÷200), exactly like production firmware. The SEN55 sits on the 5 V rail because its fan is a real load; its data pins share the C3's I²C bus (SDA=G8, SCL=G9) with the SSD1306 that renders the station readout once a second.
What's on the bench
- Battery
- ESP32-C3
- OLED 128×64
- SEN55
How it's wired
- hole a10→hole B-5
- hole a11→hole B+5
- hole a30→hole B+9
- hole a31→hole B-9
- hole a32→hole a20
- hole a33→hole f11
- hole a34→hole B-9
- hole a40→hole B-9
- hole a41→hole f10
- hole a42→hole g11
- hole a43→hole b20
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), freq=100000) 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) # SEN55: CRC-8 (initial 0xff, polynomial 0x31), 20 ms command delay def read_words(command, count): i.writeto(0x69, command) time.sleep_ms(20) d = i.readfrom(0x69, count * 3) words = [] for k in range(count): a, b, checksum = d[k*3:k*3+3] crc = 0xff for value in (a, b): crc ^= value for bit in range(8): crc = ((crc << 1) ^ (0x31 if crc & 0x80 else 0)) & 0xff if crc != checksum: raise ValueError('SEN55 checksum mismatch') words.append((a << 8) | b) return words def signed(value): return value - 65536 if value & 0x8000 else value i.writeto(0x69, b'\x00\x21') time.sleep_ms(50) while True: if not read_words(b'\x02\x02', 1)[0]: time.sleep_ms(200) continue w = read_words(b'\x03\xc4', 8) if w[1] == 0xffff or w[4] in (0x7fff, 0xffff) or w[5] in (0x7fff, 0xffff): time.sleep(1) continue pm, rh, temp = w[1]/10, signed(w[4])/100, signed(w[5])/200 fb.fill(0) fb.text('AIR QUALITY', 20, 2) fb.text('PM2.5 %.1f ug' % pm, 0, 20) fb.text('RH %.1f %%' % rh, 0, 34) fb.text('TEMP %.1f C' % temp, 0, 48) show() print('PM2.5', pm, 'RH', rh, 'T', temp) time.sleep(1)
Try this
- Select the SEN55 and drag its PM2.5 slider: the OLED and the serial line follow.
- Two devices, one bus: run I2C(0, scl=Pin(9), sda=Pin(8)).scan() at the REPL and find 0x3c and 0x69.
- Compare with the fume-extractor build, which turns these same readings into fan control.
- Hover the SEN55 supply pin: 5 V, not 3V3 — the fan needs it.



