/*
* 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
}
Comments