ESP32 Indoor Air Quality Monitoring

  • Author: Bogdan-Ionuț Spînu
  • Email: bogdan_ionut.spinu@stud.acs.pub.ro
  • Master: MPI

Introduction

Indoor air quality (IAQ) has become a critical concern in modern urban living, impacting health, productivity, and overall well-being. Poor IAQ can lead to a range of health issues, from minor irritations to severe respiratory conditions. With the increasing amount of time people spend indoors, whether at home, in offices, or in educational institutions, monitoring and maintaining optimal air quality has never been more essential. This project leverages the capabilities of the ESP32 microcontroller, Sensirion Sen55 air sensor, and ThingsBoard platform to create a real-time IAQ monitoring system. The aim is to provide a comprehensive solution that enables continuous assessment and improvement of indoor air environments.

Context

Indoor air pollution stems from various sources, including household cleaning agents, building materials, tobacco smoke, and even outdoor air pollution infiltrating indoor spaces. Common indoor pollutants include particulate matter (PM2.5 and PM10), volatile organic compounds (VOCs), carbon dioxide (CO2), and other harmful gases. These pollutants can adversely affect cognitive function, sleep quality, and overall health, particularly in vulnerable populations such as children, the elderly, and individuals with preexisting health conditions. Traditional methods of assessing IAQ often involve sporadic testing or the use of standalone air quality monitors, which can be both costly and inefficient. As a result, there is a growing need for affordable, scalable, and easy-to-implement solutions that provide real-time data and insights into indoor air quality. The integration of Internet of Things (IoT) technology offers a promising approach to address these challenges.

Hardware

  • ESP32-C3-DevKitM-1
  • Sensirion SEN55

The Sensirion Sen55 sensor is an advanced air quality monitoring module designed for high-precision indoor air assessments. It combines the ability to measure particulate matter (PM1.0, PM2.5, PM4.0, and PM10) using laser-based light scattering technology, and volatile organic compounds (VOCs) through metal oxide semiconductor (MOS) technology. Additionally, it provides accurate readings of relative humidity and temperature. Its compact design facilitates easy integration into various devices and systems, maintaining robust performance in a small form factor. The Sen55 communicates via an I²C interface, simplifying its connection with microcontrollers like the ESP32.

  • JST GHR-06-S connector with 6 jumper wires
  • USB-A to USB-C cable and an old Samsung charger

Software

ESP-IDF

Dependencies

I have developed this project within the ESP-IDF framework, designed specifically for Espressif's range of microcontrollers, including the popular ESP32 series. I have used the following external libraries that I registered as components of the project:

Code Snippets

On startup, establish the WiFi connection; a WIFI_EVENT_STA_CONNECTED event will also trigger attempting to connect to the MQTT broker provided by ThingsBoard, followed by initializing I2C communication with Sen55 and writing the start measurement command (0x0021):

    nvs_flash_init();
    wifi_connection();
 
    int16_t error = 0;
    error = sen55_init();
    if (error)
    {
        ESP_LOGE(SEN, "Error executing sen55_init(): %i\n", error);
        return;
    }
 
    // Start Measurement
    error = sen5x_start_measurement();
    if (error)
    {
        ESP_LOGE(SEN, "Error executing sen5x_start_measurement(): %i\n", error);
        return;
    }

Initializing I2C serial communication with the sensor and setting a 2°C compensation for the temperature sensor due to device self heating:

int16_t sen55_init()
{
    int16_t error = 0;
    struct esp32_i2c_config i2c_config = {
        .freq = 100000,
        .addr = 0x69,
        .port = I2C_NUM_0,
        .sda = GPIO_NUM_0,
        .scl = GPIO_NUM_1,
        .sda_pullup = true,
        .scl_pullup = true
    };
 
    error = sensirion_i2c_config_esp32(&i2c_config);
    if (error)
    {
        ESP_LOGE(SEN, "Error executing  sensirion_i2c_config_esp32(): %i\n", error);
        return error;
    }
 
    sensirion_i2c_hal_init();
 
    error = sen5x_device_reset();
    if (error)
    {
        ESP_LOGE(SEN, "Error executing sen5x_device_reset(): %i\n", error);
        return error;
    }
 
    ...
 
    float temp_offset = -2.5f;
    int16_t default_slope = 0;
    uint16_t default_time_constant = 0;
    error = sen5x_set_temperature_offset_parameters(
        (int16_t)(200 * temp_offset), default_slope, default_time_constant);
}

