breadboard
Example project · Sensors

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.

The Air-quality station (SEN55 + OLED) circuit as rendered by the simulator

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.

Related projects