From UART checksums to e-paper: building a desk air-quality monitor

I wanted to know what the air at my desk actually looks like: CO2 while the door is closed, particulates when it’s windy outside, that sort of thing. You can buy an air quality monitor, but the interesting ones are expensive and the cheap ones don’t show you the raw data. So I built one around a cheap multi-sensor module.

Talking to the ZPHS01B over UART

The heart of the build is the ZPHS01B, a multi-in-one air quality module that reports PM1.0, PM2.5, PM10, CO2, VOC, formaldehyde, CO, O3, NO2, temperature, and humidity over a plain UART serial line at 9600 baud. You ask for a reading by sending a fixed nine-byte frame, and the last byte is a checksum computed as the two’s complement of the payload sum:

const uint8_t zphs01b_cmd[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};

uint8_t calc_checksum(const uint8_t* data, int len) {
  uint8_t sum = 0;
  for (int i = 1; i < len - 1; ++i) sum += data[i];
  return (uint8_t)(~sum + 1);
}

The response is a 26-byte frame carrying every measurement in one go, checksummed the same way. Get the checksum wrong and the sensor simply ignores you, which is hardware’s way of returning an error without a message.

There was an existing Python interface for the sensor, but not one that worked well on a Mac with a USB-to-serial adapter, so I adapted it for macOS. The official documentation is thin, so I also keep my own protocol notes in the repository: pinout, framing, the full response layout. They’ve paid off every time I’ve come back to the project after a few months away.

The Python prototype came first

Before any firmware existed, the whole thing ran on my desk as a Python script: query the sensor once per second, decode the frame, and stream the readings into a live matplotlib dashboard with a rolling thirty-minute window, logging everything to CSV on the side. On a laptop, one iteration is “save file, run again”; on a microcontroller it’s a flash-and-reboot cycle with a serial console as your debugger. Protocol mistakes got caught in minutes instead of after another flash cycle, and by the time firmware entered the picture, any new bug had to be in the new code.

The ESP32 and e-paper build

A monitor that only exists while a laptop script runs isn’t a monitor, so the next step was an ESP32-S3 board reading the sensor continuously, serving the latest measurements over WiFi, and driving a CrowPanel 4.2 inch e-paper display.

E-paper is a great fit for ambient data that changes slowly, since it’s readable in any light and costs essentially nothing to leave on. It’s also restrictive in ways a normal screen isn’t. You don’t get a font stack or an icon set; I ended up converting TrueType fonts and Material Design icons into bitmap formats the Adafruit GFX library can render, which is its own small pipeline of tooling before the display shows a single glyph. The display side eventually became a reusable, standalone component: esphome-crowpanel-4.2-epaper, and judging by the stars and forks it has collected, I wasn’t the only one who wanted this panel working in ESPHome.

The same habits show up in client work: receipt printers, payment terminals, and scales come with the same thin documentation and the same need to verify every byte. But mostly: the CO2 line after two hours in a closed room made me open the window.

Working on something similar?

Tell me what you're trying to build. I'll let you know honestly whether I'm a good fit.

Get in touch
Start a conversation