Read sensor data every 30 seconds in an infinite loop, process the raw data into a JSON message and publish it to the MQTT topic provided by ThingsBoard’s MQTT broker:

    while (1)
    {
        // Read and process sensor data in a loop
        vTaskDelay(pdMS_TO_TICKS(30000)); // Delay for 30 second
 
        uint16_t mass_concentration_pm1;
        uint16_t mass_concentration_pm2p5;
        uint16_t mass_concentration_pm4;
        uint16_t mass_concentration_pm10;
        int16_t ambient_humidity;
        int16_t ambient_temperature;
        int16_t voc_index;
        int16_t nox_index;
 
        error = sen5x_read_measured_values(
            &mass_concentration_pm1, &mass_concentration_pm2p5,
            &mass_concentration_pm4, &mass_concentration_pm10,
            &ambient_humidity, &ambient_temperature, &voc_index, &nox_index);
 
        if (error)
        {
            ESP_LOGI(SEN, "Error executing sen5x_read_measured_values(): %i\n", error);
        }
        else
        {
            // Process raw sensor values
            double pm1 = roundDouble(mass_concentration_pm1 / 10.0f);
            double pm2p5 = roundDouble(mass_concentration_pm2p5 / 10.0f);
            double pm4 = roundDouble(mass_concentration_pm4 / 10.0f);
            double pm10 = roundDouble(mass_concentration_pm10 / 10.0f);
            double humidity = ambient_humidity == 0x7fff ? 0.0f : roundDouble(ambient_humidity / 100.0f);
            double temperature = ambient_temperature == 0x7fff ? 0.0f : roundDouble(ambient_temperature / 200.0f);
            double voc = voc_index == 0x7fff ? 0.0f : roundDouble(voc_index / 10.0f);
            double nox = nox_index == 0x7fff ? 0.0f : roundDouble(nox_index / 10.0f);
            double iaq_index = roundDouble(calculateIAQIndex(pm1, pm2p5, pm4, pm10, humidity, temperature, voc, nox));
 
            cJSON *root = cJSON_CreateObject();
            cJSON_AddNumberToObject(root, "pm1", pm1);
            cJSON_AddNumberToObject(root, "pm2.5", pm2p5);
            cJSON_AddNumberToObject(root, "pm4", pm4);
            cJSON_AddNumberToObject(root, "pm10", pm10);
            cJSON_AddNumberToObject(root, "humidity", humidity);
            cJSON_AddNumberToObject(root, "temperature", temperature);
            cJSON_AddNumberToObject(root, "tvoc", voc);
            cJSON_AddNumberToObject(root, "nox", nox);
            cJSON_AddNumberToObject(root, "iaq", iaq_index);
 
            char *json_message = cJSON_PrintUnformatted(root);
            cJSON_Delete(root);
            ESP_LOGI(SEN, "JSON Message: %s", json_message);
 
            if (mqtt_connected)
            {
                esp_mqtt_client_publish(mqtt_client, MQTT_TOPIC, json_message, 0, 1, 0);
                ESP_LOGI(SEN, "Sensor data published to MQTT");
            }
            else
            {
                ESP_LOGE(SEN, "Failed to publish sensor data to MQTT");
            }
            free(json_message);
        }
    }

Threshold values for the indoor air pollutants measured by Sen55, according to various guidelines from WHO, EPA (US Environmental Protection Agency), Reset Air and WELL Building standards:

const double PM1_THRESHOLD = 40;
const double PM2P5_THRESHOLD = 15;
const double PM2P5_THRESHOLD_DANGEROUS = 90;
const double PM4_THRESHOLD = 30;
const double PM10_THRESHOLD = 40;
const double PM10_THRESHOLD_DANGEROUS = 250;
const double HUMIDITY_THRESHOLD_HIGH = 55.0;
const double HUMIDITY_THRESHOLD_LOW = 35.0;
const double TEMPERATURE_THRESHOLD_HIGH = 24.0;
const double TEMPERATURE_THRESHOLD_LOW = 18.0;
const double VOC_THRESHOLD = 100;
const double VOC_THRESHOLD_DANGEROUS = 610;
const double NOX_THRESHOLD = 53;

IAQ index formula – will return a number between 0 and 100 indicating the general quality of the air from the last measurements of the sensor:

