SignCompanyVOC

This unit captures Humidity, Temperature, CO2 (ppm), and TVOC (ppb) and sends that data to an Adafruit dashboard.

SignCompanyVOC

Things used in this project

 

Hardware components

HARDWARE LIST
1 DFRobot bme280 / ens160 combo component
1 Particle Argon
1 adafruit neopixel ring 12

Software apps and online services

 

Microsoft Visual Studio Code Extension for Arduino

Hand tools and fabrication machines

HARDWARE LIST
1 Soldering iron (generic)
1 Solder Wire, Lead Free
1 Epilog Helix 50 Laser Cutter / Engraver
1 Micrometer
1 Extech Multimeter

Story

 

The internals:

 

 

Placement testing using a clear acrylic lid:

 

 

Prints shops are concerned about indoor humidity as it can affect the transfer of, and drying characteristics, of ink and the media it is being applied to.Another concern at any fabrication/production shop is the air quality the staff are subjected to during the workday.

 

The Dashboard:

 

 

This unit captures Humidity, Temperature, CO2 (ppm), and TVOC (ppb) and sends that data to an Adafruit dashboard. The neopixel rings on the device itself show the level of Humidity and TVOC only, the two measurements that the client wants to know about.

 

As the count of each rises, so too does the number and color of the neopixels for each category. TVOC counts that are safe for human exposure are color coded green, warning levels are in yellow, and danger levels are in red. A similar scale exists for Humidity.

 

Final Assembly - Ready to Install:

 

 

Schematics

 

SignCompanyVOC Fritzing as PNG

 


 

Code

 

SignCompanyVOC.ino

C/C++

CODE
/*
 * Project SignCompanyVOC
 * Description: Detects Total Volatile Organic Compounds, CO2 Temperature
 *              and humidity.
 * 
 *              All 4 readings will be sent to an Adafruit dashboard, while TVOC ppb
 *              and % Humidity will be depicted on-site by colorized neopixel rings.
 * 
 * Effects of TVOC on the human body:
 * < 50 = Normal
 * 50 to 750 = Anxious, uncomfortable
 * 750 to 6000 = depressive, headache
 * > 6000 = headache, and other nerve problems
 *
 * Effects of CO2 on the human body:
 * < 500 = Normal
 * 500 to 1000 = A little uncomfortable
 * 1000 to 2500 = Tired
 * > 2500 = Unhealthy
 * 
 * 
 * Author: Clint Wolf
 * Date: December 2022
 */

//===============================================

#include <JsonParserGeneratorRK.h>
#include <Particle.h>
#include <Wire.h>

#include <neopixel.h>

// Adafruit.io Set Up BEGIN
#include <Adafruit_MQTT.h>          
#include "Adafruit_MQTT/Adafruit_MQTT.h"
#include "Adafruit_MQTT/Adafruit_MQTT_SPARK.h"
#include "Adafruit_MQTT/Adafruit_MQTT.h"

/************************* Adafruit.io Setup ******************************************/
#define AIO_SERVER      "io.adafruit.com"
#define AIO_SERVERPORT  1883   // use 1883 for SSL
#define AIO_USERNAME    ""
#define AIO_KEY         ""

#include "Credentials.h" // for Adafruit
#include "Adafruit_BME280.h"

// define the bme object - will use the I2C interface to the BME Sensor
Adafruit_BME280 theBMEObject;

#include "ScioSense_ENS160.h"
//ScioSense_ENS160 theENSObject;
//ScioSense_ENS160 theENSObject(0x53);
//ScioSense_ENS160 ens160(ENS160_I2CADDR_0);
ScioSense_ENS160 theENSObject(ENS160_I2CADDR_1);

/************ Global State (no need to change this) ************/
TCPClient TheClient;

// Setup the MQTT client class by passing in the WiFi client, MQTT server, and login details
Adafruit_MQTT_SPARK mqtt(&TheClient,AIO_SERVER,AIO_SERVERPORT,AIO_USERNAME,AIO_KEY);

