Built-in project · Arduino Uno R3

Air quality monitor on an Arduino Uno

An SGP30 and an SHT31 on one I2C bus with a four-digit LED display: CO2 in ppm, then temperature and humidity, and a red LED above 1000 ppm. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

IntermediateRuns in your browser. Free, and no account needed.

Air quality monitor · Arduino Uno R3live0.000 s 0.00x
Click to open it in the editor
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Air quality monitor: an SGP30 gas sensor, an SHT31 thermometer and an
// HT16K33 four-digit LED display share the I2C bus, the way a home-built CO2
// monitor is made.
//
//   SGP30, SHT31, display   SDA -> A4   SCL -> A5   VIN or VCC -> 5 V   GND -> GND
//   red LED                 pin 8 through 220 ohms
//
// The SGP30 reads 400 ppm and 0 ppb for its first 15 seconds while its hot
// plate warms up, so the display counts that down after "--". Then it takes
// turns: the CO2eq in ppm for four seconds, the temperature ("23*C") and the
// humidity ("45rh") for one each. Above 1000 ppm, where a room wants a
// window opening, the red LED comes on and the display blinks.
//
// The SHT31's humidity makes the SGP30 more accurate: the sensor wants it as
// milligrams of water in a cubic meter of air, which is worked out here from
// a table of how much water air can hold, because this board has no exp().
//
// Drag the sliders on the two sensors and watch the display follow.

#include <Wire.h>
#include "Adafruit_LEDBackpack.h"
#include "Adafruit_SHT31.h"
#include "Adafruit_SGP30.h"

const int ALERT_LED = 8;
const uint16_t ALERT_PPM = 1000;

// Segments for the units: A is bit 0, G is bit 6.
const uint8_t DEGREE = 0x63;
const uint8_t LETTER_C = 0x39;
const uint8_t LETTER_R = 0x50;
const uint8_t LETTER_H = 0x74;

Adafruit_7segment display = Adafruit_7segment();
Adafruit_SHT31 sht31 = Adafruit_SHT31();
Adafruit_SGP30 sgp;

// How much water saturated air holds, in mg per cubic meter, at 0, 5, 10 ...
// 50 degrees Celsius.
const uint32_t SATURATION[] = {4847, 6797, 9399, 12830, 17300, 23050,
                               30380, 39630, 51120, 65330, 82770};

uint32_t absoluteHumidity(int32_t centiC, int32_t centiRh) {
  if (centiC < 0) centiC = 0;
  if (centiC > 4999) centiC = 4999;
  uint8_t i = centiC / 500;
  uint32_t frac = centiC % 500;
  uint32_t sat = SATURATION[i] + (SATURATION[i + 1] - SATURATION[i]) * frac / 500;
  return sat * (uint32_t)centiRh / 10000;
}

// Two digits on the left, two unit segments on the right.
void showWithUnit(int32_t value, uint8_t left, uint8_t right) {
  display.print(value * 100, DEC);
  display.writeDigitRaw(3, left);
  display.writeDigitRaw(4, right);
}

unsigned long started;
uint8_t tick = 0;
bool alarm = false;

void setup() {
  pinMode(ALERT_LED, OUTPUT);
  display.begin(0x70);
  display.setBrightness(10);
  if (!sht31.begin(0x44) || !sgp.begin()) {
    // "Err": a sensor did not answer.
    display.writeDigitRaw(0, 0x79);
    display.writeDigitRaw(1, LETTER_R);
    display.writeDigitRaw(3, LETTER_R);
    display.writeDisplay();
    while (1) delay(10);
  }
  started = millis();
}

void loop() {
  bool climate = sht31.measure();
  int32_t centiC = sht31.getTemperatureCentis();
  int32_t centiRh = sht31.getHumidityCentis();
  if (climate) sgp.setHumidity(absoluteHumidity(centiC, centiRh));
  if (!sgp.IAQmeasure()) {
    delay(1000);
    return;
  }

  unsigned long seconds = (millis() - started) / 1000;
  tick = (tick + 1) % 6;
  if (seconds < 15) {
    display.print(15 - seconds, DEC);
    display.writeDigitRaw(0, 0x40);
    display.writeDigitRaw(1, 0x40);
  } else if (tick == 4) {
    showWithUnit((centiC + 50) / 100, DEGREE, LETTER_C);
  } else if (tick == 5) {
    showWithUnit((centiRh + 50) / 100, LETTER_R, LETTER_H);
  } else {
    display.print(sgp.eCO2, DEC);
  }
  display.writeDisplay();

  bool high = seconds >= 15 && sgp.eCO2 >= ALERT_PPM;
  if (high != alarm) {
    alarm = high;
    display.blinkRate(alarm ? HT16K33_BLINK_1HZ : HT16K33_BLINK_OFF);
  }
  digitalWrite(ALERT_LED, alarm ? HIGH : LOW);

  // One measurement a second, as the SGP30's baseline algorithm wants.
  delay(1000 - 70);
}

Parts list

7 parts, plus the jumper wires. Every one is in the editor's parts bin.

How it is wired

6 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Arduino Uno R3 pin 8; Resistor, 220 Ω pin 1
  • Arduino Uno R3 pin 5V; Temperature and humidity, SHT31 pin VIN; Air quality sensor, SGP30 pin VIN; Four-digit display, HT16K33 backpack pin VCC
  • Ground: Arduino Uno R3 pin GND; Temperature and humidity, SHT31 pin GND; Air quality sensor, SGP30 pin GND; Four-digit display, HT16K33 backpack pin GND; LED, red pin C
  • Arduino Uno R3 pin A4; Temperature and humidity, SHT31 pin SDA; Air quality sensor, SGP30 pin SDA; Four-digit display, HT16K33 backpack pin SDA
  • Arduino Uno R3 pin A5; Temperature and humidity, SHT31 pin SCL; Air quality sensor, SGP30 pin SCL; Four-digit display, HT16K33 backpack pin SCL
  • Resistor, 220 Ω pin 2; LED, red pin A

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.