double calculateIAQIndex(double pm1, double pm2p5, double pm4, double pm10,
                         double humidity, double temperature, double voc, double nox)
{
    // Define a non-linear penalty function
    double nonLinearPenalty(double deviation, double threshold) {
        if (deviation <= 0.0) {
            return 0.0; // No penalty if value is within or equal to threshold
        } else {
            double ratio = deviation / threshold;
            return ratio * ratio * 100.0; // Squaring the ratio for non-linear penalty
        }
    }
 
    double scalePenalty(double value, double threshold_worrying, double threshold_dangerous) {
        if (value <= threshold_worrying) {
            return 0.0; // No penalty if value is within the good range
        } else if (value <= threshold_dangerous) {
            // Linear penalty from 0 to 50 between threshold_worrying and threshold_dangerous
            return 50.0 * (value - threshold_worrying) / (threshold_dangerous - threshold_worrying);
        } else {
            // Logarithmic penalty from 50 to 100 beyond threshold_dangerous
            return 50.0 + 50.0 * log(1 + (value - threshold_dangerous) / threshold_dangerous);
        }
    }
 
    // Calculate penalties for each parameter
    double penalty_pm1 = scalePenalty(pm1, PM1_THRESHOLD, PM10_THRESHOLD_DANGEROUS);
    double penalty_pm2p5 = scalePenalty(pm2p5, PM2P5_THRESHOLD, PM2P5_THRESHOLD_DANGEROUS);
    double penalty_pm4 = scalePenalty(pm4, PM4_THRESHOLD, PM10_THRESHOLD_DANGEROUS);
    double penalty_pm10 = scalePenalty(pm10, PM10_THRESHOLD, PM10_THRESHOLD_DANGEROUS);
    double penalty_humidity = nonLinearPenalty(humidity - HUMIDITY_THRESHOLD_HIGH, HUMIDITY_THRESHOLD_HIGH) +
                              nonLinearPenalty(HUMIDITY_THRESHOLD_LOW - humidity, HUMIDITY_THRESHOLD_LOW);
    double penalty_temperature = nonLinearPenalty(temperature - TEMPERATURE_THRESHOLD_HIGH, TEMPERATURE_THRESHOLD_HIGH) +
                                 nonLinearPenalty(TEMPERATURE_THRESHOLD_LOW - temperature, TEMPERATURE_THRESHOLD_LOW);
    double penalty_voc = scalePenalty(voc, VOC_THRESHOLD, VOC_THRESHOLD_DANGEROUS);
    double penalty_nox = nonLinearPenalty(nox - NOX_THRESHOLD, NOX_THRESHOLD);
 
    // Calculate total penalty
    double total_penalty = penalty_pm1 + penalty_pm2p5 + penalty_pm4 +
                           penalty_pm10 + 3 * (penalty_humidity + penalty_temperature) +
                           penalty_voc + penalty_nox;
 
    // Calculate IAQ index
    double iaq_index = 100.0 - total_penalty;
 
    // Ensure IAQ index is within the range [0, 100]
    if (iaq_index < 0.0) {
        iaq_index = 0.0;
    }
 
    return iaq_index;
}

ThingsBoard

I have used the open-source ThingsBoard platform customized by UPB with my university account for telemetry storage and visualization: http://digitaltwin.upb.ro:8080/home.

I have configured my device inside ThingsBoard to receive and process JSON data received via MQTT from the ESP32 board, store the timeseries measurements inside a PostgreSQL database and created a dashboard to display the evolution of IAQ parameters over time:

Challenges

Light sleep mode - optimize power consumption by shutting down wireless peripherals and reducing most voltage supply to RAM and CPUs, and use the RTC controller's internal timer to wake up the ESP32 board every 30 seconds to read and send data from sensor (esp_sleep_enable_timer_wakeup()). Due to the WiFi module being powered down and reconnecting every session, which leads to countless reconnections to the MQTT broker that take up more CPU resources and bandwidth than just sending the message itself, I have decided to not use sleep mode anymore.

References

  1. WHO global air quality guidelines. Particulate matter (PM2.5 and PM10), ozone, nitrogen dioxide, sulfur dioxide and carbon monoxide. Geneva: World Health Organization; 2021.
iothings/proiecte/2023sric/esp32-iaq.txt · Last modified: 2024/05/30 10:12 by bogdan_ionut.spinu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0