Gas Detection Device

The aim of this project was to build a portable, low-cost Gas-Sensing Device that can be used to detect air pollutants from various sources.

IntermediateShowcase (no instructions)5
Gas Detection Device

Things used in this project

Hardware components

Particle Argon
Adafruit I2C OLED display
Adafruit Tactile Button Swit
NeoPixel Ring: WS2812 5050 RGB LED
Adafruit NeoPixel Ring: WS2812 5050 RGB LED
MQ-9 I2C Gas Sensor
MQ-131 I2C Gas Sensor
HM3301 I2C Laser PM2.5 Dust Sensor
DFRobot MG-811 Analog CO2 Gas Sensor
YWrobot Breadboard Power Supply Module

Software apps and online services

Visual Studio Code Extension for Arduino
Microsoft Visual Studio Code Extension for Arduino
Adafruit IO

Hand tools and fabrication machines

Laser cutter (generic)
Laser cutter (generic)


Read more

Custom parts and enclosures

Laser Cutting Case Specs

This is the template for our Gas Detection Case, which we laid out in Adobe Illustrator and cut acrylic via an Epilog Laser Cutter. It is fastened together using various screws and washers. It also has a handle for portability.


Fritzing Diagram

This is the circuit diagram for the gas detection device. It does need a separate power supply to power the sensors on the 5V rail. The other components (CO2 sensor, OLED, Neopixel ring, and button can be powered through the 3.3V on the Particle Argon.


Gas Detection Program

This program was written in C++ programming language in Visual Studio Code with the necessary Particle Argon extensions The main functions of the program detect gas readings from 4 different gas sensors and publish the readings to the Adafruit Dashboard.
 * Project: Capstone Driver Program
 * Description: The purpose of this capstone project is to establish a cost-effective detection mechanism 
 * for two major organizations: City of Albuquerque and Fuse Makerspace. This project focuses on the 
 * detection and data analysis of airborne pollutants emitted from ABQ city- and Fuse-owned equipment.
 * Components/Sensors included:
 * - Particle Argon Microcontroller
 * - Gas Sensor: CO (Carbon Monixide) MQ-9 I2C
 * - Gas Sensor: O3 (Ozone) MQ-131 I2C
 * - Gas Sensor: PM2.5 (Particulate Matter) HM3301 I2C
 * - Gas Sensor: CO2 (Carbon Dioxide) MG-811 analog
 * - OLED Display I2C
 * - Button
 * - NeoPixel Ring

 * Author: Cyntelle Renteria & Ted Fites
 * Date: 8/23/20
 * Modifications:
 * 9/8/20 CR updated M01_get_MQ9_data with calculcation for correct CO values
 * 9/5/20 CR cleaned up code and added documentation (comments) throughout main driver program
 * 9/4/20 TF/CR attempted gas detection for NO2 sensor at Alvarado Transportation Center, resulted in damage to sensor
 * in an attempt to test circuit and code. At present, no successful readings were taken. Sensor is defunct. Function is
 * removed from main loop, but left in program. 
 * 9/4/20 CR added function M05_get_MG811_data per BR for MG-811 sensor to capture CO2 emissions (working)
 * 9/4/20 CR added OLED with button to switch menus, and updated MQ131 code (working)
 * 9/3/20 CR modified code per CC notes (still need to work on neo pixel + proper documentation of code)
 * 9/2/20 CR added function M04_get_HM3301_data to capture particulate matter data + neopixel 
 *        to visualize good/medium/poor air quality + Adafruit IO dashboard to publish data to cloud
 * 8/31/20  CR Added function M02_get_MQ131_data to capture ozone gas emissions + Json function 
 *          to post CO & O3 data to Particle Console
 * 8/31/20  TF Added function M03_GetGasConcentration_MakerIO_FINAL to calculate NO2 gas concentrations
 * 8/31/20  CR modified + tested MQ9 sensor function in clean air (working)
 * 8/23/20  CR Created program + added MQ9 function (not tested)

// HEADER section ********************************************************

//Header Files
#include <secrets.h>
#include <math.h> // referenced by functions M02_get_MQ131_data and M05_get_MG811_data
#include <Adafruit_MQTT.h> // refenced by functions MQTT_connect and MQTT_ping
#include "Adafruit_MQTT/Adafruit_MQTT.h" // refenced by functions MQTT_connect and MQTT_ping
#include "Adafruit_MQTT/Adafruit_MQTT_SPARK.h" // refenced by functions MQTT_connect and MQTT_ping
#include "Adafruit_MQTT/Adafruit_MQTT.h" // refenced by functions MQTT_connect and MQTT_ping
#include "Adafruit_SSD1306.h" // referenced by functions display_Data_OLED and displayMenu
#include "neopixel.h" // refenced by function light_Read_Sensors_Pixel and other 'light' functions

/************************* Setup *********************************/ 
#define AIO_SERVER      "" 
#define AIO_SERVERPORT  1883                   // use 8883 for SSL 

/************ Global State (you don't need to change this!) ***   ***************/ 
TCPClient TheClient; 

// Setup the MQTT client class by passing in the WiFi client and MQTT server and login details. 
/************************* END Setup *********************************/ 

/****************************** Feeds ***************************************/ 
Adafruit_MQTT_Publish CO = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/Carbon Monoxide");
Adafruit_MQTT_Publish O3 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/Ozone");
Adafruit_MQTT_Publish PM = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/Particulate Matter");
Adafruit_MQTT_Publish CO2 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/Carbon Dioxide");
/****************************** END Feeds ***************************************/ 

// Constants & variables ********************************************************

//used in multiple programs//
const int argonRES = 4096; // ARGON RESOLUTION
const float argonVOLT = 3.3; // ARGON VOLTAGE

//******* M01_get_MQ9_data constants and variables for function **********//
const int MQ9_Addr = 0x50; // Address for MQ9 I2C CO Sensor
unsigned int MQ9_data[2];
int MQ9_raw_adc = 0;
float MQ9_Vadc1;
float MQ9_RsRo;
float COppm = 0.0;
// END  Variables for M01_get_MQ9_data function **********//

//******* M02_get_MQ131_data constants and variables for function **********//
const int MQ131_Addr = 0x51; // Address for MQ131 I2C Ozone Sensor
unsigned int MQ131_data[2];
int MQ131_raw_adc = 0;
float MQ131_Vadc1;
float MQ131_RsRo;
float O3ppm = 0.0;
// END  Variables for M02_get_MQ131_data function **********//

// *** DEFUNCT ** M03_GetGasConcentration_MakerIO-FINAL: program CONSTANTS & VARIABLES
* Input resolution on Argon mc is 2 12th power. This is 4 times more refined than the resolution used in M01 & 
* M02 functions (used in source microcontroller Arduino Uno from source program).
const float V_REF = 3.3; // Per BR, adjusted internal voltage set to Argon input voltage. will triple nanoAmp 
// calculation compared to TEST VOLTS_LADDER_REF. 
const float S_F = 1.4; // sensitivity in nA/ppm. This is roughly about 1/2 the sensitivity of the barcode on the
//sensor (2.78::see Spec Sensor 3SP_NO2_5F-P-Package-110-507.pdf). A number less than 2 will inflate the #PPMs 

const int analogInPinD14 = D14;  // Analog input pin that the sensor is attached to
const int R1_VALUE = 9700;  // Value of 10kOhm resistor (falls within +/- 5% range)
const int SMPL_SIZE = 256; //Number of samples averaged, like adding 8 bits to ADC
const int VLTG_LADDER_OFF = 375; // replaces source C_OFF for accurate voltage ladder offset from sesnor reading

const float CONTROLLER_RESOLUTION = 4096; 
const float NANO_AMPS = 1000000000.0;

long int NO2SensorValue = 0;        // ORIGINAL value read from the sensor
float NO2PPMconc=0.0;  // Stores calculated NO2 gas concentration from sensor readings
// END M03_GetGasConcentration_MakerIO: program CONSTANTS & VARIABLES

//******* Variables for M04_get_HM3301_data function **********//
const int HM3301_Addr = 0x40; // Address for HM3301 I2C Dust Sensor
uint8_t HM3301_data[30];
uint16_t HM3301_adc;
uint8_t *pointer;
uint16_t HM3301_data2;
// END  Variables for M04_get_HM3301_data function **********//

//******* Variables for M05_get_MG811_data function **********//
int mgPin = A5;
float raw_MG;
float CO2ppm;

#define DC_GAIN (8.5) //define the DC gain of amplifier
#define ZERO_POINT_VOLTAGE (0.3023) //define the output of the sensor in volts when the concentration of CO2 is 400PPM
#define REACTION_VOLTAGE (0.030) //define the voltage drop of the sensor when move the sensor from air into 1000PPM CO2
#define READ_SAMPLE_INTERVAL (50) //define how many samples you are going to take in normal operation
#define READ_SAMPLE_TIMES (5) //define the time interval(in milisecond) between each samples in 

float CO2Curve[3] = {2.602, ZERO_POINT_VOLTAGE, (REACTION_VOLTAGE/(2.602-3))};
// END  Variables for M05_get_MG811_data function **********//

//******* Variables for OLED **********//
#define OLED_RESET A0
Adafruit_SSD1306 display(OLED_RESET);
char currentDateTime[25], currentTime[9];
// END  Variables for OLED **********//

//******* Variables for Button **********//
int buttonPin = D2;
bool buttonState;
bool menuSwitch = false;
//******* END Variables for Button **********//

//******* Variables for NeoPixel **********//
const int neo_pin = D3;
const int PixelON = 0xFF00FF; // first pixel in magenta
const int GoodAQ = 0x00FFFF; // pixel color cyan
const int ModAQ = 0xFFFF00; // pixel color yellow
const int HazardAQ = 0x8B0000; // pixel color red
const int ErrorAQ = 0x9932CC; // pixel color purple

#define PIXEL_PIN D3
#define PIXEL_COUNT 12
#define PIXEL_TYPE WS2812B

Adafruit_NeoPixel pixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);
// END  Variables for lightNeoPixel functions **********//

