A CO2 Node That Fails Visibly
Two CO2 nodes, one in a bedroom and one in the family room. Each pairs an Adafruit SCD30 (PID 4867, Sensirion NDIR) with a Seeed XIAO ESP32-C3 on a small carrier board, inside a Hammond 1551V4. ESPHome + Home Assistant. CERN-OHL-S v2 hardware, MIT code, CC BY 4.0 docs. Repo: github.com/wkcollis1-eng/CO2-Monitor
It is 03:00 in a closed bedroom. The SCD30's cable worked loose an hour ago. Nobody is awake to notice, and nobody is coming to fix it tonight.
A CO2 sensor that has stopped answering is dangerous in one specific way: Home Assistant keeps showing the last good number, and a flat, healthy-looking reading is exactly what a dead sensor looks like. The acceptance test is what the node reports at that moment, not whether it is online.

KiCad 3D render of the fit-check model, not a photograph - no node has been built yet. The standoff height is not final, and the model doesn't show how the USB cable leaves the box.
Status - October 2026: Draft, Not Built
- Firmware: Rev 1.0-draft, 2026-10-18.
- Compile: both nodes compile on ESPHome 2026.9.1 with 0 warnings and 0 errors.
- Watchdog: host-simulated. The clean config passes all 35 checks, and each of 5 injected faults is caught (see the harness below for what that does and doesn't prove).
- Hardware: no new PCB. The carrier is an existing ESP32-C3 sensor-node board; the KiCad file is that board unchanged, with the SCD30 and enclosure placed on it for a 3D fit check.
- Deployment: not deployed. The firmware isn't in the Home Assistant config yet.
The Three Moments The Firmware Is Built Around
- Every boot. ESPHome 2026.9.1 turns the SCD30's automatic self-calibration (ASC) on by default and writes it to the sensor at every boot. ASC assumes the sensor regularly sees fresh outdoor air. A closed room in a house with no HRV or ERV never does, so ASC would walk the baseline upward where nobody would see it. It's turned off explicitly.
- Power-up. The SCD30's first reading after power-up is 0 ppm, and ESPHome passes it through. A filter drops it. 0 ppm isn't a possible indoor value, so dropping every 0 costs nothing.
- The sensor stops answering. After 240 s with no reading, the CO2 entity goes unknown and CO2 Sensor Fault turns on. After 900 s the node reboots to re-run sensor setup, at most three times in a row. The counter is stored in flash, so a dead cable can't become a reboot loop that hides it. After the third reboot the node stays up and keeps reporting the fault.
Hardware
- Carrier: 23.0 x 38.0 mm, 2-layer, 1.6 mm
- MCU: Seeed XIAO ESP32-C3
- Sensor: Adafruit SCD30 breakout, I2C 0x61, the only device on the bus
- I2C: SDA GPIO6 (D4), SCL GPIO7 (D5), 50 kHz
- Status LED: GPIO20 (D7), kept dark - the box is opaque
- Enclosure: Hammond 1551V4, 80 x 80 x 20.3 mm ABS
PH1 - sensor header
The SCD30 is cabled from its STEMMA QT socket to PH1, a 1x04 right-angle header. No header is soldered to the SCD30.
| PH1 pad | Signal | QT wire |
|---|---|---|
| 1 | GND | black |
| 2 | +3V3 | red |
| 3 | SDA | blue |
| 4 | SCL | yellow |
Pad 2 is the square pad, not pad 1. Count from the square pad and you'll put 3V3 on the wrong wire. Place each wire by colour.
Feed the breakout from 3V3, never 5 V. Its 10K SDA/SCL pull-ups go to VIN, so VIN sets the high level the XIAO's 3.3 V pins see on the bus.
Fit in the enclosure - the open question
The 1551V4 is 15.30 mm deep inside. Both boards sit on standoffs on the enclosure floor.
- The SCD30 is the tight part. On 6 mm standoffs it clears the cover by 0.735 mm nominal. Its height tolerance is ±0.70 mm and the cover's moulded lettering stands 0.20 mm, so the worst case is a clash under the lettering. On 5 mm standoffs the worst case is 1.035 mm, or 0.835 mm under the lettering. The SCD30's standoff height is still open.
- The carrier on 6 mm standoffs leaves 0.95 mm with a XIAO soldered on pin headers. A socketed XIAO doesn't fit, and the screw under the XIAO has to go in first.
- The floor has a 1.0 mm recess at its centre (±7.53 mm). Keep standoffs off it....
Bill Collis