Project tutorial
Make Your Air Safer: Alerting Indoor IoT Air Quality Monitor

Make Your Air Safer: Alerting Indoor IoT Air Quality Monitor © MIT

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

  • 3,487 views
  • 0 comments
  • 8 respects

Components and supplies

Necessary tools and machines

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

Apps and online services

About this project

Having too many people in one room with poor ventilation can lead to unsafe conditions and an increased probability of sharing airborne viruses. Using ThingSpeak to track the air quality enables live alerts and the ability to visualize historic trends. With this monitor, you can immediately see the local air quality on the 8 pixel LED strip, you can look up what the air quality was yesterday on ThingSpeak, and you can even check the safety of the room when you are far away.

This project creates a working air-quality monitor that measures several environmental variables to compute an Air quality measurements and CO2 equivalent concentration estimate. The instructions are designed to require minimal assembly but result in a working high-quality device. The LED array provides a flexible communication output.

The BME680 air quality sensor reports a calculated air quality measurement and the accuracy of its measurement. The accuracy of the calibration is shown on the LED array as the number of pixels that are lit. The accuracy range is from 0 to 3, so two pixels are lit for each of the accuracy categories. The color of the pixels corresponds to the air quality measurement, from green at 25, yellow at around 200, and red above 400.

Assembly

1) Use a file to remove the protrusions on the solderless breadboard so that it fits snugly in the suggested box. A snug fit is desired to ensure the USB cable can be plugged in without the board sliding around in the box.

2) Press the header that comes with the BME680 into the breadboard in row e as shown in the image below. Place the BME680 sensor on the header. You may also want to support the opposite side of the sensor so that it will solder straight. Solder the header pins, being sure not to make cold joints or short any pins.

3) Place the BME680 and Nano 33 IoT. The USB connector on the Nano faces to the outside, and the pins go in row e and i. The antenna on the Nano 33 IoT is easy to break. Use care when placing or removing the Nano 33 IoT.

4) Place the wires in the breadboard. I strongly recommend color-coding your wires. I used yellow for both voltage levels, but since there are two separate power voltages (5 V and 3.3 V) it is better to use yellow and red to clearly show the two power levels.

5) Place the solderless breadboard in the bottom of the box. Mark holes for the NeoPixel wires and the USB connector.

6) Use a file or Dremel to make a 1.5 cm x 2 cm slot for the USB cable. Use a drill to make a hole for the NeoPixel wires. I suggest at least a 10 mm hole to make sure the NeoPixel stick lays flat. Drill several other holes in each side of the box for airflow. Resist the temptation to drill holes after assembly. I've destroyed a few prototypes when the drill bit slips.

7) Solder three approximately 2 cm-long wires to the NeoPixel stick. Solid core wires are best. Pre-strip the insulation before soldering to avoid accidentally damaging the pads by pulling on the wires. Be sure to use the side with DIN. (DIN is on the right side in the image below.) Only one ground connection is needed. Make sure the pads gets heated well to make a good solder joint.

8) Place the breadboard in the bottom of the box and feed the NeoPixel stick wires through the appropriate hole. Drill 1.5 mm pilot holes to hold the stick in place. Use slow, steady pressure to thread the M2 screws into the plastic case and hold the stick in place. Connect the NeoPixel wires as shown in the image above. You can use the 3.3 V power, but I suggest the 5 V power supply for the NeoPixels to give the best output.

Note: A 470-ohm resistor on the data line and a 1000 uF capacitor from input power to ground will help protect your NeoPixels. These components are not shown in these images.

9) Place the lid and plug in the USB cable.

ThingSpeak

1) Create a ThingSpeak account if you don't already have one, or sign in to your account.

2) Create a ThingSpeak channel, using the top menu Channels > My Channels > New channel. Enable all eight fields using the checkboxes and provide names for each field. Record the write API key from the API Keys tab and the Channel ID on the upper left in the channel view.

Programming

1) Start a new Arduino sketch. In the Arduino IDE, use Sketch > Include Library > Manage Libraries to install libraries if you don't already have them. Enter the name of the library in the search bar. You need these libraries for this sketch:

  • FlashAsEEPROM
  • bsec
  • ThingSpeak
  • Adafruit_NeoPixel
  • WiFiNINA

Use FlashStorage in the library manager to find the FlashAsEEPROM library. Some devices, such as ESP8266, may not need this library to use EEPROM.

2) Paste the code. Edit the WiFi information ssid and pass. Also edit the myChannelNumber and myWriteAPIKey.

3) Since the BSEC library is precompiled, you need to modify the build procedure. Modify the platform.txt file on your computer as described in step 3 of the BSEC GitHub site.

4) Select the appropriate COM port and program the device.

Create Email Alert

Log in to ThingSpeak and use the top menu to select Apps > MATLAB Analysis.

2) Select New and name your analysis. Paste the code and change the channel ID to match your channel. If your channel is private, you will need to add the read API key.

% Channel Information
channelId = nnnnnnnn;

% Get hourly Air Quality measurements
myData = thingSpeakRead(channelId,'Field',7, 'numdays',1, ...
'OutputFormat','TimeTable');
hourlyData = retime(myData,'hourly','linear');

% Calculate the maximum value and set a desired maximum
[largestValue, index] = max(hourlyData{:,1});
threshold = 4000;

% Provide the ThingSpeak alerts API key. All alerts API keys start with TAK.
alertApiKey = 'TAKxxxxxxxxxxxxx';

% Set the email options
alertUrl="https://api.thingspeak.com/alerts/send";
options = weboptions("HeaderFields", ["ThingSpeak-Alerts-API-Key", alertApiKey]);

% Set the outgoing message
alertBody = sprintf(' Largest hourly value %.1f at %s', largestValue, hourlyData.Timestamps(index));
if (largestValue <= threshold)
alertSubject = sprintf(" Over threshold for last 24 hours");
elseif (largestValue > threshold)
alertSubject = sprintf(" Last 24 hours within specification");
end

webwrite(alertUrl , "body", alertBody, "subject", alertSubject, options);

3) Use the TimeControl app to schedule the code. Select Apps > TimeControl and click New TimeControl. Name your TimeControl, select recurring for Frequency. Choose Day for Recurrence. Pick MATLAB Analysis for the Action and select the code you just created for Code to execute.

Code

Air Quality CodeArduino
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);
}

Schematics

Air Quality Schematic
Schematic mblwtik38x

Comments

Similar projects you might like

Atmospheric Air Analyser

Project in progress by Tejas Shah and Tejas Shah

  • 11,606 views
  • 12 comments
  • 33 respects

Personal Weather Station (Arduino+ ESP8266 + Thingspeak)

Project tutorial by Jayraj Desai

  • 91,139 views
  • 35 comments
  • 169 respects

AggroFox: Large-Scale and Urban Agriculture IoT Solution

Project tutorial by EdOliver and Victor Altamirano

  • 16,423 views
  • 1 comment
  • 48 respects

SmartAgro

Project tutorial by Andrei Florian

  • 37,876 views
  • 21 comments
  • 116 respects

ThingSpeak Arduino Weather Station

Project tutorial by neverofftheinternet

  • 7,335 views
  • 5 comments
  • 11 respects
Add projectSign up / Login