//******* Variables for MQTT **********//
unsigned long last;
unsigned long lastMinute;
// END  Variables for MQTT **********//

// END  Constants & variables ********************************************************

// END  HEADER section ********************************************************


void setup() 
  //declare program pin modes
  pinMode(buttonPin, INPUT_PULLDOWN); //pin mode for button
  pinMode(mgPin, INPUT); //pin mode for CO2 MG-811 sensor analog read

  //initialize serial monitor

  //initialize I2C transmission
  Wire.beginTransmission(MQ9_Addr); // wake up CO sensor
  Wire.beginTransmission(MQ131_Addr); // wake up O3 sensor
  Wire.beginTransmission(HM3301_Addr); //wake up PM sensor

  //initialize OLED display
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  //request time sync from Particle Cloud for OLED display
  waitUntil(Particle.syncTimeDone);; // set to mountain
  //removes latency from button click (enables quick switch from menu 1 to menu 2)
  attachInterrupt(buttonPin, enableButton, RISING);

  //initialize neopixels

void loop() 
**************************************  MAIN LOOP  **********************************
  //1): light pixel 1 to indicate program running
  pixel.setPixelColor(0, PixelON);
  //2): switch between two OLED menus with button
  buttonState = digitalRead(buttonPin);
  {//switches from menu 1 to menu 2
    menuSwitch = !menuSwitch;

  //display neopixel color key (menu 1) + gas concentration data (menu 2)
  //***** END Step 2 ****//

  //3): connect to for publishing gas concentration data from sensors to cloud

  //4): read and publish gas concentration data to "Gas Emissions" dashboard every 30 seconds
   if(millis()-lastMinute > 15000) 
   {//read gas sensors to detect gas concentration 
    {//publish gas concentration data to Adafruit feeds and dashboard
    lastMinute = millis();
  //***** END Step 4 ****//

  //5): light neo pixels to visualize gas concentrations (see color key on OLED)
} //*********************************** END LOOP  *************************************

void M01_get_MQ9_data() //CARBON MONOXIDE
{//reads Carbon Monixide gas concentration and calculates PPM
  // Start I2C transmission from gas sensor
  // Request 2 bytes of data
  Wire.requestFrom(MQ9_Addr, 2, true);
  // Read 2 bytes of data: raw_adc msb, raw_adc lsb
  MQ9_data[0] =;
  MQ9_data[1] =;
  // Convert the data to 12-bits
  MQ9_raw_adc = ((MQ9_data[0] & 0x0F) * 256) + MQ9_data[1]; //8 bits + 4 bits
  //voltage conversion
  MQ9_Vadc1 = MQ9_raw_adc*(argonVOLT/argonRES); //(ARGON required voltage/ARGON resolution)
  //Formula to convert raw data to PPM (parts per million) per BR
  MQ9_RsRo = (1/1.55)*((1.5-MQ9_Vadc1)/MQ9_Vadc1)*10.0;
  COppm = pow(10,-log(MQ9_RsRo)+1.48);  
  Serial.printf("Carbon Monoxide: %0.2fppm\n", COppm);

void M02_get_MQ131_data() //OZONE
{//reads Ozone gas concentration and calculates PPM
  // Start I2C transmission from gas sensor
  // Request 2 bytes of data
  Wire.requestFrom(MQ131_Addr, 2, true);
  // Read 2 bytes of data: raw_adc msb, raw_adc lsb
  MQ131_data[0] =;
  MQ131_data[1] =;
  //convert to 12 bits - combine bytes
  MQ131_raw_adc = ((MQ131_data[0] & 0x0F) * 256) + MQ131_data[1]; //8 bits + 4 bits
  //voltage conversion
  MQ131_Vadc1 = MQ131_raw_adc*(argonVOLT/argonRES); //(ARGON required voltage/ARGON resolution)
  // Formulas to convert raw data from combined bytes to PPM (parts per million) per BR
  MQ131_RsRo = (20.0/0.18)*(argonVOLT-MQ131_Vadc1)/MQ131_Vadc1;
  O3ppm = pow(10,-log(MQ131_RsRo)+1.48);
  // Serial.printf("Ozone: %0.2f ppm\n", O3ppm);

void M03_GetGasConcentration_MakerIO_FINAL()
  float rawInput=0.0;
  float calc_nA=0.0;

// 1) read and accumulate the test analog in values:
  NO2SensorValue = 0;
  for (int i = 0; i < SMPL_SIZE; i++) 
    * 8/31/20 TF. Per BR, relocate voltage ladder offset to INSIDE sample size loop to more
    * accurately reduce input sensor reading dur to ladder. This will make low sensor readings
    * more accurate.
    NO2SensorValue += analogRead(analogInPinD14) - VLTG_LADDER_OFF; // source code with analog read
    delay(3);   // needs 2 ms for the analog-to-digital converter to settle after the last reading
  Serial.printf(" TOTAL: NO2SensorValue >%i\n", NO2SensorValue);

 // 2) print the PPM results to the serial monitor:
  rawInput = (float) NO2SensorValue / (float) SMPL_SIZE / CONTROLLER_RESOLUTION;
  calc_nA =  V_REF/ (float) R1_VALUE * NANO_AMPS; // I=V/R * nanoAmps
  NO2PPMconc = (float) rawInput * calc_nA / S_F;
  Serial.printf("PPM >%0.2f\n",NO2PPMconc);
 //Trying to make each loop 1 second
  delay(218);  //1 second  3ms*256ms (each adc read)-14ms (for printing)= 218ms

void M04_get_HM3301_data() //PARTICULATE MATTER
  // Start I2C transmission from gas sensor
  Wire.requestFrom(HM3301_Addr, 29, true);
  //stores all ozone gas concentration data from 29 bytes into array
  for(int i=0;i<29;i++)
      HM3301_data[i] =;
  //bit shifting to combine bytes
  for(int n=0;n<29;n=n+2)
    HM3301_adc = (HM3301_data[n] << 8) | HM3301_data[n+1];
    // Serial.printf("Data: %i  Particulate Matter: %i\n", n+1, HM3301_adc);
  //use pointer to access single byte to get PM2.5 data from sensor 
  pointer = &HM3301_data[13]; //access PM2.5 data
  HM3301_data2 = *pointer; //will be referenced for printing by functions displayMenu() for OLED display and in Adafruit publishing 
  // Serial.printf("Particulate Matter: %i\n", HM3301_data2);

void  M05_get_MG811_data() //CARBON DIOXIDE per BR
  //perform analog read on sensor and get sensor voltage for base 0 CO2 level 
  raw_MG = MGRead(mgPin);
  //convert raw data to PPM
  CO2ppm = MGGetPercentage(raw_MG,CO2Curve);
  Serial.printf("CO2 conc = %0.2f, Voltage = %0.2f \n", CO2ppm, raw_MG);

//**** M05_get_MG811_data Support Functions ****//

float MGRead(int mgPin) 
{//read sensor data and get sensor voltage for base 0 CO2 level 
  float v = 0;
  for(int i=0; i<READ_SAMPLE_TIMES; i++)
    v += analogRead(mgPin);
  v = (v/READ_SAMPLE_TIMES)*(argonVOLT/argonRES);
  return v; //returns voltage

int MGGetPercentage(float volts, float *pcurve)
{//convert raw data from MG-811 CO2 sensor to PPM
    return -1;
    return pow(10, ((volts/DC_GAIN) - pcurve[1]) / pcurve[2] + pcurve[0]); //returns CO2Curve
//**** END M05_get_MG811_data Support Functions ****//

void  light_Read_Sensors_Pixel() // 12 O'CLOCK NEOPIXEL
{//blink neopixel 1 to indicate reading gas sensors
  for(int i=0; i<4; i++)
    pixel.setPixelColor(0, PixelON);;

void  light_CO_MQ9_Pixel() //3 O'CLOCK NEOPIXEL
  if(COppm < 10.0) //GOOD AIR QUALITY
    pixel.setPixelColor(3, GoodAQ);;

  if(COppm > 10.0 && COppm < 100.0) //MODERATE AIR QUALITY
    pixel.setPixelColor(3, ModAQ);;

  if(COppm > 100.0) //HAZARDOUS AIR QUALITY
    pixel.setPixelColor(3, HazardAQ);;

void  light_O3_MQ131_Pixel() //5 O'CLOCK NEOPIXEL
  if(O3ppm < 0.08) //GOOD AIR QUALITY
    pixel.setPixelColor(5, GoodAQ);;

    if(O3ppm > 0.08 && O3ppm < 0.2) //MODERATE AIR QUALITY
    pixel.setPixelColor(5, ModAQ);;

  if(O3ppm > 0.2) //HAZARDOUS AIR QUALITY
    pixel.setPixelColor(5, HazardAQ);;

void  light_PM_HM3301_Pixel() //7 O'CLOCK NEOPIXEL
  if(HM3301_data2 < 20) //GOOD AIR QUALITY
    pixel.setPixelColor(7, GoodAQ);;

  if(HM3301_data2 > 20 && HM3301_data2 < 35) //MODERATE AIR QUALITY
    pixel.setPixelColor(7, ModAQ);;

  if(HM3301_data2 > 35) //HAZARDOUS AIR QUALITY
    pixel.setPixelColor(7, HazardAQ);;

void  light_CO2_MG811_Pixel() //9 O'CLOCK NEOPIXEL
  if(CO2ppm < 750) //GOOD AIR QUALITY
    pixel.setPixelColor(9, GoodAQ);;

  if(CO2ppm > 750 && CO2ppm < 1200) //MODERATE AIR QUALITY
    pixel.setPixelColor(9, ModAQ);;

  if(CO2ppm > 1200) //HAZARDOUS AIR QUALITY
    pixel.setPixelColor(9, HazardAQ);;


void  display_Data_OLED()
{//OLED menu 1 displays color key for neopixels, OLED menu 2 displays timestamp and gas concentrations

void  getTime() //formats particle time on OLED
  String DateTime, TimeOnly;

  // get current time
  DateTime = Time.timeStr();
  TimeOnly = DateTime.substring(11,19);

  // using time with formatted print statements
  // Serial.printf("Date and Time is %s\n", currentDateTime);
  // Serial.printf("Time is %s\n", currentTime);

void  displayMenu()
{//menuSwitch conditions change menus on OLED
 {//toggles ON menu 2 once button is clicked
    display.printf("Time: %s\n", currentTime);
    display.printf("clockwise from pink:\n");
    display.printf("1- reading sensors..\n");
    display.printf("2- CO: %0.2fppm\n", COppm);
    display.printf("3- O3: %0.2fppm\n", O3ppm);
    display.printf("4- PM2.5: %i\n", HM3301_data2);
    display.printf("5- CO2: %0.2fppm\n", CO2ppm);
 else if(!menuSwitch)
 {//toggles ON menu 1 by default once program is run
    display.printf("Air Quality Color Key\n");
    display.printf("blue:    GOOD\n");
    display.printf("yellow:  MODERATE\n");
    display.printf("red:     HAZARDOUS\n");
//     // Serial.printf("button state: %i\n", buttonState);
//     // Serial.printf("menu: %i\n", menuSwitch);

void  enableButton()
{//function used in interrupt in setup to improves button click response 
  buttonState = true;

void MQTT_connect() 
{// Function to connect and reconnect as necessary to the MQTT server.
 // Should be called in the loop function and it will take care if connecting.
  int8_t ret;
  if (mqtt.connected()) 
  Serial.print("Connecting to MQTT... ");
  while ((ret = mqtt.connect()) != 0) 
  { // connect will return 0 for connected
       Serial.println("Retrying MQTT connection in 5 seconds...");
       delay(5000);  // wait 5 seconds
  Serial.println("MQTT Connected!");

void MQTT_ping()
{//function pings to communicate to
    if ((millis()-last)>30000) 
    Serial.printf("Pinging MQTT \n");
      Serial.printf("Disconnecting \n");
    last = millis();




2 projects • 3 followers


2 projects • 2 followers


Add projectSign up / Login