How to Build an Air Quality Meter for Your Home or Office - Part 2
2021-12-06 | By Maker.io Staff
License: General Public License Arduino
The first part of this series discussed how to build an air quality meter for monitoring the air quality throughout your house, apartment, or office. This project consists of a single base station that runs on an Arduino Nano 33 IoT and several sensor devices that you can place in different rooms. The sensor devices measure the CO2 and VOC levels in a room using a CCS811 air quality sensor. Each sensor device then transmits the measured values to the central base station, which hosts a website that displays the measured values. Users can then view the website on any connected device within their home network to determine the air quality in each room. This part of the series discusses the firmware of both devices and how they function.
The Software that Enables the Sensor Devices
Each sensor device periodically sends the current sensor values and a 60-second average of each measured value. For that, the sensor devices use standard HTTP POST requests, and they encode the data as JSON objects.
Before you get started, use the Arduino IDE’s built-in library manager to install the WiFiNINA and Adafruit CCS811 libraries. Then, you should be able to upload the code that this section discusses. First, start by creating the objects that the program uses later:
/* Import and define statements */
// Create the objects that represent the sensor boards
Adafruit_CCS811 ccs_sensor;
// The client object allows each sensor device to periodically send
// data to the base station.
WiFiClient client;
/* Other variable definitions */
Below the variable definitions and function prototypes, you’ll find the setup()-method. This method first initializes the serial port for debugging purposes. Then, the function checks whether the WiFi module is present and working. If there’s a problem with the module, the program prints an error message and goes into an endless loop. If not, the Arduino establishes a connection to a wireless network using the credentials supplied at the top of the file.
While the Arduino waits for the WiFi connection to open, it prints a message to the serial console. Once the WiFi module is ready, the Arduino initializes the webserver and sensor board. Last, the setup() method requests a baseline measurement from the air quality sensor. The function then assigns that base value to all positions of the two arrays that hold the 60-second averages. Doing this is necessary to prevent the average values from starting at zero:
void setup()
{
// initialize the serial console for printing debug messages
Serial.begin(9600);
// check whether the WiFi module is connected and working
// More accurately: Check whether there's NO module attached
// (or the Arduino can't detect it for whatever reason)
if (WiFi.status() == WL_NO_MODULE)
{
Serial.println("WiFi Module not found!");
Serial.println("Halting...");
// don't continue if an error occured
while (true);
}
/* Debug messages omitted */
int status = WiFi.begin(SSID, PASS);
// attempt to connect to WiFi network:
while (status != WL_CONNECTED)
{
// Print period-characters to the serial console while
// the controller establishes a wifi connection
if(millis() - lastConnectionDelay > 250)
{
Serial.print(".");
lastConnectionDelay = millis();
}
}
/* Debug messages omitted */
// initialize the two sensor boards
bool ccs_sensor_initialized = initializeCCS811();
/* Debug messages omitted */
// Endless loop that effectively halts the program at this point
// if the Arduino couldn't initialize one of the sensors.
while (!ccs_sensor_initialized) {}
// The sensor works. Get a baseline measurement
getAirQualityData();
// And fill all positions of the average-arrays
// with that base value
for(int i = 0; i < BUFFER_SIZE; i++)
{
co2[i] = lastCO2;
vocs[i] = lastVOC;
}
client.setTimeout(2500);
/* Debug messages omitted */
}
I omitted some error outputs in this snippet, and I also removed the initializeCCS811() function. You can refer to this hookup guide if you’d like to learn more about the sensor and how to use it. You can also find the complete source code at the end of this article.
Next, the loop()-method periodically reads the current sensor values and transmits them to the base station. For that purpose, the program first checks how much time has elapsed since it last sent the values to the base station. If more than ten seconds have passed, the function requests the current air quality data from the sensor. Then, the function calculates the average of the sensor values from the past sixty seconds. Last, the program tries to establish a connection to the base station. If it succeeds, the program sends the sensor data and the calculated average values to the base station as a JSON-encoded object. If the Arduino couldn’t establish a connection, it outputs an error message:
void loop()
{
// Update the air-quality variables every ten seconds
if(millis() - lastSensorUpdate > 10000)
{
getAirQualityData();
float co2Sum = 0.0f;
float vocSum = 0.0f;
for(int i = 0; i < BUFFER_SIZE; i++)
{
co2Sum += co2[i];
vocSum += vocs[i];
}
averages[0] = co2Sum / (float)BUFFER_SIZE;
averages[1] = vocSum / (float)BUFFER_SIZE;
Serial.println("Updated sensor data!");
if(client.connect(BASE, 80))
{
Serial.println("Connected to base station!");
client.println(generateHTTPRequest());
client.stop();
Serial.print("Sent sensor data to the base station located at: ");
Serial.println(BASE);
}
else
{
Serial.print("Couldn't transmit data to the base station at: ");
Serial.println(BASE);
}
lastSensorUpdate = millis();
}
}
As you can see, I generate the HTTP request and JSON strings in external functions. Those functions each just return a string that contains the requested data:
String generateHTTPRequest(void)
{
String req = "";
req += "POST / HTTP/1.1\n";
req += "Connection: close\n";
req += "Host: ";
req += + BASE;
req += "\n";
req += "id: ";
req += DEVICE_ID;
req += "\n";
req += "\r\n";
req += generateJSONPayload();
req += "\n";
return req;
}
The generateJSONPayload()-function works similarly. You can view the entire source code at the end of this article.
The Hub Device’s Firmware
In contrast to the sensor device firmware, the center hub needs to open a web server that allows each sensor device to transmit data using an HTTP POST request. Then, the hub device needs to receive the data and decode the JSON-encoded object. For that purpose, you’ll have to install the ArduinoJSON library using the Arduino IDE’s built-in library manager. Besides handling POST requests, the center hub also manages incoming GET requests from a user’s web browser. If it receives a get request, the center hub responds with a simple HTTP website that displays a list of connected sensor devices alongside the most recent sensor values:
The setup function of the hub firmware looks almost the same as in the sensor device firmware. It, however, omits the CCS811 initialization code. Therefore, I won’t discuss the setup function of the hub device in detail. You can, again, find the entire code listing at the end of this article.
The loop method of the center device is more interesting, as it greatly differs from the one found in the sensor device’s firmware. Instead of periodically requesting updates from each sensor device, the center hub listens for incoming requests. Regardless of the type, the firmware first reads the entire HTTP request header and body. The program then determines whether it received a POST or a GET request. If it receives a GET request, the Arduino sends a simple HTTP response that contains the HTML website you can see above:
void loop()
{
// Check whether a client requested the web site
WiFiClient client = webserver.available();
// A client established a connection to the base station.
// At this point, the program doesn't know whether it's one
// of the sensor devices or a user checking the website.
if (client)
{
String header = "";
String payload = "";
bool headerReceived = false;
while (client.connected())
{
// Read the entire request from the client
if (client.available())
{
String line = client.readStringUntil('\n');
// The current line is part of the header
if(!headerReceived)
header += line;
else
payload += line;
// The last line of the header is empty
if(line == "\r")
headerReceived = true;
}
else
break;
}
// The header contained a GET request ( -> user )
if(header.indexOf("GET") != -1)
{
client.println(generateHTTPResponse());
}
// The header contained a POST request ( -> sensor device )
else if(header.indexOf("POST") != -1)
{
/* POST request handling omitted */
}
else
{
Serial.println("HTTP method not supported!");
}
// close the connection
client.stop();
}
}
Until now, I omitted the POST request handler of the loop function because I wanted to discuss that part in more detail. The POST request handler first creates a dynamic JSONDocument object with a size of 1024 bytes. Then the program passes in the HTTP payload that it received from one of the sensor devices. The sketch outputs a message to the serial console if the ArduinoJSON library encounters an error while parsing the JSON document. Otherwise, the program extracts the relevant information from the JSON-formatted payload:
void loop()
{
/* Parts omitted; See snippet above */
if (client)
{
String header = "";
String payload = "";
/* HTTP request parsing */
// The header contained a GET request ( -> user )
if(header.indexOf("GET") != -1)
{
/* GET request handling (See above) */
}
// The header contained a POST request ( -> sensor device )
else if(header.indexOf("POST") != -1)
{
DynamicJsonDocument doc(1024);
DeserializationError err = deserializeJson(doc, payload);
if(err)
{
Serial.print("deserializeJson() failed with code: ");
Serial.println(err.f_str());
}
else
{
String id = doc["id"];
float lastVoc = doc["voc"];
float lastCO2 = doc["co2"];
float vocAvg = doc["voc_average"];
float co2Avg = doc["co2_average"];
ClientList* node = clients;
if(node == NULL)
{
Serial.println("list is empty!");
clients = node = new ClientList();
}
else
{
// Iterate over all known sensor devices until the program
// either reaches the last one or finds the one it's looking for
while(node->next != NULL && node->id != id)
node = node->next;
// If the program reaches the last node without finding the current
// ID, the program creates a new last node
if(node->id != id)
{
node->next = new ClientList();
node = node->next;
}
}
node->id = id;
node->voc = lastVoc;
node->co2 = lastCO2;
node->averageCO2 = co2Avg;
node->averageVOC = vocAvg;
}
}
else
{
Serial.println("HTTP method not supported!");
}
/* Parts omitted */
}
}
The base station keeps a list of sensor devices that it knows. In the snippet above, the node pointer references the first entry of that list. If the first entry is NULL, the program creates a new list and sets the values of the first list entry to the values it just received in the most recent POST request.
If the list already contains items, the program loops through all the list entries. While doing this, the program checks the ID of each list entry against the ID that came with the newest POST request. If the IDs match, the program updates the values of the list entry with the same ID. If the program reaches the end of the list, it creates a new list element for the sensor station and stores the transmitted values.
Summary
This project consists of two different Arduino sketches. The firmware that runs on each sensor device first initializes the Wi-Fi module and the sensor breakout board. Then, it periodically queries the current sensor values. Apart from storing the most recent sensor readings, the program also stores average values collected over the last minute. Whenever the firmware reads the sensor values, it also transmits the result to the base station in the form of a JSON-encoded string.
The firmware of the base station runs a simple HTTP server that listens for GET and POST requests. If the program detects a GET request, the base station replies with a simple HTTP website that displays a table of connected devices and the data each of them transmitted. In case of receiving a POST request, the program parses the JSON data that comes with the request. The firmware keeps a list of sensor devices that have already sent a request. If the most recent request came from a known sensor device, the program updates the list entry of that device. Otherwise, the firmware adds a new entry to that list.
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum