Make Your Air Safer: Alerting Indoor IoT Air Quality Monitor

Ensure proper ventilation in your indoor space. Measure and log indoor air quality. Configure alerts in ThingSpeak to keep your air safe.

IntermediateFull instructions provided4 hours7,994
Make Your Air Safer: Alerting Indoor IoT Air Quality Monitor

Things used in this project

Hardware components

Arduino Nano 33 IoT
Arduino Nano 33 IoT
×1
Bosch BME680
×1
LED Stick, NeoPixel Stick
LED Stick, NeoPixel Stick
×1
Box 3.94 × 2.36 × 0.98"
×1
Solderless Breadboard Half Size
Solderless Breadboard Half Size
×1
Breadboard Wire Kit
Digilent Breadboard Wire Kit
×1
screws M2 x 4
×2

Software apps and online services

ThingSpeak API
ThingSpeak API

Hand tools and fabrication machines

File
Drill bit 1.6 mm or close (3/32")
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Air Quality Schematic

Code

Air Quality Code

Arduino
Use to program the indoor air quality device
#include <FlashAsEEPROM.h>
#include <WiFiNINA.h>
#include "bsec.h"
#define TS_ENABLE_SSL
#include "ThingSpeak.h"
#include <Adafruit_NeoPixel.h>
#define LED_COUNT 8

unsigned long myChannelNumber = 123456879101112;
const char * myWriteAPIKey = "XXXXXXXXXXXXXXXX";

// This is the config settings file.
const uint8_t bsec_config_iaq[] = {
  #include "config/generic_33v_300s_4d/bsec_iaq.txt"

};

#define STATE_SAVE_PERIOD UINT32_C(360 * 60 * 1000) // 360 minutes - 4 times a day
#define LED_PIN 14

// Helper functions declarations
void checkIaqSensorStatus(void);
void loadState(void);
void updateState(void);
void connectWiFi();
void initializePixels();
void postDataToThingSpeak();
void blinkForever();

char ssid[] = "SSID"; // your network SSID (name) 
char pass[] = "PASS"; // your network password
WiFiSSLClient client;
int status = WL_IDLE_STATUS; // the Wifi radio's status

const long postInterval = 25000;
long lastUpdate = millis();

Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
int brightValue = 14;
int brightDirection = 1;

// The Bsec object uses the precompiled BSEC library and environmental measurements to compute Air Quality and CO2 Equivalent.
Bsec iaqSensor;
uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {
  0
};
uint16_t stateUpdateCounter = 0;

void setup(void) {
  Serial.begin(115200);
  delay(1500);
  Wire.begin();

  pinMode(LED_BUILTIN, OUTPUT);
  iaqSensor.begin(UINT8_C(0x77), Wire);

  Serial.println("\nBSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix));
  checkIaqSensorStatus();

  iaqSensor.setConfig(bsec_config_iaq);
  checkIaqSensorStatus();

  loadState();

  bsec_virtual_sensor_t sensorList[] = {
    BSEC_OUTPUT_RAW_TEMPERATURE,
    BSEC_OUTPUT_RAW_PRESSURE,
    BSEC_OUTPUT_RAW_HUMIDITY,
    BSEC_OUTPUT_RAW_GAS,
    BSEC_OUTPUT_IAQ,
    BSEC_OUTPUT_STATIC_IAQ,
    BSEC_OUTPUT_CO2_EQUIVALENT,
    BSEC_OUTPUT_BREATH_VOC_EQUIVALENT,
    BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE,
    BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY,
  };

  iaqSensor.updateSubscription(sensorList, 10, BSEC_SAMPLE_RATE_LP);
  checkIaqSensorStatus();
  // Print the header
  Serial.println("Timestamp [ms], raw temperature [°C], pressure [hPa], raw relative humidity [%], gas [Ohm], IAQ, IAQ accuracy, temperature [°C], relative humidity [%]");
  ThingSpeak.begin(client); // Initialize ThingSpeak 
  initializePixels();

}

void loop(void) {

  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  // If new data is available.
  if (iaqSensor.run()) {
    Serial.print(String(millis()) + " , ");
    Serial.print(String(iaqSensor.rawTemperature) + " , ");
    Serial.print(String(iaqSensor.pressure) + " , ");
    Serial.print(String(iaqSensor.rawHumidity) + " , ");
    Serial.print(String(iaqSensor.gasResistance) + " , ");
    Serial.print(String(iaqSensor.iaq) + " , ");
    Serial.print(String(iaqSensor.iaqAccuracy) + " , ");
    Serial.print(String(iaqSensor.temperature) + " , ");
    Serial.print(String(iaqSensor.humidity) + " , ");
    Serial.print(String(iaqSensor.staticIaq) + " , ");
    Serial.print(String(iaqSensor.co2Equivalent) + " , ");
    Serial.println(String(iaqSensor.breathVocEquivalent));

    updateLED(int(iaqSensor.iaq), int(iaqSensor.iaqAccuracy));

    if (millis() - lastUpdate > postInterval) {
      lastUpdate = millis();
      postDataToThingSpeak();
    }

    updateState();
  } else {
    checkIaqSensorStatus();
  }
}

void checkIaqSensorStatus(void) {
  // Check the sensor status and report with the onboard LEDs

  if (iaqSensor.status != BSEC_OK) {
    if (iaqSensor.status < BSEC_OK) {
      Serial.println("BSEC error code : " + String(iaqSensor.status));
      blinkForever();
    } else {
      Serial.println("BSEC warning code : " + String(iaqSensor.status));
    }
  }

  if (iaqSensor.bme680Status != BME680_OK) {
    if (iaqSensor.bme680Status < BME680_OK) {
      Serial.println("BME680 error code : " + String(iaqSensor.bme680Status));
      blinkForever();
    } else {
      Serial.println("BME680 error code : " + String(iaqSensor.bme680Status));
    }
  }
  // Check library why we are clearing warning, then leave comment
  iaqSensor.status = BSEC_OK;
}

void loadState(void) {
  // Load the last state from EEPROM to make sure calibrations stay around in case of reboot.
  // Note that this example writes to EEPROM.  If you already have data in EEPROM it is best to clear the EEPROM before using this example.  

  if (EEPROM.read(0) == BSEC_MAX_STATE_BLOB_SIZE) {
    // Existing state in EEPROM
    Serial.println("Reading state from EEPROM");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE; i++) {
      bsecState[i] = EEPROM.read(i + 1);
      //Serial.println(bsecState[i], HEX);
    }

    iaqSensor.setState(bsecState);
    checkIaqSensorStatus();
  } else {
    // Erase the EEPROM with zeroes
    Serial.println("Erasing EEPROM");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE + 1; i++)
      EEPROM.write(i, 0);

    EEPROM.commit();
  }
}