/****************************** Feeds ***************************************/ 
// Setup a feed called <object> for publishing. 
// Notice MQTT paths for AIO follow the form: <username>/feeds/<feedname>

//Example: Adafruit_MQTT_Publish theTemperatureObject = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/Feed2_Temperature");
//Adafruit_MQTT_Publish theSoundObject = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/SafetyWarning_Sound");

//Note that if the feed exists in a group on Adafruit, it must be referenced using a period as in the following:
//Adafruit_MQTT_Publish theSoundObject = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/fuse.safetywarningsound");
Adafruit_MQTT_Publish theTemperatureObject = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/nmmep.SignCompany_Temperature");
Adafruit_MQTT_Publish theHumidityObject = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/nmmep.SignCompany_Humidity");
Adafruit_MQTT_Publish theTVOCObject = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/nmmep.SignCompany_TVOC");
Adafruit_MQTT_Publish theeCO2Object = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/nmmep.SignCompany_eCO2");

// Adafruit.io Set Up END

// The broker's server may sever the connection if a publish event is not done prior to the default of 5 minutes.
// This variable will capture the elapsed time since last publish and 
// if that elapsed time >= 2 minutes, the program will ping the server.
int var_LastServerPing = 0;
//int var_LastCurrentSensorAPLaserRead = 0;

//-----------------------------------------

// NEOPIXEL SET UPS

// Define constants for the two neopixel rings
const int PIN_Humidity = 6; // Pin 6 (D6) on Argon for output signal to neopixel ring
const int PIN_TVOC = 7; // Pin 7 (D7) on Argon for output signal to neopixel ring
const int NUMPIXELS = 12; // the actual number of neopixels (per ring)
//Adafruit_NeoPixel thePixelObject(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
//supports WS2811/WS2812/WS2813
Adafruit_NeoPixel thePixelObject_Humidity(NUMPIXELS, PIN_Humidity, WS2812B);
Adafruit_NeoPixel thePixelObject_TVOC(NUMPIXELS, PIN_TVOC, WS2812B);
//neopixel thePixelObject(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
// Constructor: number of LEDs, pin number, LED type

const int pixelDelay=250; // just a variable I created to delay a change in pixel properties
const int startPixel=0; // the first pixel is 0 (zero based)
int var_PixelBrightness = 10; // the brightness value can go from 1 to 255
// thePixelObject.Color() takes RGB values from 0,0,0, to 255,255,255 - or a defined color from an included COLORS file

// RangeColor variables - this is the only place you need to change the pixel colors
int var_LowRangeColor = 0x005500; // green
int var_MidRangeColor = 0x555500; // yellow
int var_HighRangeColor = 0x550000; // red

// This array will hold the values for each neopixel in each pixel ring. 
// It is populated in "void setup()" using the RangeColor variables above.
int arr_NeoPixelColors[12];

//int arr_SampleData[6] = {0,10,26,27,62,91}; // used only for testing
int arr_Humidity[12] = {0,9,18,27,36,45,54,63,72,81,90,100};
int arr_TVOC[12] = {0,20,35,49,100,250,450,600,750,1500,2500,5000};

bool BMEstatus;

float var_Temperature; // is returned in Celsius 
float var_Humidity_As_Float; // will be converted to an Integer once read from the sensor
int var_Humidity;

int var_TVOC;
int var_eCO2;

bool ENSstatus;

//===============================================================================