void updateState(void) {
  // Increment the state update counter.
  // Save the calibration state of the sensor if the time elapsed is greater than the save period.

  bool shouldUpdate = false;
  if (stateUpdateCounter == 0) {
    if (iaqSensor.iaqAccuracy >= 3) {
      shouldUpdate = true;
      stateUpdateCounter++;
    }
  } else {
    if ((stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) {
      shouldUpdate = true;
      stateUpdateCounter++;
    }
  }

  if (shouldUpdate) {
    iaqSensor.getState(bsecState);
    checkIaqSensorStatus();
    Serial.println("Writing state to EEPROM");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE; i++) {
      EEPROM.write(i + 1, bsecState[i]);
      Serial.println(bsecState[i], HEX);
    }

    EEPROM.write(0, BSEC_MAX_STATE_BLOB_SIZE);
    EEPROM.commit();
  }
}

void connectWiFi() {
  // Make a connection to wifi.  Show an error condition on the pixels if failed
  if ((WiFi.status() != WL_CONNECTED)) {
    WiFi.begin(ssid, pass); // Connect to WPA/WPA2 network. Change this line if using open or WEP network
    Serial.print(".");
    delay(10000);
  }
  if ((WiFi.status() == WL_CONNECTED)) {
    Serial.println("Connected");
  } else {
    Serial.println("Failed to connect");
    updateLED(300, 3);
    delay(1000);
    updateLED(100, 0);
    delay(1000);
    updateLED(300, 3);
  }
}

void updateLED(int airQ, int airA) {
  // Set the LEDs to reflect the calibration state and the air quality.
  long qualColor;
  int colorControl = airQ / 2;
  qualColor = strip.Color(colorControl, 255 - colorControl, 0);
  for (int i = 0; i < 8; i++) {
    if (i > ((airA + 1) * 2) - 1) {
      strip.setPixelColor(i, strip.Color(9, 9, 9));
    } else {
      strip.setPixelColor(i, qualColor);
    }
  }
  brightValue = brightValue + brightDirection;

  if (brightValue > 15 || brightValue < 1) {
    brightDirection = brightDirection * -1;
  }
  strip.setBrightness(brightValue);
  strip.show();
  delay(20);
}

void postDataToThingSpeak() {

  ThingSpeak.setField(1, String(iaqSensor.temperature));
  ThingSpeak.setField(2, String(iaqSensor.pressure));
  ThingSpeak.setField(3, String(iaqSensor.humidity));
  ThingSpeak.setField(4, String(iaqSensor.gasResistance));
  ThingSpeak.setField(5, String(iaqSensor.iaq));
  ThingSpeak.setField(6, String(iaqSensor.iaqAccuracy));
  ThingSpeak.setField(7, String(iaqSensor.co2Equivalent));
  ThingSpeak.setField(8, String(iaqSensor.breathVocEquivalent));

  // write to the ThingSpeak channel
  int responseCode = ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);
  if (responseCode == 200) {
    Serial.println("Channel update successful.");
  } else {
    Serial.println("Problem updating channel. HTTP error code " + String(responseCode));
  }
}

void initializePixels() {

  strip.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
  for (int j = 0; j < 8; j++) {
    strip.setPixelColor(j, strip.Color(255 * ((double) rand() / (double) RAND_MAX), 255 * ((double) rand() / (double) RAND_MAX), 255 * ((double) rand() / (double) RAND_MAX)));
  }
  strip.setBrightness(brightValue); // Set BRIGHTNESS 
  strip.show();
}

void blinkForever() {
  for (;;)
    digitalWrite(LED_BUILTIN, HIGH);
  delay(100);
  digitalWrite(LED_BUILTIN, LOW);
  delay(100);
}

Credits

Christopher Stapels

Christopher Stapels

3 projects • 13 followers
Hans Scharler

Hans Scharler

15 projects • 86 followers
IoT Engineer, Maker - I have a toaster that has been tweeting since 2008.
DRainer

DRainer

1 project • 1 follower
Rob Purser

Rob Purser

2 projects • 4 followers
Senior Development Manager for IoT and Hardware Interfacing for MATLAB at MathWorks. Leads the creation of software that connects MATLAB with the real world
Thomas Azir

Thomas Azir

1 project • 1 follower
Colin Streck

Colin Streck

1 project • 1 follower
Matt Malloy

Matt Malloy

1 project • 1 follower

Comments