void setup() 
{
  // Put initialization like pinMode and begin functions here.
  Serial.begin(9600);
  //while(!Serial);
  waitFor(Serial.isConnected, 15000);
  delay(1);

  // Start of BME280 SECTION ================================
    //Serial.println(F("BME280 test"));
    //BMEstatus = theBMEObject.begin(0x76);  // we specified a hex
    BMEstatus = theBMEObject.begin();
    if (!BMEstatus) 
    {
        Serial.println("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
        Serial.print("SensorID was: 0x"); Serial.println(theBMEObject.sensorID(),HEX);
        Serial.println("        ID of 0xFF probably means a bad address, a BMP 180 or BMP 085");
        Serial.println("   ID of 0x56-0x58 represents a BMP 280,");
        Serial.println("        ID of 0x60 represents a BME 280.");
        Serial.println("        ID of 0x61 represents a BME 680.");
        while (1);
    }
    else 
    {
      Serial.println("BME280 Up and Running");
    }

  // End of BME280 SECTION===================================

  // Start of ENS160 SECTION=================================

    ////ENSstatus = theENSObject.begin(1,0); // used to create "debug.txt" from PowerShell
    ENSstatus = theENSObject.begin();
    Serial.println(theENSObject.available() ? "ENS160 Up and Running" : "ENS160 failed");
    
    if (theENSObject.available())
      {
    //   // Print ENS160 versions
    //   Serial.print("\tRev: "); Serial.print(theENSObject.getMajorRev());
    //   Serial.print("."); Serial.print(theENSObject.getMinorRev());
    //   Serial.print("."); Serial.println(theENSObject.getBuild());
    //
    //   Serial.print("\tStandard mode ");
    Serial.println(theENSObject.setMode(ENS160_OPMODE_STD) ? "ENS160 setMode done." : "ENS160 setMode failed!");
      }
  
  // End of ENS160 SECTION===================================

  Wire.begin(); // starts up the I2C interface
  Wire.beginTransmission(0x40);
  Wire.write(0x88);
  Wire.endTransmission(false);

  Serial.println(); // just giving myself a line break to more easily see the message in the next line on the terminal
  Serial.println("The program has activated the Serial port.");

  pinMode(6,OUTPUT); // used to send a signal for a neopixel ring - Humidity
  pinMode(7,OUTPUT); // used to send a signal for a neopixel ring - TVOC


  // NEOPIXEL SETUPS
  
  LoadPixelColorArray(); // Using the values defined by the RangeColor variables, this call will populate the array
  thePixelObject_Humidity.begin();
  thePixelObject_TVOC.begin();

  ManagePixelRings_StartupDisplay(); // lets the user know the system is active by displaying lights

  
  //thePixelObject_Humidity.clear(); // sets all pixels' colors to 'off' - to initialize ALL pixels

  //Connect to WiFi but not Particle Cloud
  WiFi.connect();
  while(WiFi.connecting()) 
     {
      Serial.printf(".");
       delay(250);
     }
  Serial.printf("\n");
  delay(5);
}

//===============================================================================

void loop() 
{
  MQTT_connect();
  KeepConnectionAlive();

  ReadBMESensor();
  ReadENSSensor();

  DetermineHumidityNeopixels();
  DetermineTVOCNeopixels();

  // The below was used for testing the pixel lighting values
  //for(int var_hum=0;var_hum<6;var_hum++)
  //{
    //DetermineHumidityNeopixels(arr_SampleData[var_hum]);
    //Serial.print("Humidity value for testing: ");
    //Serial.println(arr_SampleData[var_hum]);
    //delay(10000);
  //}
  
  PublishDataToDashboard();

  delay(30000); // pause between readings
}

//===============================================================================

void ReadBMESensor()
{
 
  // Temperature and Humidity from BME chip
  var_Temperature = (theBMEObject.readTemperature() * 9/5) + 32; // conversion to degrees Fahrenheit
  Serial.print("Temperature: "); Serial.println(var_Temperature);
  
  var_Humidity = (theBMEObject.readHumidity());
  Serial.print("Humidity: "); Serial.println(var_Humidity);
}

//===============================================================================

void ReadENSSensor()
{
  if (theENSObject.available()) 
    {
      theENSObject.measure(0);
  
      // TVOC and eCO2 from ENS chip
    
      var_TVOC = theENSObject.getTVOC(); 
      Serial.print("TVOC: "); Serial.print(var_TVOC); Serial.println("ppb\t");
    
      var_eCO2 = theENSObject.geteCO2();
      Serial.print("eCO2: "); Serial.print(var_eCO2); Serial.println("ppm\t");
    
      Serial.println("--------");
    
    }
}

//===============================================================================

void DetermineHumidityNeopixels()
{
  // Run through Humidity array, compare humidity reading integer to array integer values

  if (var_Humidity == 0)
    {
      PopulateHumidityNeopixels(0);
    }
  else
    {
      if (var_Humidity == 100)
        {
          PopulateHumidityNeopixels(11);
        }
      else
        {
          for (int i=0; i<12; i++)
            {
              if (var_Humidity > arr_Humidity[i] && var_Humidity <= arr_Humidity[i+1])
                {
                  PopulateHumidityNeopixels(i+1); // sends the index value to be used to light up the proper number of neopixels
                }
            }
        }
    }
}

//===============================================================================

void DetermineTVOCNeopixels() // use this line AFTER testing with array values
{
  // The value of TVOC can get WAY over 5000, but even at 5000, it represents an unhealthy environment.
  // So, I'm converting any value over 5000 to 5000 and maxing out the neopixel array at that point.
  if (var_TVOC > 5000)
    {
      var_TVOC = 5000;
    }

  // Run through TVOC array, compare TVOC reading integer to array integer values
  if (var_TVOC == 0)
    {
      PopulateTVOCNeopixels(0);
    }
  else
    {
      if (var_TVOC == 6000)
        {
          PopulateTVOCNeopixels(11);
        }
      else
        {
          for (int i=0; i<12; i++)
            {
              if (var_TVOC > arr_TVOC[i] && var_TVOC <= arr_TVOC[i+1])
                {
                  PopulateTVOCNeopixels(i+1); // sends the index value to be used to light up the proper number of neopixels
                }
            }
        }
    }
}

//===============================================================================

void PopulateHumidityNeopixels(int theIndexValue)
{
  thePixelObject_Humidity.clear();
  thePixelObject_Humidity.show();
  if (theIndexValue == 0)
    {
      // format of: setPixelColor(the individual pixel number, the pixel color to use)
      thePixelObject_Humidity.setPixelColor(0,arr_NeoPixelColors[0]);
      thePixelObject_Humidity.setBrightness(var_PixelBrightness);
      thePixelObject_Humidity.show(); // sends the updated neopixel values out to the neopixels
    }
  else
    {
      for (int h=0; h<(theIndexValue+1); h++) // runs through each neopixel address
        {
          // format of: setPixelColor(the individual pixel number, the pixel color to use)
          thePixelObject_Humidity.setPixelColor(h,arr_NeoPixelColors[h]);
          thePixelObject_Humidity.setBrightness(var_PixelBrightness);
          thePixelObject_Humidity.show(); // sends the updated neopixel values out to the neopixels
        }
    }
}

//===============================================================================

void PopulateTVOCNeopixels(int theIndexValue)
{
  thePixelObject_TVOC.clear();
  thePixelObject_TVOC.show();
  if (theIndexValue == 0)
    {
      // format of: setPixelColor(the individual pixel number, the pixel color to use)
      thePixelObject_TVOC.setPixelColor(0,arr_NeoPixelColors[0]);
      thePixelObject_TVOC.setBrightness(var_PixelBrightness);
      thePixelObject_TVOC.show(); // sends the updated neopixel values out to the neopixels
    }
  else
    {
      for (int h=0; h<(theIndexValue+1); h++) // runs through each neopixel address
        {
          // format of: setPixelColor(the individual pixel number, the pixel color to use)
          thePixelObject_TVOC.setPixelColor(h,arr_NeoPixelColors[h]);
          thePixelObject_TVOC.setBrightness(var_PixelBrightness);
          thePixelObject_TVOC.show(); // sends the updated neopixel values out to the neopixels
        }
    }
}

//===============================================================================

void PublishDataToDashboard()
{
  if(mqtt.Update())
    {
      theTemperatureObject.publish(var_Temperature);
      theHumidityObject.publish(var_Humidity);
      theTVOCObject.publish(var_TVOC);
      theeCO2Object.publish(var_eCO2);
    }

  //delay(10000);
}

//===============================================================================

void LoadPixelColorArray()
{
for (int c=0; c<4; c++)
  {
    arr_NeoPixelColors[c] = var_LowRangeColor;
    arr_NeoPixelColors[c+4] = var_MidRangeColor;
    arr_NeoPixelColors[c+8] = var_HighRangeColor;
  }
}

//===============================================================================

void ManagePixelRings_StartupDisplay()
{
  // Clear all pixels
  thePixelObject_Humidity.clear();
  thePixelObject_Humidity.show();
  thePixelObject_TVOC.clear();
  thePixelObject_TVOC.show();

  //Lights ON sequence
  for (int i=0; i<12; i++) // runs through each neopixel address
    {
      thePixelObject_Humidity.setPixelColor(i,arr_NeoPixelColors[i]);
      thePixelObject_TVOC.setPixelColor(i,arr_NeoPixelColors[i]);
               
      thePixelObject_Humidity.setBrightness(var_PixelBrightness);
      thePixelObject_Humidity.show(); // sends the updated neopixel values out to the neopixels

      thePixelObject_TVOC.setBrightness(var_PixelBrightness);
      thePixelObject_TVOC.show(); // sends the updated neopixel values out to the neopixels

      delay(75);
    }

  delay(1000); // a delay when all pixels are lit

  //Lights OFF sequence
  for (int i=11; i>-1; i--) // runs through each neopixel address
    {
      thePixelObject_Humidity.setPixelColor(i,0x000000);
      thePixelObject_TVOC.setPixelColor(i,0x000000);
               
      thePixelObject_Humidity.setBrightness(var_PixelBrightness);
      thePixelObject_Humidity.show(); // sends the updated neopixel values out to the neopixels

      thePixelObject_TVOC.setBrightness(var_PixelBrightness);
      thePixelObject_TVOC.show(); // sends the updated neopixel values out to the neopixels

      delay(75);
    }
}

//===============================================================================

// Function to connect and reconnect as necessary to the MQTT server.

void MQTT_connect() 
{
  int8_t ret;
 
  // Stop if already connected.
  if (mqtt.connected()) 
  {
    return;
  }
 
  Serial.print("Connecting to MQTT... ");
 
  while ((ret = mqtt.connect()) != 0) 
  { // connect will return 0 for connected
       Serial.println(mqtt.connectErrorString(ret));
       Serial.println("Retrying MQTT connection in 5 seconds...");
       mqtt.disconnect();
       delay(5000);  // wait 5 seconds
  }
  Serial.println("MQTT Connected!");
}

//===========================================================

void KeepConnectionAlive()
{
  // The default "keep-alive" timeframe is 5 minutes, or 300 seconds.
  // I will check for a publish event and if none has occured in the last 2
  // minutes, I will ping the server.

  if ((millis()-var_LastServerPing) > 120000) // (120 seconds)
    {
      Serial.println("Pinging the MQTT server");
      // ping the server to keep the mqtt connection alive
      if(! mqtt.ping()) // if I cannot ping the server, force a disconnect
         {
           Serial.println("Forcing an MQTT disconnect");
           mqtt.disconnect(); // forcing a disconnect will force a reconnection
           Serial.println("Attempting to reestablish MQTT connection");
         }
      var_LastServerPing = millis();
    }
  delay(5);  // just enough of a delay to allow completion of other tasks
}

The article was first published in hackster, January 6, 2023

cr: https://www.hackster.io/clint-wolf/signcompanyvoc-8e9340

author: Clint Wolf

License
All Rights
Reserved
licensBg
0